"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var crawler_exports = {};
__export(crawler_exports, {
  crawl: () => crawl,
  crawlSinglePage: () => crawlSinglePage
});
module.exports = __toCommonJS(crawler_exports);
var playwright = __toESM(require("playwright-core"));
var import_blueprint = require("./blueprint");
var import_serialization = require("./serialization");
const DESTRUCTIVE_VERB_RE = /(delete|remove|destroy|submit|send|confirm|place\s*order|checkout|pay|purchase|buy|charge|cancel|close\s*account|deactivate|publish|approve|reject|logout|sign\s*out)/i;
function isSameOrigin(a, b) {
  try {
    return new URL(a).origin === new URL(b).origin;
  } catch {
    return false;
  }
}
function isBogusHref(href) {
  if (!href)
    return true;
  const trimmed = href.trim();
  if (!trimmed)
    return true;
  const stripped = trimmed.replace(/^["']|["']$/g, "");
  if (!stripped || stripped === "#")
    return true;
  if (stripped.startsWith("#"))
    return true;
  if (/^javascript:/i.test(stripped))
    return true;
  if (/^mailto:/i.test(stripped))
    return true;
  if (/^tel:/i.test(stripped))
    return true;
  return false;
}
function resolveUrl(href, base) {
  try {
    return new URL(href, base).href;
  } catch {
    return null;
  }
}
function substituteUrlSegments(url, params) {
  try {
    const parsed = new URL(url);
    const segments = parsed.pathname.split("/");
    for (let i = 0; i < segments.length; i++) {
      for (const [paramName, paramValue] of Object.entries(params)) {
        if (paramValue && segments[i] === paramValue) {
          segments[i] = `{${paramName}}`;
          break;
        }
      }
    }
    parsed.pathname = segments.join("/");
    const newParams = new URLSearchParams();
    for (const [key, value] of parsed.searchParams.entries()) {
      let replaced = value;
      for (const [paramName, paramValue] of Object.entries(params)) {
        if (paramValue && replaced === paramValue) {
          replaced = `{${paramName}}`;
          break;
        }
      }
      newParams.append(key, replaced);
    }
    parsed.search = newParams.toString();
    return parsed.href.replace(/%7B/gi, "{").replace(/%7D/gi, "}");
  } catch {
    return url;
  }
}
async function probeClickNavigations(page, fromUrl, blueprint, probeMode, sameOriginOnly, entryUrl, bogusHrefLinkLabels) {
  if (probeMode === "none")
    return [];
  const results = [];
  const seenXpaths = /* @__PURE__ */ new Set();
  const candidates = [];
  for (const section of blueprint.sections) {
    for (const el of section.elements) {
      if (el.role === "button") {
        candidates.push({
          logicalName: el.logicalName,
          label: el.accessibleName,
          role: "button",
          xpath: el.xpath,
          mutability: el.mutability
        });
      } else if (el.role === "link" && bogusHrefLinkLabels.has(el.accessibleName)) {
        candidates.push({
          logicalName: el.logicalName,
          label: el.accessibleName,
          role: "link",
          xpath: el.xpath,
          mutability: el.mutability
        });
      }
    }
    for (const rep of section.repeatingElements) {
      if (rep.role !== "button" && rep.role !== "link")
        continue;
      if (rep.items.length === 0)
        continue;
      const probedItem = rep.items[0].parameters;
      const label = rep.accessibleNameTemplate.replace(
        /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
        (_, k) => probedItem[k] ?? `{${k}}`
      );
      if (rep.role === "link" && !bogusHrefLinkLabels.has(label))
        continue;
      candidates.push({
        logicalName: rep.logicalName,
        label,
        role: rep.role,
        xpath: (0, import_blueprint.resolveXpath)(rep, probedItem),
        mutability: rep.mutability,
        repeatingSource: {
          parameters: [...rep.parameters],
          probedItemParameters: { ...probedItem }
        }
      });
    }
  }
  for (const candidate of candidates) {
    if (seenXpaths.has(candidate.xpath))
      continue;
    seenXpaths.add(candidate.xpath);
    if (probeMode === "immutable-only") {
      if (candidate.mutability !== "immutable")
        continue;
      if (DESTRUCTIVE_VERB_RE.test(candidate.label))
        continue;
    }
    try {
      await page.goto(fromUrl, { waitUntil: "networkidle" });
      const beforeUrl = page.url();
      await page.locator(`xpath=${candidate.xpath}`).click();
      try {
        await page.waitForURL((url) => url.toString() !== beforeUrl, { timeout: 2e3 });
      } catch {
        continue;
      }
      const afterUrl = page.url();
      if (afterUrl === beforeUrl)
        continue;
      if (sameOriginOnly && !isSameOrigin(afterUrl, entryUrl))
        continue;
      const mutability = candidate.mutability === "mutable" ? "mutable" : "immutable";
      results.push({
        logicalName: candidate.logicalName,
        label: candidate.label,
        url: (0, import_serialization.normalizeUrl)(afterUrl),
        mutability,
        probedRole: candidate.role,
        repeatingSource: candidate.repeatingSource
      });
    } catch {
    }
  }
  return results;
}
async function crawl(entryUrl, options) {
  const { depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath } = options;
  const normalizedEntryUrl = (0, import_serialization.normalizeUrl)(entryUrl);
  const visited = /* @__PURE__ */ new Set();
  const pages = {};
  const edges = [];
  const browser = await playwright.chromium.launch();
  try {
    const context = await browser.newContext(playwrightStoragePath ? { storageState: playwrightStoragePath } : void 0);
    const page = await context.newPage();
    const queue = [{ url: normalizedEntryUrl, fromUrl: null, edgeVia: null, depth: 0 }];
    while (queue.length > 0 && Object.keys(pages).length < maxPages) {
      const entry = queue.shift();
      const { url, fromUrl, edgeVia, depth: currentDepth } = entry;
      if (visited.has(url))
        continue;
      visited.add(url);
      await page.goto(url, { waitUntil: "networkidle" });
      const resolvedUrl = (0, import_serialization.normalizeUrl)(page.url());
      if (resolvedUrl !== url) {
        if (visited.has(resolvedUrl))
          continue;
        visited.add(resolvedUrl);
      }
      let blueprint;
      try {
        blueprint = await (0, import_blueprint.buildPageBlueprint)(page);
      } catch (err) {
        if (err instanceof import_blueprint.BlueprintInvariantError) {
          console.warn(`[dom-analyzer] Skipping blueprint for ${page.url()} \u2014 invariant violated: ${err.message}`);
          continue;
        }
        throw err;
      }
      pages[resolvedUrl] = blueprint;
      if (fromUrl && edgeVia) {
        const via = {
          logicalName: edgeVia.logicalName,
          mutability: edgeVia.mutability
        };
        let toTemplate;
        if (edgeVia.repeatingSource) {
          const tmpl = substituteUrlSegments(
            resolvedUrl,
            edgeVia.repeatingSource.probedItemParameters
          );
          if (tmpl !== resolvedUrl) {
            toTemplate = tmpl;
            const usedParams = edgeVia.repeatingSource.parameters.filter(
              (p) => tmpl.includes(`{${p}}`)
            );
            if (usedParams.length > 0)
              via.parameters = usedParams;
          }
        }
        edges.push({
          from: (0, import_serialization.normalizeUrl)(fromUrl),
          to: resolvedUrl,
          ...toTemplate ? { toTemplate } : {},
          via,
          label: edgeVia.label,
          action: edgeVia.action
        });
      }
      if (currentDepth < depth) {
        const aria = await page.locator("body").ariaSnapshot();
        const bogusHrefLinkLabels = /* @__PURE__ */ new Set();
        for (const { label, href } of extractLinksFromAriaSnapshot(aria)) {
          if (isBogusHref(href)) {
            bogusHrefLinkLabels.add(label);
            continue;
          }
          const resolved = resolveUrl(href, resolvedUrl);
          if (!resolved)
            continue;
          const normalized = (0, import_serialization.normalizeUrl)(resolved);
          if (sameOriginOnly && !isSameOrigin(normalized, normalizedEntryUrl))
            continue;
          if (visited.has(normalized))
            continue;
          const linkMatch = findLinkByLabel(blueprint, label);
          const logicalName = linkMatch?.logicalName ?? "unknown_link";
          queue.push({
            url: normalized,
            fromUrl: resolvedUrl,
            edgeVia: { logicalName, mutability: "immutable", action: "link", label },
            depth: currentDepth + 1
          });
        }
        const clickNavs = await probeClickNavigations(
          page,
          resolvedUrl,
          blueprint,
          probeButtons,
          sameOriginOnly,
          normalizedEntryUrl,
          bogusHrefLinkLabels
        );
        for (const nav of clickNavs) {
          if (visited.has(nav.url))
            continue;
          queue.push({
            url: nav.url,
            fromUrl: resolvedUrl,
            edgeVia: {
              logicalName: nav.logicalName,
              mutability: nav.mutability,
              // A probed link that took us to a new URL is recorded as a
              // 'link' action (matches the nav intent). Probed buttons stay
              // as 'click'.
              action: nav.probedRole === "link" ? "link" : "click",
              label: nav.label,
              repeatingSource: nav.repeatingSource
            },
            depth: currentDepth + 1
          });
        }
      }
    }
    await context.close();
  } finally {
    await browser.close();
  }
  return {
    schemaVersion: import_serialization.SCHEMA_VERSION,
    entryUrl: normalizedEntryUrl,
    cachedAt: (0, import_serialization.nowTimestamp)(),
    pages,
    edges
  };
}
async function crawlSinglePage(entryUrl, options) {
  const browser = await playwright.chromium.launch();
  try {
    const context = await browser.newContext(options.playwrightStoragePath ? { storageState: options.playwrightStoragePath } : void 0);
    const page = await context.newPage();
    await page.goto(entryUrl, { waitUntil: "networkidle" });
    let blueprint;
    try {
      blueprint = await (0, import_blueprint.buildPageBlueprint)(page);
    } catch (err) {
      if (err instanceof import_blueprint.BlueprintInvariantError) {
        console.warn(`[dom-analyzer] Blueprint skipped \u2014 invariant violated: ${err.message}`);
        await context.close();
        throw err;
      }
      throw err;
    }
    const normalizedEntryUrl = (0, import_serialization.normalizeUrl)(page.url());
    await context.close();
    return {
      schemaVersion: import_serialization.SCHEMA_VERSION,
      entryUrl: normalizedEntryUrl,
      cachedAt: (0, import_serialization.nowTimestamp)(),
      pages: { [normalizedEntryUrl]: blueprint },
      edges: []
    };
  } finally {
    await browser.close();
  }
}
function extractLinksFromAriaSnapshot(aria) {
  const out = [];
  const lines = aria.split("\n");
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const m = line.match(/-\s+link\s+"([^"]+)"/);
    if (!m)
      continue;
    const label = m[1];
    for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
      const urlMatch = lines[j].match(/-\s+\/url:\s+(\S+)/);
      if (urlMatch) {
        out.push({ label, href: urlMatch[1] });
        break;
      }
    }
  }
  return out;
}
function findLinkByLabel(blueprint, label) {
  for (const section of blueprint.sections) {
    for (const el of section.elements) {
      if (el.role === "link" && el.accessibleName === label)
        return { logicalName: el.logicalName };
    }
  }
  return null;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  crawl,
  crawlSinglePage
});
