package browser

import (
	"fmt"
	"regexp"
	"strings"
)

// CdpFindByRoleOpts configura el matching del accessible name de CdpFindByRole.
// Si Name == "", solo se filtra por role (cualquier name vale).
type CdpFindByRoleOpts struct {
	// Name es el accessible name a matchear. Vacio = no filtra por name.
	Name string
	// Exact: true = el name normalizado debe ser igual al buscado.
	// false (default) = el name normalizado contiene el buscado (substring).
	Exact bool
	// Regex: true = Name se interpreta como expresion regular (RE2 de Go).
	// Tiene prioridad sobre Exact si ambos estan a true.
	Regex bool
	// CaseSensitive: false (default) = comparacion insensible a mayusculas.
	// Para Regex, false añade el flag (?i) a la expresion.
	CaseSensitive bool
}

// normalizeWhiteSpace replica la regla de Playwright (utils/isomorphic/stringUtils.ts):
// elimina el zero-width space (U+200B) y el soft hyphen (U+00AD), recorta extremos y
// colapsa cualquier run de whitespace a un unico espacio. Es la normalizacion que
// Playwright aplica a ambos lados al comparar el accessible name (getByRole({name})),
// para que diferencias de whitespace/caracteres invisibles no rompan el match.
func normalizeWhiteSpace(s string) string {
	// Strip zero-width space y soft hyphen.
	s = strings.ReplaceAll(s, "​", "")
	s = strings.ReplaceAll(s, "­", "")
	// Colapsar runs de whitespace a un espacio.
	s = whitespaceRun.ReplaceAllString(s, " ")
	// Trim de extremos.
	return strings.TrimSpace(s)
}

// whitespaceRun matchea uno o mas caracteres de espacio en blanco. Equivale a
// `\s+` de la regex de normalizeWhiteSpace de Playwright.
var whitespaceRun = regexp.MustCompile(`\s+`)

// CdpFindByRole localiza el primer elemento por su ROLE ARIA y, opcionalmente, su
// accessible name — el equivalente a getByRole de Playwright. Reutiliza el AX tree
// que ya pedimos para page_perceive (Accessibility.getFullAXTree) en vez de tocar el
// DOM/CSS, lo que la hace robusta a cambios de markup/estilos.
//
// Recorre los nodos del AX tree y matchea:
//   - role: igualdad exacta del rol ARIA (ej "button", "link", "textbox").
//   - name (si opts.Name != ""): el accessible name del nodo contra opts.Name, con
//     normalizeWhiteSpace aplicado a ambos lados (regla Playwright). Por defecto es
//     substring; Exact => igualdad; Regex => expresion regular. Insensible a
//     mayusculas salvo CaseSensitive.
//
// Retorna (ref, count, error):
//   - ref: backendDOMNodeId del primer match — el mismo #ref que produce el outline
//     de page_perceive y que consume CdpClickRef/CdpHoverRef.
//   - count: numero total de nodos que matchean. count > 1 indica ambiguedad: el
//     caller decide si refinar (Name mas especifico, Exact, etc.).
//   - error: conexion nula, role vacio, regex invalida, fallo CDP, o 0 matches.
func CdpFindByRole(c *CDPConn, role string, opts CdpFindByRoleOpts) (ref int, count int, err error) {
	if c == nil {
		return 0, 0, fmt.Errorf("cdp find by role: conexion nula")
	}
	if role == "" {
		return 0, 0, fmt.Errorf("cdp find by role: role vacio")
	}

	// Construir el matcher del name una sola vez (compila la regex si aplica).
	matchName, err := buildNameMatcher(opts)
	if err != nil {
		return 0, 0, fmt.Errorf("cdp find by role: %w", err)
	}

	// Accessibility.enable (idempotente, cacheado) antes de getFullAXTree.
	if err := c.ensureAX(); err != nil {
		return 0, 0, fmt.Errorf("cdp find by role: Accessibility.enable: %w", err)
	}

	res, err := c.sendCDP("Accessibility.getFullAXTree", nil)
	if err != nil {
		return 0, 0, fmt.Errorf("cdp find by role: Accessibility.getFullAXTree: %w", err)
	}

	nodes := axoParseNodes(res)

	firstRef := 0
	haveFirst := false
	for _, n := range nodes {
		if n.ignored {
			continue
		}
		if n.role != role {
			continue
		}
		if opts.Name != "" && !matchName(n.name) {
			continue
		}
		count++
		if !haveFirst {
			// axoRefID prefiere backendDOMNodeID; ese es el ref que consume CdpClickRef.
			if id, ok := atoiRef(axoRefID(n)); ok {
				firstRef = id
				haveFirst = true
			}
		}
	}

	if count == 0 {
		if opts.Name != "" {
			return 0, 0, fmt.Errorf("cdp find by role: no element with role %q and name %q", role, opts.Name)
		}
		return 0, 0, fmt.Errorf("cdp find by role: no element with role %q", role)
	}
	if !haveFirst {
		// Hubo matches pero ninguno tenia un ref entero usable (backendDOMNodeId
		// ausente y nodeId no numerico): no podemos devolver un #ref valido.
		return 0, count, fmt.Errorf("cdp find by role: %d match(es) para role %q pero sin backendDOMNodeId usable", count, role)
	}
	return firstRef, count, nil
}

// buildNameMatcher devuelve la funcion que decide si un accessible name candidato
// matchea opts.Name, normalizando ambos lados con normalizeWhiteSpace. Si Name == ""
// el matcher siempre es true (no se filtra por name). Compila la regex una vez.
func buildNameMatcher(opts CdpFindByRoleOpts) (func(candidate string) bool, error) {
	if opts.Name == "" {
		return func(string) bool { return true }, nil
	}

	want := normalizeWhiteSpace(opts.Name)

	if opts.Regex {
		pat := opts.Name
		if !opts.CaseSensitive {
			pat = "(?i)" + pat
		}
		re, err := regexp.Compile(pat)
		if err != nil {
			return nil, fmt.Errorf("regex invalida %q: %w", opts.Name, err)
		}
		return func(candidate string) bool {
			return re.MatchString(normalizeWhiteSpace(candidate))
		}, nil
	}

	if !opts.CaseSensitive {
		want = strings.ToLower(want)
	}

	return func(candidate string) bool {
		got := normalizeWhiteSpace(candidate)
		if !opts.CaseSensitive {
			got = strings.ToLower(got)
		}
		if opts.Exact {
			return got == want
		}
		return strings.Contains(got, want)
	}, nil
}

// atoiRef convierte el ref string (backendDOMNodeId, ya normalizado a entero-string
// por axoStr) a int. Devuelve (0, false) si no es un entero parseable.
func atoiRef(s string) (int, bool) {
	if s == "" {
		return 0, false
	}
	neg := false
	i := 0
	if s[0] == '-' {
		neg = true
		i = 1
		if len(s) == 1 {
			return 0, false
		}
	}
	n := 0
	for ; i < len(s); i++ {
		ch := s[i]
		if ch < '0' || ch > '9' {
			return 0, false
		}
		n = n*10 + int(ch-'0')
	}
	if neg {
		n = -n
	}
	return n, true
}
