/**
 * Selector normalization & preflight validation.
 *
 * Production logs showed three recurring selector failures:
 *   1. Over-escaped quotes from JSON tool calls — e.g. a[href=\\\"/User\\\"].
 *   2. Empty / whitespace-only strings.
 *   3. Unmatched brackets/quotes that crash the CSS parser.
 *
 * This module catches those before they reach Playwright, so the LLM gets
 * an actionable repair candidate instead of a generic locator timeout.
 *
 * It is purposely conservative: only obviously-broken selectors are repaired
 * or rejected. A selector that looks weird but parses cleanly is left alone —
 * the truth is in `document.querySelector`.
 */
import { safeSliceHead } from "../utils/safe-slice.js";
/**
 * The set of selectors a destructive action (click on submit/post/delete-like
 * controls, type into a non-editable wrapper, etc.) should refuse to use
 * without an explicit user-provided ref. They match too many elements on a
 * typical page and are almost always a sign of a bad LLM guess.
 */
const DANGEROUSLY_BROAD = new Set([
    "*",
    "body",
    "html",
    "div",
    "span",
    "a",
    "button",
    "input",
]);
const SELECTOR_ERROR_VALUE_MAX_CHARS = 160;
function summarizeSelectorForError(selector) {
    if (selector.length <= SELECTOR_ERROR_VALUE_MAX_CHARS)
        return selector;
    return `${safeSliceHead(selector, SELECTOR_ERROR_VALUE_MAX_CHARS)}... [TRUNCATED ${selector.length} chars]`;
}
export function normalizeSelector(raw) {
    if (typeof raw !== "string") {
        return {
            ok: false,
            selector: "",
            original: String(raw ?? ""),
            error: "selector must be a string",
        };
    }
    const original = raw;
    let s = raw.trim();
    if (s === "") {
        return { ok: false, selector: "", original, error: "selector is empty" };
    }
    let note;
    // JSON tool calls sometimes double-escape quotes: \\\" → ".
    // We unescape conservatively: only when the selector contains \" but no
    // already-correct ", since `[name="foo"]` is the canonical form and
    // `[name=\"foo\"]` is almost certainly an artifact of bad JSON encoding.
    if (s.includes('\\"')) {
        const unescaped = s.replace(/\\"/g, '"');
        note = `unescaped \\\" → \"`;
        s = unescaped;
    }
    // Same pattern, single-quote variant.
    if (s.includes("\\'")) {
        s = s.replace(/\\'/g, "'");
        note = note ? `${note}; unescaped \\' → '` : `unescaped \\' → '`;
    }
    // Reject unmatched quotes inside the string (excluding escaped pairs which
    // we just removed). Count plain " and ' — if either is odd, the parser
    // will throw.
    const dq = (s.match(/"/g) ?? []).length;
    const sq = (s.match(/'/g) ?? []).length;
    if (dq % 2 !== 0) {
        return {
            ok: false,
            selector: "",
            original,
            error: `unmatched double quote in selector: ${summarizeSelectorForError(s)}`,
            note,
        };
    }
    if (sq % 2 !== 0) {
        return {
            ok: false,
            selector: "",
            original,
            error: `unmatched single quote in selector: ${summarizeSelectorForError(s)}`,
            note,
        };
    }
    // Reject unmatched brackets. We do a depth-aware scan that ignores
    // characters inside string literals so `[name="]"]` doesn't false-flag.
    const depth = scanBracketDepth(s);
    if (depth !== 0) {
        return {
            ok: false,
            selector: "",
            original,
            error: `unmatched brackets in selector: ${summarizeSelectorForError(s)}`,
            note,
        };
    }
    return { ok: true, selector: s, original, note };
}
/**
 * Returns true if the selector is too broad to be safely auto-resolved for a
 * destructive action. The caller still gets to override (e.g. when the user
 * explicitly passes such a selector), but stale-ref auto-recovery and
 * fuzzy-match paths should refuse to use these.
 */
export function isSelectorBroad(selector) {
    const trimmed = selector.trim();
    if (DANGEROUSLY_BROAD.has(trimmed.toLowerCase()))
        return true;
    // Bare tag name with no attribute/class/id qualifier.
    if (/^[a-zA-Z]+$/.test(trimmed))
        return true;
    return false;
}
/**
 * Build a Playwright-safe attribute selector. Used when emitting stable
 * selector hints from snapshot enrichment so we never produce malformed
 * attribute literals from values that contain quotes.
 */
export function attrSelector(attr, value) {
    // CSS attribute selectors only need to escape the chosen quote and
    // backslash. We pick the quote variant that minimizes escaping.
    const hasDouble = value.includes('"');
    const hasSingle = value.includes("'");
    if (!hasDouble) {
        return `[${attr}="${value.replace(/\\/g, "\\\\")}"]`;
    }
    if (!hasSingle) {
        return `[${attr}='${value.replace(/\\/g, "\\\\")}']`;
    }
    // Both quote types present — escape the double-quote variant.
    const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
    return `[${attr}="${escaped}"]`;
}
function scanBracketDepth(s) {
    let depth = 0;
    let parenDepth = 0;
    let i = 0;
    while (i < s.length) {
        const c = s[i];
        // Skip over string literals.
        if (c === '"' || c === "'") {
            const quote = c;
            i++;
            while (i < s.length && s[i] !== quote) {
                if (s[i] === "\\")
                    i++; // skip the escape
                i++;
            }
            i++; // skip closing quote
            continue;
        }
        if (c === "[")
            depth++;
        else if (c === "]")
            depth--;
        else if (c === "(")
            parenDepth++;
        else if (c === ")")
            parenDepth--;
        if (depth < 0 || parenDepth < 0)
            return depth + parenDepth;
        i++;
    }
    return depth + parenDepth;
}
//# sourceMappingURL=selectors.js.map