import { spawn } from "node:child_process";
import { mkdir, rm, writeFile } from "node:fs/promises";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";

const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const port = Number(process.env.CDP_PORT || 9229);
const origin = process.env.JSX_ORIGIN || "DAL";
const destination = process.env.JSX_DESTINATION || "HOU";
const beginDate = process.env.JSX_DATE || "2026-04-11";
const headless = process.env.HEADLESS === "1";
const userDataDir = process.env.CHROME_USER_DATA_DIR || `/tmp/jsx-chrome-${Date.now()}`;
const artifactsDir = process.env.JSX_ARTIFACTS_DIR || "/tmp/jsx-probe";
const navigationUrl = process.env.JSX_URL || "https://www.jsx.com/";
const settleMs = Number(process.env.SETTLE_MS || 8000);
const requestTimeoutMs = Number(process.env.REQUEST_TIMEOUT_MS || 30000);

await mkdir(artifactsDir, { recursive: true });

function log(message, details) {
  if (details === undefined) {
    console.log(`[probe] ${message}`);
    return;
  }
  console.log(`[probe] ${message}`, details);
}

async function fetchJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} for ${url}`);
  }
  return response.json();
}

async function waitForDebugger() {
  const deadline = Date.now() + requestTimeoutMs;
  while (Date.now() < deadline) {
    try {
      return await fetchJson(`http://127.0.0.1:${port}/json/version`);
    } catch {
      await delay(250);
    }
  }
  throw new Error("Timed out waiting for Chrome remote debugger");
}

function launchChrome() {
  const args = [
    `--remote-debugging-port=${port}`,
    `--user-data-dir=${userDataDir}`,
    "--no-first-run",
    "--no-default-browser-check",
    "--disable-default-apps",
    "--disable-popup-blocking",
    "--disable-background-networking",
    "--disable-renderer-backgrounding",
    "--window-size=1440,1200",
    navigationUrl,
  ];

  if (headless) {
    args.splice(args.length - 1, 0, "--headless=new");
  }

  log("Launching Chrome", { chromePath, port, userDataDir, headless, navigationUrl });
  const child = spawn(chromePath, args, {
    detached: false,
    stdio: ["ignore", "pipe", "pipe"],
  });

  child.stdout.on("data", chunk => {
    const text = chunk.toString().trim();
    if (text) {
      console.log(`[chrome] ${text}`);
    }
  });

  child.stderr.on("data", chunk => {
    const text = chunk.toString().trim();
    if (text) {
      console.error(`[chrome] ${text}`);
    }
  });

  return child;
}

class CDPClient {
  constructor(socketUrl) {
    this.socketUrl = socketUrl;
    this.ws = new WebSocket(socketUrl);
    this.nextId = 1;
    this.pending = new Map();
    this.eventHandlers = new Map();
    this.openPromise = new Promise((resolve, reject) => {
      this.ws.addEventListener("open", () => resolve());
      this.ws.addEventListener("error", event => reject(event.error || new Error("WebSocket open failed")));
    });

    this.ws.addEventListener("message", event => {
      const message = JSON.parse(event.data.toString());
      if (message.id) {
        const pending = this.pending.get(message.id);
        if (!pending) {
          return;
        }
        this.pending.delete(message.id);
        if (message.error) {
          pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
        } else {
          pending.resolve(message.result);
        }
        return;
      }

      const handlers = this.eventHandlers.get(message.method) || [];
      for (const handler of handlers) {
        handler(message.params || {});
      }
    });
  }

  async ready() {
    await this.openPromise;
  }

  async send(method, params = {}) {
    await this.ready();
    const id = this.nextId++;
    const payload = JSON.stringify({ id, method, params });
    const promise = new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      setTimeout(() => {
        if (this.pending.has(id)) {
          this.pending.delete(id);
          reject(new Error(`Timed out waiting for ${method}`));
        }
      }, requestTimeoutMs);
    });

    this.ws.send(payload);
    return promise;
  }

  on(method, handler) {
    const handlers = this.eventHandlers.get(method) || [];
    handlers.push(handler);
    this.eventHandlers.set(method, handlers);
  }

  async close() {
    if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
      this.ws.close();
    }
  }
}

async function clickPoint(cdp, point) {
  await cdp.send("Input.dispatchMouseEvent", {
    type: "mouseMoved",
    x: point.x,
    y: point.y,
    button: "none",
  });
  await cdp.send("Input.dispatchMouseEvent", {
    type: "mousePressed",
    x: point.x,
    y: point.y,
    button: "left",
    clickCount: 1,
  });
  await cdp.send("Input.dispatchMouseEvent", {
    type: "mouseReleased",
    x: point.x,
    y: point.y,
    button: "left",
    clickCount: 1,
  });
}

async function clickButtonByText(cdp, textPattern) {
  const result = await cdp.send("Runtime.evaluate", {
    expression: `
(() => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const textOf = element => (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
  const button = Array.from(document.querySelectorAll("button, [role='button']"))
    .filter(visible)
    .find(element => ${textPattern}.test(textOf(element)));

  if (!button) {
    return { success: false, reason: "button-not-found", pattern: "${textPattern}" };
  }

  const rect = button.getBoundingClientRect();
  return {
    success: true,
    text: textOf(button),
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2
  };
})()
`,
    awaitPromise: true,
    returnByValue: true,
  });

  const details = result.result?.value;
  if (!details?.success) {
    return details;
  }

  await clickPoint(cdp, details);
  await delay(500);
  return { success: true, text: details.text };
}

async function newTarget() {
  const response = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, {
    method: "PUT",
  });
  if (!response.ok) {
    throw new Error(`Failed to create target: HTTP ${response.status}`);
  }
  return response.json();
}

function visibleFilter(element) {
  return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
}

const pageProbeScript = `
(() => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const textOf = element => (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
  const describe = element => {
    const label = element.labels && element.labels.length
      ? Array.from(element.labels).map(label => textOf(label)).join(" | ")
      : "";
    return {
      tag: element.tagName.toLowerCase(),
      type: element.getAttribute("type") || "",
      id: element.id || "",
      name: element.getAttribute("name") || "",
      role: element.getAttribute("role") || "",
      testId: element.getAttribute("data-testid") || "",
      placeholder: element.getAttribute("placeholder") || "",
      ariaLabel: element.getAttribute("aria-label") || "",
      value: element.value || "",
      text: textOf(element).slice(0, 120),
      label: label.slice(0, 120),
    };
  };

  const elements = Array.from(document.querySelectorAll("input, button, select, textarea, [role='button'], [role='combobox'], [data-testid]"))
    .filter(visible)
    .map(describe)
    .slice(0, 250);

  return {
    url: location.href,
    title: document.title,
    readyState: document.readyState,
    bodyTextPreview: textOf(document.body).slice(0, 1000),
    localStorage: Object.fromEntries(Object.keys(localStorage).sort().map(key => [key, localStorage.getItem(key)])),
    sessionStorage: Object.fromEntries(Object.keys(sessionStorage).sort().map(key => [key, sessionStorage.getItem(key)])),
    elements,
  };
})()
`;

const acceptConsentScript = `
(() => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const exactLabels = new Set([
    "accept",
    "accept all",
    "allow all",
    "i agree",
    "agree"
  ]);

  const candidates = Array.from(document.querySelectorAll("button, [role='button']"))
    .filter(visible);

  for (const candidate of candidates) {
    const text = (candidate.innerText || candidate.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
    if (exactLabels.has(text)) {
      candidate.click();
      return { clicked: true, text };
    }
  }

  return { clicked: false };
})()
`;

const setOneWayTripScript = `
(async () => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const textOf = element => (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
  const tripTrigger = Array.from(document.querySelectorAll("[role='combobox']"))
    .find(element => /round trip|one way|multi city/i.test(textOf(element)));

  if (!tripTrigger) {
    return { success: false, reason: "trip-trigger-not-found" };
  }

  const anyOption = () => Array.from(document.querySelectorAll("mat-option, [role='option']")).find(visible);
  const tried = [];

  tripTrigger.scrollIntoView?.({ block: "center" });
  tripTrigger.click();
  tried.push("click");
  await new Promise(resolve => setTimeout(resolve, 500));

  if (!anyOption()) {
    tripTrigger.focus?.();
    await new Promise(resolve => setTimeout(resolve, 100));
    for (const key of ["Enter", " ", "ArrowDown"]) {
      tripTrigger.dispatchEvent(new KeyboardEvent("keydown", {
        key,
        code: key === " " ? "Space" : key,
        keyCode: key === "Enter" ? 13 : key === " " ? 32 : 40,
        bubbles: true,
        cancelable: true,
      }));
      tried.push("key:" + key);
      await new Promise(resolve => setTimeout(resolve, 350));
      if (anyOption()) break;
    }
  }

  if (!anyOption()) {
    const ctxKey = Object.keys(tripTrigger).find(key => key.startsWith("__ngContext__"));
    if (ctxKey) {
      for (const item of tripTrigger[ctxKey] || []) {
        if (item && typeof item === "object" && typeof item.open === "function") {
          try { item.open(); tried.push("ngContext.open"); } catch {}
          await new Promise(resolve => setTimeout(resolve, 400));
          if (anyOption()) break;
        }
      }
    }
  }

  const options = Array.from(document.querySelectorAll("mat-option, [role='option']")).filter(visible);
  const oneWay = options.find(element => /^one way$/i.test(textOf(element)));
  if (!oneWay) {
    return {
      success: false,
      reason: "one-way-option-not-found",
      tried,
      visibleOptions: options.map(textOf).filter(Boolean).slice(0, 40)
    };
  }

  oneWay.scrollIntoView?.({ block: "center" });
  for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
    oneWay.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
  }
  oneWay.click?.();
  await new Promise(resolve => setTimeout(resolve, 500));

  let usedAngular = false;
  const returnVisible = Array.from(document.querySelectorAll("input")).some(input => {
    const label = (input.getAttribute("aria-label") || "").toLowerCase();
    return label.includes("return date") && (input.offsetWidth || input.offsetHeight);
  });
  if (returnVisible) {
    const ctxKey = Object.keys(oneWay).find(key => key.startsWith("__ngContext__"));
    if (ctxKey) {
      for (const item of oneWay[ctxKey] || []) {
        if (!item || typeof item !== "object") continue;
        if (typeof item._selectViaInteraction === "function") {
          try { item._selectViaInteraction(); usedAngular = true; break; } catch {}
        }
        if (typeof item.select === "function" && item.value !== undefined) {
          try { item.select(); usedAngular = true; break; } catch {}
        }
      }
    }
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  const finalReturnVisible = Array.from(document.querySelectorAll("input")).some(input => {
    const label = (input.getAttribute("aria-label") || "").toLowerCase();
    return label.includes("return date") && (input.offsetWidth || input.offsetHeight);
  });

  return {
    success: !finalReturnVisible,
    selected: textOf(oneWay),
    tried,
    usedAngular,
    finalReturnVisible
  };
})()
`;

const dismissMarketingPopupScript = `
(() => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const closeButton = Array.from(document.querySelectorAll("button, [role='button']"))
    .filter(visible)
    .find(element => (element.getAttribute("aria-label") || "").toLowerCase() === "close dialog");

  if (!closeButton) {
    return { dismissed: false };
  }

  closeButton.click();
  return { dismissed: true };
})()
`;

const selectStationScript = (index, stationCode) => `
(async () => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const textOf = element => (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
  const metaOf = element => [
    element.getAttribute("placeholder") || "",
    element.getAttribute("aria-label") || "",
    element.getAttribute("name") || "",
    element.id || "",
    textOf(element.parentElement || element),
  ].join(" ").toLowerCase();
  const nativeValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
  const setInputValue = (input, value) => {
    input.focus();
    if (nativeValueSetter) {
      nativeValueSetter.call(input, value);
    } else {
      input.value = value;
    }
    input.dispatchEvent(new InputEvent("input", { bubbles: true, data: value }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
  };

  const stationButtons = Array.from(document.querySelectorAll("[aria-label='Station select'], [aria-label*='Station select']"))
    .filter(visible);
  const trigger = stationButtons[${index}];
  if (!trigger) {
    return {
      success: false,
      reason: "station-trigger-not-found",
      triggers: stationButtons.map(textOf)
    };
  }

  trigger.click();
  await new Promise(resolve => setTimeout(resolve, 700));

  const searchInput = Array.from(document.querySelectorAll("input"))
    .filter(visible)
    .find(input => {
      const placeholder = (input.getAttribute("placeholder") || "").toLowerCase();
      return placeholder === "airport or city" || (/airport|station|city|search|origin|destination/.test(metaOf(input)) && !/date|email|phone/.test(metaOf(input)));
    });

  if (searchInput) {
    setInputValue(searchInput, "${stationCode}");
    await new Promise(resolve => setTimeout(resolve, 700));
  }

  const stationPattern = new RegExp("(^|\\\\b)${stationCode}(\\\\b|$)", "i");
  const candidates = Array.from(document.querySelectorAll("li[role='option'].station-options__item, li[role='option'], .station-options__item"))
    .filter(visible);
  const target = candidates.find(element => stationPattern.test(textOf(element)));

  if (!target) {
    return {
      success: false,
      reason: "station-option-not-found",
      searchInputMeta: searchInput ? metaOf(searchInput) : null,
      visibleOptions: candidates.map(textOf).filter(Boolean).slice(0, 80)
    };
  }

  target.scrollIntoView?.({ block: "center" });
  for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
    target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
  }
  target.click?.();
  await new Promise(resolve => setTimeout(resolve, 500));

  const after = () => Array.from(document.querySelectorAll("[aria-label='Station select']")).filter(visible)[${index}];
  let finalText = (after()?.textContent || "").trim();
  let usedAngular = false;
  if (!finalText.includes("${stationCode}")) {
    const ctxKey = Object.keys(target).find(key => key.startsWith("__ngContext__"));
    if (ctxKey) {
      for (const item of target[ctxKey] || []) {
        if (!item || typeof item !== "object") continue;
        for (const method of ["_selectViaInteraction", "select", "onSelect", "onClick", "handleClick"]) {
          if (typeof item[method] === "function") {
            try { item[method](); usedAngular = true; break; } catch {}
          }
        }
        if (usedAngular) break;
      }
    }
    await new Promise(resolve => setTimeout(resolve, 400));
    finalText = (after()?.textContent || "").trim();
  }

  return {
    success: finalText.includes("${stationCode}"),
    selected: textOf(target),
    finalText,
    usedAngular
  };
})()
`;

const setDepartDateScript = isoDate => `
(async () => {
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  const [year, month, day] = "${isoDate}".split("-").map(Number);
  const monthNames = [
    "January","February","March","April","May","June",
    "July","August","September","October","November","December"
  ];
  const targetMonthName = monthNames[month - 1];

  const input = Array.from(document.querySelectorAll("input")).find(element => {
    const label = (element.getAttribute("aria-label") || "").toLowerCase();
    return label.includes("depart date") || (label.includes("date") && !label.includes("return"));
  });
  if (!input) {
    return { success: false, reason: "depart-input-not-found" };
  }

  const wrapper = input.closest("mat-form-field, jsx-form-field, .form-field, .jsx-form-field");
  const toggle =
    wrapper?.querySelector("mat-datepicker-toggle button") ||
    wrapper?.querySelector("button[aria-label*='calendar' i]") ||
    wrapper?.querySelector("button[aria-label*='date picker' i]") ||
    input;
  toggle.click();
  await sleep(800);

  const findCalendar = () => {
    const direct = document.querySelector(
      "mat-calendar, .mat-calendar, mat-datepicker-content, [class*='mat-datepicker']"
    );
    if (direct) return direct;
    const overlay = document.querySelector(".cdk-overlay-container");
    if (overlay) {
      const panes = overlay.querySelectorAll(".cdk-overlay-pane, [class*='datepicker'], [class*='calendar'], [class*='date-picker']");
      for (const pane of panes) {
        if (!(pane.offsetWidth || pane.offsetHeight)) continue;
        const text = pane.textContent || "";
        if (/january|february|march|april|may|june|july|august|september|october|november|december/i.test(text)) {
          return pane;
        }
      }
    }
    const candidates = Array.from(document.querySelectorAll("div, section, article"))
      .filter(element => element.offsetWidth && element.offsetHeight && element.offsetWidth < 1000)
      .filter(element => /\\b(Sun|Mon|Tue|Wed|Thu|Fri|Sat)\\b/.test(element.textContent || ""))
      .filter(element => /january|february|march|april|may|june|july|august|september|october|november|december/i.test(element.textContent || ""));
    return candidates[0] || null;
  };

  let calendar = findCalendar();
  if (!calendar) {
    input.click();
    input.focus?.();
    await sleep(500);
    calendar = findCalendar();
  }
  if (!calendar) {
    return { success: false, reason: "datepicker-did-not-open" };
  }

  const next = calendar.querySelector(".mat-calendar-next-button, [aria-label*='Next month' i]");
  const prev = calendar.querySelector(".mat-calendar-previous-button, [aria-label*='Previous month' i]");
  const monthIndex = name =>
    ["january","february","march","april","may","june","july","august","september","october","november","december"]
      .indexOf(name.toLowerCase());

  for (let i = 0; i < 24; i++) {
    const header = calendar.querySelector(".mat-calendar-period-button, [class*='calendar-header']");
    const text = header?.textContent?.trim() || "";
    if (text.includes(targetMonthName) && text.includes(String(year))) break;
    const match = text.match(/(\\w+)\\s+(\\d{4})/);
    let forward = true;
    if (match) {
      const current = parseInt(match[2], 10) * 12 + monthIndex(match[1]);
      const target = year * 12 + (month - 1);
      forward = current < target;
    }
    const button = forward ? next : prev;
    if (!button || button.disabled) break;
    button.click();
    await sleep(250);
  }

  const ariaExact = targetMonthName + " " + day + ", " + year;
  const ariaLoose = targetMonthName + " " + day + " " + year;
  const allCells = Array.from(calendar.querySelectorAll("[aria-label]"))
    .filter(element => element.offsetWidth || element.offsetHeight);
  let cell = allCells.find(element => {
    const aria = (element.getAttribute("aria-label") || "").trim();
    return aria === ariaExact || aria === ariaLoose || aria.startsWith(ariaExact);
  });

  if (!cell) {
    const dayRe = new RegExp("\\\\b" + day + "\\\\b");
    cell = allCells.find(element => {
      const aria = element.getAttribute("aria-label") || "";
      return aria.includes(targetMonthName) && aria.includes(String(year)) && dayRe.test(aria);
    });
  }

  if (!cell) {
    return { success: false, reason: "day-cell-not-found", ariaExact };
  }

  cell.scrollIntoView?.({ block: "center" });
  for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
    cell.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
  }
  cell.click?.();
  await sleep(700);

  const doneButton = Array.from(document.querySelectorAll("button, [role='button']"))
    .filter(element => element.offsetWidth || element.offsetHeight)
    .find(element => /^\\s*done\\s*$/i.test((element.innerText || element.textContent || "").trim()));
  let doneClicked = false;
  if (doneButton) {
    doneButton.scrollIntoView?.({ block: "center" });
    for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
      doneButton.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
    }
    doneButton.click?.();
    doneClicked = true;
    await sleep(600);
  }

  document.body.click();
  for (const element of document.querySelectorAll("input")) {
    element.dispatchEvent(new Event("blur", { bubbles: true }));
  }
  await sleep(300);

  return {
    success: true,
    value: input.value,
    doneClicked
  };
})()
`;

const forceAngularFormUpdateScript = `
(async () => {
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  document.body.click();
  for (const input of document.querySelectorAll("input")) {
    input.dispatchEvent(new Event("blur", { bubbles: true }));
  }
  await sleep(300);

  let touched = 0;
  for (const element of document.querySelectorAll("*")) {
    const ctxKey = Object.keys(element).find(key => key.startsWith("__ngContext__"));
    if (!ctxKey) continue;
    for (const item of element[ctxKey] || []) {
      if (!item || typeof item !== "object" || !(item.controls || item.control)) continue;
      const form = item.controls ? item : item.control?.controls ? item.control : null;
      if (!form) continue;
      try { form.markAllAsTouched?.(); } catch {}
      try { form.updateValueAndValidity?.(); } catch {}
      touched++;
    }
    if (touched > 20) break;
  }

  const findButton = Array.from(document.querySelectorAll("button"))
    .find(button => /find flights/i.test(button.textContent || ""));
  return {
    success: true,
    touched,
    findButtonDisabled: Boolean(findButton?.disabled || findButton?.getAttribute("aria-disabled") === "true")
  };
})()
`;

const componentSnapshotScript = `
(() => {
  const safe = (value, depth = 0) => {
    if (depth > 3) return null;
    if (value === null || value === undefined) return value;
    if (typeof value === "function") return "[function]";
    if (value instanceof Date) return value.toISOString();
    if (typeof value !== "object") return value;
    if (Array.isArray(value)) return value.slice(0, 10).map(item => safe(item, depth + 1));
    const output = {};
    for (const key of Object.keys(value).slice(0, 25)) {
      try {
        output[key] = safe(value[key], depth + 1);
      } catch {
        output[key] = "[unreadable]";
      }
    }
    return output;
  };

  for (const element of document.querySelectorAll("*")) {
    const ctxKey = Object.keys(element).find(key => key.startsWith("__ngContext__"));
    if (!ctxKey) continue;
    const ctx = element[ctxKey];
    if (!Array.isArray(ctx)) continue;
    for (let idx = 0; idx < ctx.length; idx++) {
      const item = ctx[idx];
      if (!item || typeof item !== "object") continue;
      if (!("beginDate" in item) || !("origin" in item) || !("destination" in item) || typeof item.search !== "function") {
        continue;
      }

      const keys = Object.keys(item).sort();
      const proto = Object.getPrototypeOf(item);
      const protoMethods = proto
        ? Object.getOwnPropertyNames(proto)
            .filter(name => name !== "constructor" && typeof item[name] === "function")
            .sort()
        : [];

      return {
        found: true,
        idx,
        hostTag: (element.tagName || "").toLowerCase(),
        hostClass: String(element.className || ""),
        keys,
        interestingKeys: keys.filter(name => /(origin|destination|home|station|airport|date|trip|search|market|store)/i.test(name)),
        protoMethods: protoMethods.filter(name => /(origin|destination|home|station|airport|date|trip|search|select|set|update|submit)/i.test(name)),
        gatingValues: {
          departureSelectionComplete: item.departureSelectionComplete,
          arrivalSelectionComplete: item.arrivalSelectionComplete,
          tripType: item.tripType,
          searchStart: item.searchStart,
          searchComplete: item.searchComplete,
          searchButtonDisabled: Boolean(item.searchButton?.disabled),
          searchButtonAriaDisabled: item.searchButton?.getAttribute?.("aria-disabled") || null
        },
        methodBodies: Object.fromEntries(
          ["fromUpdated", "toUpdated", "setSelectedStations", "validateTripSelection", "validateDates", "search"]
            .filter(name => typeof item[name] === "function")
            .map(name => [name, String(item[name]).slice(0, 1200)])
        ),
        fromStationProtoMethods: item.fromStation ? Object.getOwnPropertyNames(Object.getPrototypeOf(item.fromStation) || {})
          .filter(name => name !== "constructor" && typeof item.fromStation[name] === "function")
          .filter(name => /(select|choose|station|input|search|update|focus|click|set)/i.test(name))
          .sort() : [],
        toStationProtoMethods: item.toStation ? Object.getOwnPropertyNames(Object.getPrototypeOf(item.toStation) || {})
          .filter(name => name !== "constructor" && typeof item.toStation[name] === "function")
          .filter(name => /(select|choose|station|input|search|update|focus|click|set)/i.test(name))
          .sort() : [],
        originPreview: safe(item.origin),
        destinationPreview: safe(item.destination),
        beginDatePreview: safe(item.beginDate),
        interestingValues: Object.fromEntries(
          keys
            .filter(name => /(origin|destination|home|station|airport|date|trip|market|arrivalSelectionComplete|departureSelectionComplete)/i.test(name))
            .slice(0, 20)
            .map(name => {
              try {
                return [name, safe(item[name])];
              } catch {
                return [name, "[unreadable]"];
              }
            })
        )
      };
    }
  }

  return { found: false };
})()
`;

const stationOverlayDebugScript = index => `
(async () => {
  const visible = element => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
  const textOf = element => (element.innerText || element.textContent || "").replace(/\\s+/g, " ").trim();
  const stationButtons = Array.from(document.querySelectorAll("[aria-label='Station select'], [aria-label*='Station select']"))
    .filter(visible);
  const trigger = stationButtons[${index}];
  if (!trigger) {
    return { success: false, reason: "station-trigger-not-found" };
  }

  trigger.click();
  await new Promise(resolve => setTimeout(resolve, 900));

  const elements = Array.from(document.querySelectorAll("input, button, [role='button'], [role='option'], [role='dialog'], mat-option, li, .mat-mdc-option, .mat-option"))
    .filter(visible)
    .map(element => ({
      tag: element.tagName.toLowerCase(),
      role: element.getAttribute("role") || "",
      ariaLabel: element.getAttribute("aria-label") || "",
      placeholder: element.getAttribute("placeholder") || "",
      id: element.id || "",
      className: element.className || "",
      text: textOf(element).slice(0, 160)
    }))
    .filter(item => item.text || item.ariaLabel || item.placeholder)
    .slice(0, 150);

  return {
    success: true,
    triggerText: textOf(trigger),
    activeElement: document.activeElement ? {
      tag: document.activeElement.tagName.toLowerCase(),
      ariaLabel: document.activeElement.getAttribute("aria-label") || "",
      placeholder: document.activeElement.getAttribute("placeholder") || "",
      id: document.activeElement.id || "",
      text: textOf(document.activeElement).slice(0, 160)
    } : null,
    elements
  };
})()
`;

const availabilityProbeScript = `
(async ({ origin, destination, beginDate }) => {
  const payload = {
    beginDate,
    destination,
    origin,
    passengers: {
      types: [{ count: 1, type: "ADT" }]
    },
    taxesAndFees: 2,
    filters: {
      maxConnections: 4,
      compressionType: 1,
      sortOptions: [4],
      fareTypes: ["R"],
      exclusionType: 2
    },
    numberOfFaresPerJourney: 10,
    codes: {
      currencyCode: "USD"
    },
    ssrCollectionsMode: 1
  };

  const tokenData = JSON.parse(sessionStorage.getItem("navitaire.digital.token") || "null");
  const token = tokenData?.token || "";

  const response = await fetch("https://api.jsx.com/api/nsk/v4/availability/search/simple", {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json, text/plain, */*",
      "Authorization": token
    },
    body: JSON.stringify(payload)
  });

  const body = await response.text();

  return {
    tokenPresent: Boolean(token),
    tokenPrefix: token.slice(0, 24),
    tokenExpiration: tokenData?.expiration || null,
    status: response.status,
    bodyLength: body.length,
    bodySnippet: body.slice(0, 4000)
  };
})(${JSON.stringify({ origin, destination, beginDate })})
`;

async function main() {
  const chrome = launchChrome();
  let cdp;

  try {
    await waitForDebugger();
    const target = await newTarget();
    log("Connected target", { id: target.id, url: target.url });
    cdp = new CDPClient(target.webSocketDebuggerUrl);
    await cdp.ready();

    const requests = [];
    const responseMetaByRequestId = new Map();

    cdp.on("Network.requestWillBeSent", params => {
      const url = params.request?.url || "";
      if (!url.includes("jsx.com")) {
        return;
      }
      const record = {
        stage: "request",
        type: params.type || "",
        method: params.request?.method || "",
        url,
        postData: params.request?.postData || "",
        hasAuthHeader: Boolean(params.request?.headers?.Authorization),
      };
      requests.push(record);
      console.log(JSON.stringify(record));
    });

    cdp.on("Network.responseReceived", params => {
      const url = params.response?.url || "";
      if (!url.includes("jsx.com")) {
        return;
      }
      const record = {
        stage: "response",
        type: params.type || "",
        status: params.response?.status || 0,
        mimeType: params.response?.mimeType || "",
        url,
      };
      responseMetaByRequestId.set(params.requestId, record);
      requests.push(record);
      console.log(JSON.stringify(record));
    });

    cdp.on("Network.loadingFinished", async params => {
      const meta = responseMetaByRequestId.get(params.requestId);
      if (!meta || !meta.url.includes("api.jsx.com")) {
        return;
      }
      try {
        const body = await cdp.send("Network.getResponseBody", { requestId: params.requestId });
        const snippet = (body.body || "").slice(0, 3000);
        const record = {
          stage: "body",
          status: meta.status,
          url: meta.url,
          bodySnippet: snippet,
          base64Encoded: Boolean(body.base64Encoded),
        };
        requests.push(record);
        console.log(JSON.stringify(record));
      } catch (error) {
        console.log(JSON.stringify({
          stage: "body-error",
          url: meta.url,
          error: error.message,
        }));
      }
    });

    await cdp.send("Page.enable");
    await cdp.send("Runtime.enable");
    await cdp.send("Network.enable", {
      maxTotalBufferSize: 100_000_000,
      maxResourceBufferSize: 10_000_000,
      maxPostDataSize: 1_000_000,
    });
    await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });

    log("Navigating page target", navigationUrl);
    await cdp.send("Page.navigate", { url: navigationUrl });
    await delay(settleMs);

    const consentResult = await cdp.send("Runtime.evaluate", {
      expression: acceptConsentScript,
      awaitPromise: true,
      returnByValue: true,
    });
    log("Consent result", consentResult.result?.value);

    await delay(1500);

    const initialProbe = await cdp.send("Runtime.evaluate", {
      expression: pageProbeScript,
      awaitPromise: true,
      returnByValue: true,
    });
    await writeFile(`${artifactsDir}/page-probe-before-search.json`, JSON.stringify(initialProbe.result?.value || {}, null, 2));
    log("Page probe saved", `${artifactsDir}/page-probe-before-search.json`);

    if (process.env.DEBUG_STATION_OVERLAY === "1") {
      const overlayProbe = await cdp.send("Runtime.evaluate", {
        expression: stationOverlayDebugScript(0),
        awaitPromise: true,
        returnByValue: true,
      });
      await writeFile(
        `${artifactsDir}/station-overlay-debug.json`,
        JSON.stringify(overlayProbe.result?.value || {}, null, 2)
      );
      log("Station overlay debug", overlayProbe.result?.value);
    }

    const popupDismissResult = await cdp.send("Runtime.evaluate", {
      expression: dismissMarketingPopupScript,
      awaitPromise: true,
      returnByValue: true,
    });
    log("Marketing popup result", popupDismissResult.result?.value);

    const tripTypeResult = await cdp.send("Runtime.evaluate", {
      expression: setOneWayTripScript,
      awaitPromise: true,
      returnByValue: true,
    });
    log("Trip type result", tripTypeResult.result?.value);

    const originResult = await cdp.send("Runtime.evaluate", {
      expression: selectStationScript(0, origin),
      awaitPromise: true,
      returnByValue: true,
    });
    log("Origin selection result", originResult.result?.value);

    const destinationResult = await cdp.send("Runtime.evaluate", {
      expression: selectStationScript(1, destination),
      awaitPromise: true,
      returnByValue: true,
    });
    log("Destination selection result", destinationResult.result?.value);

    const dateResult = await cdp.send("Runtime.evaluate", {
      expression: setDepartDateScript(beginDate),
      awaitPromise: true,
      returnByValue: true,
    });
    log("Depart date result", dateResult);

    const formUpdateResult = await cdp.send("Runtime.evaluate", {
      expression: forceAngularFormUpdateScript,
      awaitPromise: true,
      returnByValue: true,
    });
    log("Form update result", formUpdateResult.result?.value);

    const componentSnapshot = await cdp.send("Runtime.evaluate", {
      expression: componentSnapshotScript,
      awaitPromise: true,
      returnByValue: true,
    });
    await writeFile(
      `${artifactsDir}/component-snapshot-before-search.json`,
      JSON.stringify(componentSnapshot.result?.value || {}, null, 2)
    );
    log("Component snapshot", componentSnapshot.result?.value);

    const searchResult = await clickButtonByText(cdp, "/^find flights$/i");
    log("Search click result", searchResult);

    if (process.env.DIRECT_AVAILABILITY_PROBE === "1") {
      const availabilityProbe = await cdp.send("Runtime.evaluate", {
        expression: availabilityProbeScript,
        awaitPromise: true,
        returnByValue: true,
      });
      await writeFile(
        `${artifactsDir}/availability-probe.json`,
        JSON.stringify(availabilityProbe.result?.value || {}, null, 2)
      );
      log("Availability probe", availabilityProbe.result?.value);
    }

    await delay(settleMs);

    const finalProbe = await cdp.send("Runtime.evaluate", {
      expression: pageProbeScript,
      awaitPromise: true,
      returnByValue: true,
    });
    await writeFile(`${artifactsDir}/page-probe-after-search.json`, JSON.stringify(finalProbe.result?.value || {}, null, 2));
    await writeFile(`${artifactsDir}/network-events.json`, JSON.stringify(requests, null, 2));
    log("Artifacts saved", artifactsDir);
  } finally {
    if (cdp) {
      await cdp.close();
    }
    chrome.kill("SIGTERM");
    await delay(500);
    await rm(userDataDir, { recursive: true, force: true });
  }
}

await main();
