package browser

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

// fillNodeInfo es el diagnostico que devuelve fillPrepare tras inspeccionar y
// preparar el nodo en el contexto JS de la pagina. Replica la logica de
// InjectedScript.fill de Playwright sin usar el "native value setter": para los
// campos de texto/contenteditable selecciona el contenido previo y deja que el
// motor inserte el valor con eventos confiables (ruta needsinput); para los
// inputs especiales fija el valor y dispara los eventos (ruta setvalue).
type fillNodeInfo struct {
	// Route es "needsinput" (hay que insertar el valor via Input.insertText),
	// "setvalue" (ya se fijo el valor + eventos, nada mas que hacer) o "" si hubo error.
	Route string `json:"route"`
	// Error describe por que el nodo no se puede rellenar (no editable, readonly,
	// disabled, oculto, tipo no soportado). Vacio si todo OK.
	Error string `json:"error"`
}

// resolveObjectID resuelve un backendDOMNodeId a un Runtime objectId, para poder
// ejecutar JS con `this` apuntando a ese nodo concreto via Runtime.callFunctionOn.
func resolveObjectID(c *CDPConn, backendNodeID int) (string, error) {
	res, err := c.sendCDP("DOM.resolveNode", map[string]any{"backendNodeId": backendNodeID})
	if err != nil {
		return "", fmt.Errorf("resolveNode ref %d: %w", backendNodeID, err)
	}
	obj, _ := res["object"].(map[string]any)
	objID, _ := obj["objectId"].(string)
	if objID == "" {
		return "", fmt.Errorf("sin objectId para ref %d", backendNodeID)
	}
	return objID, nil
}

// callFunctionOnJSON ejecuta functionDeclaration con `this` = objectId, pasando
// args como argumentos posicionales, y deserializa el valor de retorno (por valor)
// en out. La funcion JS debe devolver un objeto serializable.
func callFunctionOnJSON(c *CDPConn, objectID, functionDeclaration string, args []any, out any) error {
	callArgs := make([]any, len(args))
	for i, a := range args {
		callArgs[i] = map[string]any{"value": a}
	}
	res, err := c.sendCDP("Runtime.callFunctionOn", map[string]any{
		"objectId":            objectID,
		"functionDeclaration": functionDeclaration,
		"arguments":           callArgs,
		"returnByValue":       true,
		"awaitPromise":        true,
	})
	if err != nil {
		return err
	}
	if exc, ok := res["exceptionDetails"]; ok && exc != nil {
		excMap, _ := exc.(map[string]any)
		text, _ := excMap["text"].(string)
		return fmt.Errorf("excepcion JS: %s", text)
	}
	if out == nil {
		return nil
	}
	resVal, ok := res["result"].(map[string]any)
	if !ok {
		return fmt.Errorf("resultado inesperado: %v", res)
	}
	b, err := json.Marshal(resVal["value"])
	if err != nil {
		return fmt.Errorf("marshal valor de retorno: %w", err)
	}
	return json.Unmarshal(b, out)
}

// fillPrepareJS es la funcion JS (con `this` = elemento) que valida editabilidad,
// detecta el tipo y prepara el nodo. Replica InjectedScript.fill de Playwright:
// NO usa el native value setter para text/textarea/contenteditable (selecciona el
// valor previo y devuelve "needsinput" para que Input.insertText, con eventos
// confiables del motor, haga que React/Vue reconcilien solos). Para inputs
// especiales fija el valor y dispara input/change con {bubbles, composed}.
//
// arg[0] = value (string).
const fillPrepareJS = `function(value){
  var el = this;
  if (!el || el.nodeType !== 1) return {route:"", error:"el #ref no es un elemento"};
  // Visibilidad: rect con area + no display:none/visibility:hidden.
  var rect = el.getBoundingClientRect();
  var style = el.ownerDocument.defaultView.getComputedStyle(el);
  if (style.visibility === "hidden" || style.display === "none" || (rect.width === 0 && rect.height === 0))
    return {route:"", error:"elemento no visible"};
  var tag = el.nodeName.toLowerCase();
  if (tag === "input") {
    var type = (el.type || "text").toLowerCase();
    if (el.disabled) return {route:"", error:"input deshabilitado"};
    if (el.readOnly) return {route:"", error:"input es readonly"};
    var kSetValue = {color:1, date:1, time:1, "datetime-local":1, month:1, range:1, week:1};
    var kTypeInto = {"":1, email:1, number:1, password:1, search:1, tel:1, text:1, url:1};
    if (!kTypeInto[type] && !kSetValue[type])
      return {route:"", error:"input de tipo '"+type+"' no se puede rellenar"};
    if (type === "number") {
      value = value.trim();
      if (value !== "" && isNaN(Number(value)))
        return {route:"", error:"no se puede escribir texto en input[type=number]"};
    }
    if (type === "color") value = value.toLowerCase();
    if (kSetValue[type]) {
      value = value.trim();
      el.focus();
      el.value = value;
      if (el.value !== value) return {route:"", error:"valor malformado para input[type="+type+"]"};
      el.dispatchEvent(new Event("input", {bubbles:true, composed:true}));
      el.dispatchEvent(new Event("change", {bubbles:true}));
      return {route:"setvalue", error:""};
    }
    // Ruta needsinput: seleccionar el valor previo para que insertText lo reemplace.
    el.select();
    el.focus();
    return {route:"needsinput", error:""};
  }
  if (tag === "textarea") {
    if (el.disabled) return {route:"", error:"textarea deshabilitado"};
    if (el.readOnly) return {route:"", error:"textarea es readonly"};
    el.selectionStart = 0;
    el.selectionEnd = el.value.length;
    el.focus();
    return {route:"needsinput", error:""};
  }
  if (el.isContentEditable) {
    el.focus();
    var range = el.ownerDocument.createRange();
    range.selectNodeContents(el);
    var sel = el.ownerDocument.defaultView.getSelection();
    if (sel) { sel.removeAllRanges(); sel.addRange(range); }
    return {route:"needsinput", error:""};
  }
  return {route:"", error:"el elemento no es input, textarea ni [contenteditable]"};
}`

// fillVerifyJS lee el valor actual del nodo (input.value/textarea.value o
// textContent de contenteditable) para verificar que el fill surtio efecto.
// arg[0] = expected (string). Devuelve {ok:bool, got:string, verifiable:bool}.
const fillVerifyJS = `function(expected){
  var el = this;
  var tag = el.nodeName.toLowerCase();
  if (tag === "input" || tag === "textarea") {
    var type = tag === "input" ? (el.type||"text").toLowerCase() : "text";
    var got = String(el.value);
    var exp = expected;
    if (type === "number" || type === "color" || type === "date" || type === "time" ||
        type === "datetime-local" || type === "month" || type === "range" || type === "week") {
      exp = expected.trim();
      if (type === "color") exp = exp.toLowerCase();
    }
    return {ok: got === exp, got: got, verifiable: true};
  }
  // contenteditable: no verificable de forma fiable (el motor normaliza el HTML).
  return {ok: true, got: String(el.textContent||""), verifiable: false};
}`

// CdpFill rellena un campo de texto controlado por frameworks (React/Vue) de
// forma robusta, estilo Playwright. backendNodeID es un backendDOMNodeId (el #ref
// del AX outline de page_perceive).
//
// Comportamiento (replica InjectedScript.fill):
//  1. Valida visible + enabled + editable (no readonly/disabled) en el contexto JS.
//  2. Enfoca el nodo.
//  3. Detecta el tipo:
//     - text/textarea/email/search/url/tel/password/number/contenteditable: ruta
//       "needsinput" — selecciona el valor previo y luego inserta value con
//       Input.insertText (eventos input/beforeinput confiables del motor; React/Vue
//       reconcilian solos). Con value=="" borra la seleccion (Delete) en vez de insertar.
//     - color/date/time/datetime-local/month/range/week: ruta "setvalue" — fija
//       el.value y dispara input{bubbles,composed} + change{bubbles}.
//  4. Verifica que el.value === value al final (casos verificables); si no, error.
//
// A diferencia del patron focus+type que concatena al valor existente, CdpFill
// reemplaza el contenido entero y es fiable con inputs controlados por frameworks.
func CdpFill(c *CDPConn, backendNodeID int, value string) error {
	if c == nil {
		return fmt.Errorf("cdp fill: conexion nula")
	}

	objID, err := resolveObjectID(c, backendNodeID)
	if err != nil {
		return fmt.Errorf("cdp fill: %w", err)
	}

	// Enfocar el nodo (idempotente; fillPrepareJS tambien enfoca, pero DOM.focus
	// hace scroll-into-view y deja el activeElement listo para Input.insertText).
	if _, err := c.sendCDP("DOM.focus", map[string]any{"backendNodeId": backendNodeID}); err != nil {
		return fmt.Errorf("cdp fill: focus ref %d: %w", backendNodeID, err)
	}

	// Validar + preparar el nodo (selecciona valor previo o fija value+eventos).
	var info fillNodeInfo
	if err := callFunctionOnJSON(c, objID, fillPrepareJS, []any{value}, &info); err != nil {
		return fmt.Errorf("cdp fill: preparar ref %d: %w", backendNodeID, err)
	}
	if info.Error != "" {
		return fmt.Errorf("cdp fill: ref %d no editable: %s", backendNodeID, info.Error)
	}

	switch info.Route {
	case "setvalue":
		// El valor ya se fijo y se dispararon los eventos en fillPrepareJS.
	case "needsinput":
		if value == "" {
			// Sin valor: borrar la seleccion (el valor previo ya esta seleccionado).
			// Delete elimina la seleccion sin insertar nada.
			del := map[string]any{"type": "keyDown", "key": "Delete", "code": "Delete", "windowsVirtualKeyCode": 46}
			if _, err := c.sendCDP("Input.dispatchKeyEvent", del); err != nil {
				return fmt.Errorf("cdp fill: borrar ref %d: %w", backendNodeID, err)
			}
			delUp := map[string]any{"type": "keyUp", "key": "Delete", "code": "Delete", "windowsVirtualKeyCode": 46}
			if _, err := c.sendCDP("Input.dispatchKeyEvent", delUp); err != nil {
				return fmt.Errorf("cdp fill: borrar ref %d: %w", backendNodeID, err)
			}
		} else {
			// Insertar el valor (reemplaza la seleccion previa) en un round-trip.
			// Input.insertText emite los eventos confiables que React/Vue necesitan.
			if _, err := c.sendCDP("Input.insertText", map[string]any{"text": value}); err != nil {
				return fmt.Errorf("cdp fill: insertText ref %d: %w", backendNodeID, err)
			}
		}
	default:
		return fmt.Errorf("cdp fill: ruta de preparacion desconocida %q para ref %d", info.Route, backendNodeID)
	}

	// Verificar que el valor cuajo (solo casos verificables: input/textarea).
	var ver struct {
		OK         bool   `json:"ok"`
		Got        string `json:"got"`
		Verifiable bool   `json:"verifiable"`
	}
	if err := callFunctionOnJSON(c, objID, fillVerifyJS, []any{value}, &ver); err != nil {
		// La verificacion en si fallo (nodo desaparecido, etc.): no enmascarar.
		return fmt.Errorf("cdp fill: verificar ref %d: %w", backendNodeID, err)
	}
	if ver.Verifiable && !ver.OK {
		return fmt.Errorf("cdp fill: verificacion fallida en ref %d: el campo quedo con %q, se esperaba %q", backendNodeID, ver.Got, value)
	}

	return nil
}

// CdpFillSelector resuelve un selector CSS a su backendDOMNodeId (via
// DOM.getDocument + DOM.querySelector + DOM.describeNode) y delega en CdpFill.
// Util cuando se tiene un selector estable en vez del #ref del AX outline.
func CdpFillSelector(c *CDPConn, selector string, value string) error {
	if c == nil {
		return fmt.Errorf("cdp fill selector: conexion nula")
	}
	if strings.TrimSpace(selector) == "" {
		return fmt.Errorf("cdp fill selector: selector vacio")
	}

	docRes, err := c.sendCDP("DOM.getDocument", map[string]any{"depth": 0})
	if err != nil {
		return fmt.Errorf("cdp fill selector: DOM.getDocument: %w", err)
	}
	root, ok := docRes["root"].(map[string]any)
	if !ok {
		return fmt.Errorf("cdp fill selector: respuesta de DOM.getDocument sin root")
	}
	rootNodeID, ok := root["nodeId"].(float64)
	if !ok {
		return fmt.Errorf("cdp fill selector: DOM.getDocument sin nodeId raiz")
	}

	qsRes, err := c.sendCDP("DOM.querySelector", map[string]any{
		"nodeId":   int(rootNodeID),
		"selector": selector,
	})
	if err != nil {
		return fmt.Errorf("cdp fill selector: DOM.querySelector %q: %w", selector, err)
	}
	nodeIDVal, ok := qsRes["nodeId"].(float64)
	if !ok || int(nodeIDVal) == 0 {
		return fmt.Errorf("cdp fill selector: el selector %q no coincide con ningun elemento", selector)
	}

	// Resolver el nodeId a backendNodeId (CdpFill opera sobre backendDOMNodeId).
	descRes, err := c.sendCDP("DOM.describeNode", map[string]any{"nodeId": int(nodeIDVal)})
	if err != nil {
		return fmt.Errorf("cdp fill selector: DOM.describeNode %q: %w", selector, err)
	}
	node, ok := descRes["node"].(map[string]any)
	if !ok {
		return fmt.Errorf("cdp fill selector: DOM.describeNode %q sin node", selector)
	}
	backendID, ok := node["backendNodeId"].(float64)
	if !ok || int(backendID) == 0 {
		return fmt.Errorf("cdp fill selector: %q sin backendNodeId", selector)
	}

	return CdpFill(c, int(backendID), value)
}
