package browser

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

// CdpStorageState agrupa cookies, localStorage y sessionStorage capturados de una
// pestaña activa.
type CdpStorageState struct {
	Cookies        []map[string]any  `json:"cookies"`
	LocalStorage   map[string]string `json:"localStorage"`
	SessionStorage map[string]string `json:"sessionStorage"`
}

// isStorageAccessDenied reconoce el error de CdpEvaluate cuando el origen no
// permite acceder a window.localStorage/sessionStorage (about:blank, chrome://,
// data:, sandbox sin allow-same-origin): el navegador lanza SecurityError. Es
// puro: decide a partir del texto del error. Distingue ese caso legítimo (no hay
// storage que guardar -> {}) de un error real (conexión caída, JS roto) que SÍ
// debe propagarse para no escribir una sesión incompleta en silencio.
func isStorageAccessDenied(err error) bool {
	if err == nil {
		return false
	}
	s := err.Error()
	return strings.Contains(s, "SecurityError") ||
		strings.Contains(s, "Access is denied") ||
		strings.Contains(s, "operation is insecure") ||
		strings.Contains(s, "denied for this document")
}

// readWebStorage lee window.<store> (localStorage|sessionStorage) como mapa.
// Distingue tres casos:
//   - storage accesible (con o sin datos) -> (mapa, nil)
//   - origen sin storage accesible (about:blank, chrome://) -> ({}, nil)
//   - error REAL de evaluación (conexión caída, JS roto, JSON inválido) -> (nil, error)
func readWebStorage(c *CDPConn, store string) (map[string]string, error) {
	raw, err := CdpEvaluate(c, "JSON.stringify(Object.assign({}, window."+store+"))")
	if err != nil {
		if isStorageAccessDenied(err) {
			// Origen sin storage accesible: vacío legítimo, no error.
			return map[string]string{}, nil
		}
		return nil, fmt.Errorf("leer %s: %w", store, err)
	}
	if raw == "" || raw == "undefined" || raw == "null" {
		return map[string]string{}, nil
	}
	var m map[string]string
	if err := json.Unmarshal([]byte(raw), &m); err != nil {
		return nil, fmt.Errorf("parsear %s: %w", store, err)
	}
	return m, nil
}

// cookieDomainMatchesHost indica si una cookie con `domain` aplica al `host` dado.
// Cubre el caso de dominios con punto inicial (".example.com") y subdominios.
func cookieDomainMatchesHost(domain, host string) bool {
	if domain == "" || host == "" {
		return false
	}
	d := strings.TrimPrefix(domain, ".")
	return host == d || strings.HasSuffix(host, "."+d)
}

// storageStateToMaps convierte []any (respuesta CDP) a []map[string]any.
func storageStateToMaps(raw []any) []map[string]any {
	out := make([]map[string]any, 0, len(raw))
	for _, item := range raw {
		if m, ok := item.(map[string]any); ok {
			out = append(out, m)
		}
	}
	return out
}

// CdpSaveStorageState captura cookies y localStorage de la pagina actual y los
// escribe como JSON a outPath. Permite restaurar la sesion autenticada en
// ejecuciones posteriores sin repetir el login.
func CdpSaveStorageState(c *CDPConn, outPath string) error {
	if c == nil {
		return fmt.Errorf("cdp save storage state: conexion nula")
	}
	if outPath == "" {
		return fmt.Errorf("cdp save storage state: outPath vacio")
	}

	// Habilitar dominio Network para acceder a las cookies
	if _, err := c.sendCDP("Network.enable", nil); err != nil {
		return fmt.Errorf("cdp save storage state: Network.enable: %w", err)
	}

	// Obtener todas las cookies del perfil
	res, err := c.sendCDP("Network.getAllCookies", nil)
	if err != nil {
		return fmt.Errorf("cdp save storage state: getAllCookies: %w", err)
	}

	var cookies []map[string]any
	if rawCookies, ok := res["cookies"].([]any); ok {
		cookies = storageStateToMaps(rawCookies)
	} else {
		cookies = []map[string]any{}
	}

	// Filtrar al origen actual: Network.getAllCookies devuelve cookies de TODOS
	// los dominios del perfil. Para guardar "la sesión de ESTE sitio" solo
	// conservamos las que aplican al host cargado, evitando arrastrar cookies de
	// otros sitios visitados en la misma sesión del navegador.
	if host, herr := CdpEvaluate(c, "location.hostname"); herr == nil {
		host = strings.TrimSpace(host)
		if host != "" && host != "undefined" {
			filtered := make([]map[string]any, 0, len(cookies))
			for _, ck := range cookies {
				dom, _ := ck["domain"].(string)
				if cookieDomainMatchesHost(dom, host) {
					filtered = append(filtered, ck)
				}
			}
			cookies = filtered
		}
	}

	// Capturar localStorage y sessionStorage del origen actualmente cargado. Un
	// error real (no un origen sin storage) aborta el guardado: mejor fallar que
	// escribir una sesión incompleta que el caller creería válida.
	localStorage, err := readWebStorage(c, "localStorage")
	if err != nil {
		return fmt.Errorf("cdp save storage state: %w", err)
	}
	sessionStorage, err := readWebStorage(c, "sessionStorage")
	if err != nil {
		return fmt.Errorf("cdp save storage state: %w", err)
	}
	state := CdpStorageState{
		Cookies:        cookies,
		LocalStorage:   localStorage,
		SessionStorage: sessionStorage,
	}

	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("cdp save storage state: marshal: %w", err)
	}

	if err := os.WriteFile(outPath, data, 0644); err != nil {
		return fmt.Errorf("cdp save storage state: escribir archivo: %w", err)
	}

	return nil
}
