import { safeSliceHead } from "../utils/safe-slice.js";
import { normalizeSelector } from "./selectors.js";
export const DEFAULT_OUTLINE_MAX_CHARS = 6_000;
export const OUTLINE_PREVIEW_CHARS = 120;
export const OUTLINE_MAX_LANDMARKS = 12;
export const OUTLINE_MAX_HEADINGS = 24;
export const OUTLINE_MIN_GROUP_SIZE = 4;
export const OUTLINE_MAX_GROUPS = 8;
export const OUTLINE_GROUP_SAMPLE_SIZE = 3;
export const OUTLINE_MAX_CONTROLS = 24;
export const OUTLINE_SELECTOR_PATH_MAX_DEPTH = 5;
const ERROR_VALUE_MAX_CHARS = 80;
const ERROR_VALUE_SAMPLE_CHARS = ERROR_VALUE_MAX_CHARS * 4;
export async function collectPageOutline(page, opts = {}) {
    let scopeSelector;
    if (opts.selector) {
        const norm = normalizeSelector(opts.selector);
        if (!norm.ok) {
            return capOutlineText(`Error: selector ${summarizeErrorValue(norm.error ?? "invalid selector")}. Original: ${quoteErrorValue(norm.original)}.`, opts.maxChars);
        }
        scopeSelector = norm.selector;
    }
    const raw = await page.evaluate(collectOutlineInPage, {
        scopeSelector,
        previewChars: OUTLINE_PREVIEW_CHARS,
        maxLandmarks: OUTLINE_MAX_LANDMARKS,
        maxHeadings: OUTLINE_MAX_HEADINGS,
        minGroupSize: OUTLINE_MIN_GROUP_SIZE,
        maxGroups: OUTLINE_MAX_GROUPS,
        groupSampleSize: OUTLINE_GROUP_SAMPLE_SIZE,
        maxControls: OUTLINE_MAX_CONTROLS,
        selectorPathMaxDepth: OUTLINE_SELECTOR_PATH_MAX_DEPTH,
    });
    if (raw == null) {
        return capOutlineText(`Error: selector ${quoteErrorValue(scopeSelector ?? "")} not found on page.`, opts.maxChars);
    }
    return formatPageOutline(raw, {
        selector: scopeSelector,
        maxChars: opts.maxChars,
    });
}
function collectOutlineInPage(args) {
    const landmarkSelector = [
        "main",
        "nav",
        "header",
        "footer",
        "aside",
        '[role="main"]',
        '[role="navigation"]',
        '[role="banner"]',
        '[role="contentinfo"]',
        '[role="complementary"]',
        '[role="dialog"]',
    ].join(", ");
    const headingSelector = 'h1, h2, h3, [role="heading"]';
    const controlSelector = [
        "a[href]",
        "button",
        "input",
        "textarea",
        "select",
        '[role="button"]',
        '[role="link"]',
        '[role="textbox"]',
        '[role="searchbox"]',
        '[role="tab"]',
        '[role="menuitem"]',
    ].join(", ");
    let scope;
    if (args.scopeSelector) {
        try {
            scope = document.querySelector(args.scopeSelector);
        }
        catch {
            scope = null;
        }
    }
    else {
        scope = document.body;
    }
    if (!scope)
        return null;
    return {
        landmarks: collectItems(scope, landmarkSelector, args.maxLandmarks, (el) => {
            const selector = getBestSelector(el, args);
            return {
                kind: landmarkKind(el),
                label: getElementName(el) || landmarkKind(el),
                selector: selector.selector,
                preview: getPreview(el),
                stable: selector.stable,
            };
        }),
        headings: collectItems(scope, headingSelector, args.maxHeadings, (el) => {
            const label = getElementName(el);
            if (!label)
                return undefined;
            const selector = getBestSelector(el, args);
            return {
                kind: headingKind(el),
                label,
                selector: selector.selector,
                preview: getPreview(el),
                stable: selector.stable,
            };
        }),
        groups: collectGroups(scope),
        controls: collectItems(scope, controlSelector, args.maxControls, (el) => {
            const selector = getBestSelector(el, args);
            const label = getElementName(el) || controlKind(el);
            return {
                kind: controlKind(el),
                label,
                selector: selector.selector,
                preview: getControlPreview(el),
                stable: selector.stable,
            };
        }),
    };
    function collectItems(root, selector, maxItems, buildItem) {
        const items = [];
        const seen = new Set();
        for (const el of matchingElements(root, selector)) {
            if (seen.has(el) || !isVisibleElement(el))
                continue;
            seen.add(el);
            const item = buildItem(el);
            if (!item)
                continue;
            items.push(item);
            if (items.length >= maxItems)
                break;
        }
        return items;
    }
    function matchingElements(root, selector) {
        const matches = [];
        try {
            if (root.matches(selector))
                matches.push(root);
            matches.push(...Array.from(root.querySelectorAll(selector)));
        }
        catch {
            return [];
        }
        return matches;
    }
    function collectGroups(root) {
        const bySelector = new Map();
        const containers = [root, ...Array.from(root.querySelectorAll("*"))];
        for (const container of containers) {
            if (!isContainerVisible(container))
                continue;
            const buckets = new Map();
            for (const child of Array.from(container.children)) {
                if (!isVisibleElement(child))
                    continue;
                const signature = repeatedSignature(child);
                if (!signature)
                    continue;
                const bucket = buckets.get(signature.key);
                if (bucket) {
                    bucket.children.push(child);
                }
                else {
                    buckets.set(signature.key, { signature, children: [child] });
                }
            }
            for (const { signature, children } of buckets.values()) {
                if (children.length < args.minGroupSize)
                    continue;
                const existing = bySelector.get(signature.selector);
                if (existing && existing.count >= children.length)
                    continue;
                const sampleElement = children[0];
                const sampleSelector = sampleElement
                    ? getPermalinkSelector(sampleElement, signature.selector)
                    : undefined;
                const samples = children
                    .slice(0, args.groupSampleSize)
                    .map((child) => getPreview(child))
                    .filter((sample) => sample.length > 0);
                bySelector.set(signature.selector, {
                    kind: signature.kind,
                    count: children.length,
                    selector: signature.selector,
                    sampleSelector,
                    samples,
                    priority: signature.priority,
                });
            }
        }
        return Array.from(bySelector.values())
            .sort((a, b) => a.priority - b.priority || b.count - a.count)
            .slice(0, args.maxGroups)
            .map(({ priority: _priority, ...group }) => group);
    }
    function repeatedSignature(el) {
        const testId = attr(el, "data-testid");
        if (testId) {
            return {
                key: `testid:${testId}`,
                kind: `${humanize(testId)} items`,
                selector: attrSelector("data-testid", testId),
                priority: 0,
            };
        }
        const tag = tagName(el);
        if (tag.includes("-")) {
            return {
                key: `custom:${tag}`,
                kind: `${humanize(tag)} items`,
                selector: tag,
                priority: 1,
            };
        }
        const role = roleName(el);
        if (tag === "article" && role === "article") {
            return {
                key: "article:article",
                kind: "article cards",
                selector: 'article[role="article"]',
                priority: 2,
            };
        }
        return {
            key: `tag:${tag}:role:${role}`,
            kind: role ? `${role} ${tag} items` : `${tag} items`,
            selector: role ? `${tag}${attrSelector("role", role)}` : tag,
            priority: role ? 3 : 4,
        };
    }
    function isContainerVisible(el) {
        return el === document.body || isVisibleElement(el);
    }
    function isVisibleElement(el) {
        if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) {
            return false;
        }
        for (let current = el; current; current = current.parentElement) {
            if (current instanceof HTMLElement && current.hidden)
                return false;
            if (attr(current, "aria-hidden") === "true")
                return false;
            const style = getComputedStyle(current);
            if (style.display === "none" ||
                style.visibility === "hidden" ||
                style.visibility === "collapse" ||
                style.opacity === "0") {
                return false;
            }
        }
        const rects = Array.from(el.getClientRects());
        return rects.some((rect) => rect.width > 0 && rect.height > 0);
    }
    function getBestSelector(el, collectArgs, permalinkBase) {
        const testId = attr(el, "data-testid");
        if (testId) {
            const selector = attrSelector("data-testid", testId);
            if (isUniqueSelector(selector))
                return { selector, stable: true };
        }
        const id = attr(el, "id");
        if (id) {
            const selector = `#${cssEscape(id)}`;
            if (isUniqueSelector(selector))
                return { selector, stable: true };
        }
        const name = attr(el, "name");
        if (name) {
            const selector = attrSelector("name", name);
            if (isUniqueSelector(selector))
                return { selector, stable: true };
        }
        const ariaLabel = attr(el, "aria-label");
        if (ariaLabel) {
            const selector = attrSelector("aria-label", ariaLabel);
            if (isUniqueSelector(selector))
                return { selector, stable: true };
        }
        const permalinkSelector = getPermalinkSelector(el, permalinkBase);
        if (permalinkSelector) {
            return { selector: permalinkSelector, stable: true };
        }
        return { selector: cssPath(el, collectArgs.selectorPathMaxDepth), stable: false };
    }
    function getPermalinkSelector(el, preferredBase) {
        const bases = [preferredBase, simpleElementSelector(el)].filter((base) => Boolean(base));
        const suffixes = Array.from(el.querySelectorAll("a[href]"))
            .map((anchor) => permalinkSuffix(anchor))
            .filter((suffix) => Boolean(suffix))
            .sort((a, b) => permalinkPriority(a) - permalinkPriority(b));
        for (const suffix of suffixes) {
            for (const base of bases) {
                const selector = `${base}:has(${attrSelector("href", suffix, "$=", "a")})`;
                if (selectorCount(selector) === 1)
                    return selector;
            }
        }
        return undefined;
    }
    function permalinkPriority(suffix) {
        const lower = suffix.toLowerCase();
        if (lower.includes("/status/"))
            return 0;
        if (lower.includes("/watch") || lower.includes("watch?"))
            return 1;
        return 2;
    }
    function simpleElementSelector(el) {
        const testId = attr(el, "data-testid");
        if (testId)
            return attrSelector("data-testid", testId);
        const tag = tagName(el);
        const role = roleName(el);
        if (tag === "article" && role === "article")
            return 'article[role="article"]';
        if (role)
            return `${tag}${attrSelector("role", role)}`;
        return tag;
    }
    function permalinkSuffix(anchor) {
        const raw = attr(anchor, "href");
        if (!raw || raw === "#" || raw.toLowerCase().startsWith("javascript:")) {
            return undefined;
        }
        try {
            const url = new URL(anchor.href, document.baseURI);
            const suffix = `${url.pathname}${url.search}${url.hash}`;
            if (suffix && suffix !== "/" && suffix !== "#") {
                return clipText(suffix, 180);
            }
        }
        catch {
            if (raw.startsWith("/") && raw.length > 1)
                return clipText(raw, 180);
        }
        return undefined;
    }
    function cssPath(el, maxDepth) {
        const segments = [];
        let current = el;
        while (current &&
            current !== document.documentElement &&
            segments.length < maxDepth) {
            segments.unshift(cssPathSegment(current));
            current = current.parentElement;
        }
        return segments.join(" > ") || tagName(el);
    }
    function cssPathSegment(el) {
        const tag = tagName(el);
        const parent = el.parentElement;
        if (!parent)
            return tag;
        const sameTagSiblings = Array.from(parent.children).filter((child) => tagName(child) === tag);
        const index = sameTagSiblings.indexOf(el) + 1;
        return `${tag}:nth-of-type(${Math.max(index, 1)})`;
    }
    function selectorCount(selector) {
        try {
            return document.querySelectorAll(selector).length;
        }
        catch {
            return 0;
        }
    }
    function isUniqueSelector(selector) {
        return selectorCount(selector) === 1;
    }
    function landmarkKind(el) {
        const role = roleName(el);
        if (role === "main")
            return "main";
        if (role === "navigation")
            return "navigation";
        if (role === "banner")
            return "banner";
        if (role === "contentinfo")
            return "contentinfo";
        if (role === "complementary")
            return "complementary";
        if (role === "dialog")
            return "dialog";
        const tag = tagName(el);
        if (tag === "nav")
            return "navigation";
        if (tag === "header")
            return "banner";
        if (tag === "footer")
            return "contentinfo";
        if (tag === "aside")
            return "complementary";
        return tag;
    }
    function headingKind(el) {
        const tag = tagName(el);
        if (tag === "h1" || tag === "h2" || tag === "h3")
            return tag;
        const level = attr(el, "aria-level");
        if (level && /^[1-6]$/.test(level))
            return `h${level}`;
        return "heading";
    }
    function controlKind(el) {
        const role = roleName(el);
        if (role === "button" ||
            role === "link" ||
            role === "textbox" ||
            role === "searchbox" ||
            role === "tab" ||
            role === "menuitem") {
            return role;
        }
        const tag = tagName(el);
        if (tag === "a")
            return "link";
        if (tag === "button")
            return "button";
        if (tag === "textarea")
            return "textbox";
        if (tag === "select")
            return "combobox";
        if (tag === "input") {
            const type = (attr(el, "type") || "text").toLowerCase();
            if (type === "search")
                return "searchbox";
            if (type === "button" || type === "submit" || type === "reset") {
                return "button";
            }
            if (type === "checkbox" || type === "radio")
                return type;
            return "textbox";
        }
        return tag;
    }
    function getElementName(el) {
        const labelledBy = attr(el, "aria-labelledby");
        if (labelledBy) {
            const label = labelledBy
                .split(/\s+/)
                .map((id) => document.getElementById(id)?.textContent ?? "")
                .join(" ");
            const compacted = compactText(label);
            if (compacted)
                return compacted;
        }
        for (const nameAttr of ["aria-label", "title", "alt", "placeholder"]) {
            const value = attr(el, nameAttr);
            if (value)
                return compactText(value);
        }
        const labelText = associatedLabelText(el);
        if (labelText)
            return labelText;
        const name = attr(el, "name");
        if (name)
            return compactText(name);
        const text = compactText(el instanceof HTMLElement ? el.innerText : el.textContent ?? "");
        if (text)
            return text;
        if (el instanceof HTMLInputElement && el.value) {
            return compactText(el.value);
        }
        if (tagName(el) === "a") {
            const suffix = permalinkSuffix(el);
            if (suffix)
                return suffix;
        }
        return "";
    }
    function associatedLabelText(el) {
        const labeled = el;
        if (!("labels" in labeled) || !labeled.labels)
            return "";
        return compactText(Array.from(labeled.labels)
            .map((label) => label.textContent ?? "")
            .join(" "));
    }
    function getPreview(el) {
        return compactText(el instanceof HTMLElement ? el.innerText : el.textContent ?? "");
    }
    function getControlPreview(el) {
        const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
            ? el.value
            : "";
        const preview = compactText(value || getPreview(el));
        const label = getElementName(el);
        return preview && preview !== label ? preview : undefined;
    }
    function compactText(value) {
        return clipText(value.replace(/\s+/g, " ").trim(), args.previewChars);
    }
    function clipText(value, maxChars) {
        return Array.from(value).slice(0, maxChars).join("");
    }
    function humanize(value) {
        return (value
            .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
            .replace(/[_-]+/g, " ")
            .replace(/\s+/g, " ")
            .trim()
            .toLowerCase() || "repeated");
    }
    function roleName(el) {
        return (attr(el, "role") || "").toLowerCase().trim().split(/\s+/)[0] ?? "";
    }
    function tagName(el) {
        return el.tagName.toLowerCase();
    }
    function attr(el, name) {
        return el.getAttribute(name)?.trim() ?? "";
    }
    function cssEscape(value) {
        if (typeof CSS !== "undefined" && CSS.escape) {
            return CSS.escape(value);
        }
        return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`);
    }
    function attrSelector(name, value, operator = "=", element = "") {
        return `${element}[${name}${operator}${quoteAttrValue(value)}]`;
    }
    function quoteAttrValue(value) {
        if (!value.includes('"')) {
            return `"${escapeAttrValue(value, '"')}"`;
        }
        if (!value.includes("'")) {
            return `'${escapeAttrValue(value, "'")}'`;
        }
        return `"${escapeAttrValue(value, '"')}"`;
    }
    function escapeAttrValue(value, quote) {
        return value
            .replace(/\\/g, "\\\\")
            .replace(new RegExp(quote, "g"), `\\${quote}`)
            .replace(/\n/g, "\\A ");
    }
}
export function formatPageOutline(raw, opts = {}) {
    const maxChars = normalizeMaxChars(opts.maxChars);
    if (maxChars <= 0)
        return "";
    const detailSelector = pickDetailSelector(raw, opts);
    const lines = [
        `Page outline (mode=outline, maxChars=${maxChars}${formatScope(opts)})`,
        "",
        ...formatItemSection("Landmarks", raw.landmarks, OUTLINE_MAX_LANDMARKS),
        "",
        ...formatItemSection("Headings", raw.headings, OUTLINE_MAX_HEADINGS),
        "",
        ...formatGroupSection(raw.groups),
        "",
        ...formatItemSection("Controls", raw.controls, OUTLINE_MAX_CONTROLS),
        "",
        "Next:",
        `- For content, call snapshot({selector:${quote(detailSelector)}, mode:"full", maxChars:10000}) or extractVisibleText({selector:${quote(detailSelector)}}).`,
        '- For actions, use selector from this outline or call snapshot({selector:"...", mode:"interactive"}).',
    ];
    return capWithTruncationHint(lines.join("\n"), maxChars, detailSelector);
}
function normalizeMaxChars(maxChars) {
    if (maxChars === undefined || !Number.isFinite(maxChars)) {
        return DEFAULT_OUTLINE_MAX_CHARS;
    }
    return Math.max(0, Math.min(Math.floor(maxChars), DEFAULT_OUTLINE_MAX_CHARS));
}
function capOutlineText(text, maxChars) {
    const limit = normalizeMaxChars(maxChars);
    if (limit <= 0)
        return "";
    if (text.length <= limit)
        return text;
    const suffix = "\n[...TRUNCATED]";
    if (suffix.length >= limit) {
        return safeSliceHead(suffix.trimStart(), limit);
    }
    return safeSliceHead(text, limit - suffix.length) + suffix;
}
function quoteErrorValue(value) {
    return quote(summarizeErrorValue(value));
}
function summarizeErrorValue(value) {
    const sample = safeSliceHead(value, ERROR_VALUE_SAMPLE_CHARS);
    const compactedSample = sample.replace(/\s+/g, " ").trim();
    if (value.length <= sample.length && compactedSample.length <= ERROR_VALUE_MAX_CHARS) {
        return compactedSample;
    }
    return `${safeSliceHead(compactedSample, ERROR_VALUE_MAX_CHARS)}... [TRUNCATED ${value.length} chars]`;
}
function formatScope(opts) {
    return opts.selector ? `, selector=${quote(opts.selector)}` : "";
}
function formatItemSection(title, items, maxItems) {
    const boundedItems = items.slice(0, maxItems);
    const lines = [`${title}:`];
    if (boundedItems.length === 0) {
        lines.push("- none");
        return lines;
    }
    lines.push(...boundedItems.map(formatOutlineItem));
    if (items.length > maxItems) {
        lines.push(`- ... ${items.length - maxItems} more omitted`);
    }
    return lines;
}
function formatOutlineItem(item) {
    const parts = [
        `- ${item.kind} ${quote(compactText(item.label, OUTLINE_PREVIEW_CHARS))}`,
        `selector=${quote(item.selector)}`,
    ];
    if (item.preview) {
        parts.push(`preview=${quote(compactText(item.preview, OUTLINE_PREVIEW_CHARS))}`);
    }
    if (item.stable === true) {
        parts.push("stable");
    }
    return parts.join(" ");
}
function formatGroupSection(groups) {
    const boundedGroups = groups.slice(0, OUTLINE_MAX_GROUPS);
    const lines = ["Repeated groups:"];
    if (boundedGroups.length === 0) {
        lines.push("- none");
        return lines;
    }
    for (const group of boundedGroups) {
        const parts = [
            `- ${group.count} x ${group.kind}`,
            `selector=${quote(group.selector)}`,
        ];
        if (group.sampleSelector) {
            parts.push(`sampleSelector=${quote(group.sampleSelector)}`);
        }
        lines.push(parts.join(" "));
        const samples = group.samples
            .slice(0, OUTLINE_GROUP_SAMPLE_SIZE)
            .map((sample) => quote(compactText(sample, OUTLINE_PREVIEW_CHARS)));
        if (samples.length > 0) {
            lines.push(`  samples: ${samples.join(" | ")}`);
        }
    }
    if (groups.length > OUTLINE_MAX_GROUPS) {
        lines.push(`- ... ${groups.length - OUTLINE_MAX_GROUPS} more omitted`);
    }
    return lines;
}
function compactText(value, maxChars) {
    const compacted = value.replace(/\s+/g, " ").trim();
    return safeSliceHead(compacted, maxChars);
}
function quote(value) {
    return JSON.stringify(value);
}
function pickDetailSelector(raw, opts) {
    return (opts.selector ||
        raw.landmarks[0]?.selector ||
        raw.headings[0]?.selector ||
        raw.groups[0]?.sampleSelector ||
        raw.groups[0]?.selector ||
        raw.controls[0]?.selector ||
        "...");
}
function capWithTruncationHint(text, maxChars, detailSelector) {
    if (text.length <= maxChars)
        return text;
    const hint = chooseTruncationHint(text, maxChars, detailSelector);
    if (!hint) {
        return safeSliceHead(text, maxChars);
    }
    const head = safeSliceHead(text, maxChars - hint.length);
    return `${head}${hint}`;
}
function chooseTruncationHint(text, maxChars, detailSelector) {
    const bodyReserve = Math.min(text.length, "Page outline".length);
    const preferredHint = buildTruncationHint(detailSelector);
    if (preferredHint.length <= maxChars - bodyReserve) {
        return preferredHint;
    }
    const fallbackHint = buildTruncationHint("...");
    if (fallbackHint.length <= maxChars - bodyReserve) {
        return fallbackHint;
    }
    const semanticHint = buildSemanticTruncationHint();
    if (semanticHint.length <= maxChars - bodyReserve) {
        return semanticHint;
    }
    if (semanticHint.length <= maxChars) {
        return semanticHint;
    }
    const minimalHint = "\n[TRUNCATED]";
    if (minimalHint.length <= maxChars - bodyReserve) {
        return minimalHint;
    }
    return undefined;
}
function buildTruncationHint(selector) {
    return `\n[TRUNCATED snapshot({selector:${quote(selector)}, mode:"full", maxChars:10000}) or extractVisibleText({selector:${quote(selector)}})]`;
}
function buildSemanticTruncationHint() {
    return '\n[TRUNCATED use snapshot full 10000 or extractVisibleText selector "..."]';
}
//# sourceMappingURL=page-outline.js.map