package browser

import (
	"encoding/json"
	"fmt"
	"strings"
	"sync"
)

// frameCtxCache mapea frameID -> executionContextId del isolated world creado
// para ese frame. Evita pagar Page.createIsolatedWorld en cada CdpEvalInFrame.
// Es puro y testeable de forma aislada (su propio mutex, sin tocar CDP).
type frameCtxCache struct {
	mu sync.Mutex
	m  map[string]int
}

func newFrameCtxCache() *frameCtxCache {
	return &frameCtxCache{m: map[string]int{}}
}

func (f *frameCtxCache) get(frameID string) (int, bool) {
	f.mu.Lock()
	defer f.mu.Unlock()
	id, ok := f.m[frameID]
	return id, ok
}

func (f *frameCtxCache) set(frameID string, ctxID int) {
	f.mu.Lock()
	f.m[frameID] = ctxID
	f.mu.Unlock()
}

func (f *frameCtxCache) invalidate(frameID string) {
	f.mu.Lock()
	delete(f.m, frameID)
	f.mu.Unlock()
}

// isStaleContextError reconoce el error de CDP cuando un executionContextId
// cacheado ya no existe (el frame recargó/navegó y su isolated world murió). Es
// puro: decide a partir del texto del error. Permite reintentar recreando el
// mundo en vez de fallar.
func isStaleContextError(err error) bool {
	if err == nil {
		return false
	}
	s := err.Error()
	return strings.Contains(s, "Cannot find context") ||
		strings.Contains(s, "context with specified id") ||
		strings.Contains(s, "Execution context was destroyed") ||
		strings.Contains(s, "uniqueContextId")
}

// frameCtxCacheLazy devuelve el cache de contextos del frame de esta conexion,
// inicializandolo en el primer uso. El mutex de CDPConn solo protege este
// lazy-init del puntero.
func (c *CDPConn) frameCtxCacheLazy() *frameCtxCache {
	c.frameCtxMu.Lock()
	defer c.frameCtxMu.Unlock()
	if c.frameCtx == nil {
		c.frameCtx = newFrameCtxCache()
	}
	return c.frameCtx
}

// createIsolatedWorld crea un mundo aislado en el frame y devuelve su
// executionContextId.
func createIsolatedWorld(c *CDPConn, frameID string) (int, error) {
	ctxRes, err := c.sendCDP("Page.createIsolatedWorld", map[string]any{
		"frameId":              frameID,
		"worldName":            "fn_registry_isolated",
		"grantUniversalAccess": false,
	})
	if err != nil {
		return 0, fmt.Errorf("createIsolatedWorld: %w", err)
	}
	ctxIDRaw, ok := ctxRes["executionContextId"]
	if !ok {
		return 0, fmt.Errorf("createIsolatedWorld: executionContextId no encontrado en respuesta")
	}
	ctxID, ok := ctxIDRaw.(float64)
	if !ok {
		return 0, fmt.Errorf("createIsolatedWorld: executionContextId tipo inesperado: %T", ctxIDRaw)
	}
	return int(ctxID), nil
}

// evalInFrameContext ejecuta la expresion en el executionContextId dado y
// serializa el resultado como string (mismo patron que CdpEvaluate).
func evalInFrameContext(c *CDPConn, ctxID int, frameID, expression string) (string, error) {
	evRes, err := c.sendCDP("Runtime.evaluate", map[string]any{
		"expression":    expression,
		"contextId":     ctxID,
		"returnByValue": true,
		"awaitPromise":  true,
	})
	if err != nil {
		return "", fmt.Errorf("Runtime.evaluate: %w", err)
	}

	if exc, ok := evRes["exceptionDetails"]; ok && exc != nil {
		excMap, _ := exc.(map[string]any)
		text, _ := excMap["text"].(string)
		return "", fmt.Errorf("excepcion JS en frame %q: %s", frameID, text)
	}

	resVal, ok := evRes["result"].(map[string]any)
	if !ok {
		return "", fmt.Errorf("resultado inesperado: %v", evRes)
	}

	value, ok := resVal["value"]
	if !ok {
		typ, _ := resVal["type"].(string)
		return typ, nil
	}
	if s, ok := value.(string); ok {
		return s, nil
	}
	b, err := json.Marshal(value)
	if err != nil {
		return fmt.Sprintf("%v", value), nil
	}
	return string(b), nil
}

// CdpEvalInFrame ejecuta una expresion JavaScript en el contexto aislado de un iframe
// especifico usando Page.createIsolatedWorld + Runtime.evaluate con el contextId del frame.
// Retorna el resultado serializado como string.
//
// Cachea el executionContextId por frameID en la conexion: la primera llamada
// crea el mundo aislado, las siguientes lo reutilizan. Si el contexto cacheado
// caducó (el frame navegó/recargó), recrea el mundo una vez y reintenta.
func CdpEvalInFrame(c *CDPConn, frameID, expression string) (string, error) {
	if c == nil {
		return "", fmt.Errorf("cdp eval in frame: conexion nula")
	}
	if frameID == "" {
		return "", fmt.Errorf("cdp eval in frame: frameID vacio")
	}

	// Page.enable es idempotente; necesario antes de crear mundos aislados.
	if _, err := c.sendCDP("Page.enable", nil); err != nil {
		return "", fmt.Errorf("cdp eval in frame: Page.enable: %w", err)
	}

	cache := c.frameCtxCacheLazy()

	ctxID, cached := cache.get(frameID)
	if !cached {
		newID, err := createIsolatedWorld(c, frameID)
		if err != nil {
			return "", fmt.Errorf("cdp eval in frame: %w", err)
		}
		ctxID = newID
		cache.set(frameID, ctxID)
	}

	out, evErr := evalInFrameContext(c, ctxID, frameID, expression)
	if evErr != nil && cached && isStaleContextError(evErr) {
		// El contexto cacheado murió (frame recargó). Recrear una vez.
		cache.invalidate(frameID)
		newID, err := createIsolatedWorld(c, frameID)
		if err != nil {
			return "", fmt.Errorf("cdp eval in frame: %w", err)
		}
		cache.set(frameID, newID)
		out, evErr = evalInFrameContext(c, newID, frameID, expression)
	}
	if evErr != nil {
		return "", fmt.Errorf("cdp eval in frame: %w", evErr)
	}
	return out, nil
}
