/**
 * Flock Network 抓包 POC：真实打开 get-a-quote，录 HAR + 旁路 XHR 日志，
 * 分析是否存在可直连的 quote/rate JSON 接口（对齐 MotherShip axel/quote 思路）。
 *
 * 用法:
 *   npm run capture:flock-har
 *   RPA_HEADLESS=false npm run capture:flock-har
 */
import fs from "node:fs";
import path from "node:path";
import type { Page, Request, Response } from "playwright";
import {
  analyzeFlockHarEntries,
  analyzeFlockNetworkLog,
  mergeAndDedupeCandidates,
  summarizeFlockHosts,
  verdictFromCandidates,
  type FlockHarFile,
  type FlockNetworkLogEntry,
  type FlockQuoteApiCandidate,
} from "@/lib/flock/har-analyze";
import { resolveFlockQuoteAccount } from "@/lib/flock/resolve-account";
import { getFlockQuoteUrl, getFlockQuoteWaitMs } from "@/lib/flock/env";
import { createRpaBrowserContext } from "@/lib/rpa/browser-context";
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
import {
  formatExternalSiteNavigationError,
  gotoWithResilience,
  isTransientNavigationError,
} from "@/lib/rpa/page-goto";
import { DEFAULT_FLOCK_DEMO } from "@/workers/rpa/flock/demo-input";
import {
  clickFlockQuoteSubmit,
  fillFlockQuoteForm,
  waitForFlockQuoteForm,
} from "@/workers/rpa/flock/form-interactions";
import {
  completeFlockRegistrationIfNeeded,
  isFlockQuoteResultsPage,
  isFlockRegistrationPage,
} from "@/workers/rpa/flock/register-account";
import { extractFlockQuoteCards } from "@/workers/rpa/flock/quote-extract";
import {
  flockQuoteLimitMessage,
  flockRegistrationErrorMessage,
} from "@/workers/rpa/flock/page-state";
import type { FlockQuoteInput } from "@/workers/rpa/flock/types";

const ROOT = process.cwd();
const OUT_DIR = path.join(ROOT, ".rpa", "flock-network-poc");

function stamp(): string {
  const d = new Date();
  const p = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
}

function demoToInput(): FlockQuoteInput {
  return {
    pickupDate: DEFAULT_FLOCK_DEMO.pickupDateDisplay,
    pickupZip: DEFAULT_FLOCK_DEMO.pickupZip,
    deliveryZip: DEFAULT_FLOCK_DEMO.deliveryZip,
    palletCount: DEFAULT_FLOCK_DEMO.palletCount,
    totalWeightLb: DEFAULT_FLOCK_DEMO.totalWeightLb,
    lengthIn: DEFAULT_FLOCK_DEMO.lengthIn,
    widthIn: DEFAULT_FLOCK_DEMO.widthIn,
    heightIn: DEFAULT_FLOCK_DEMO.heightIn,
  };
}

function shouldCaptureRequest(req: Request): boolean {
  const type = req.resourceType();
  if (type === "document" || type === "stylesheet" || type === "image" || type === "font" || type === "media") {
    return false;
  }
  const url = req.url();
  if (/\.(js|css|png|jpe?g|gif|svg|woff2?|map)(\?|$)/i.test(url)) {
    return false;
  }
  return type === "xhr" || type === "fetch" || type === "other" || type === "websocket";
}

async function attachNetworkLogger(
  page: Page,
  sink: FlockNetworkLogEntry[],
): Promise<void> {
  page.on("response", async (res: Response) => {
    try {
      const req = res.request();
      if (!shouldCaptureRequest(req)) return;
      const headers = res.headers();
      const mime = headers["content-type"] ?? "";
      let responseBodyPreview: string | undefined;
      let requestBodyPreview: string | undefined;
      try {
        const post = req.postData();
        if (post) requestBodyPreview = post.slice(0, 4_000);
      } catch {
        /* ignore */
      }
      if (/json|text|javascript|graphql/i.test(mime) || mime === "") {
        try {
          const body = await res.text();
          responseBodyPreview = body.slice(0, 8_000);
        } catch {
          /* body may be unavailable */
        }
      }
      sink.push({
        method: req.method(),
        url: req.url(),
        status: res.status(),
        mimeType: mime,
        requestBodyPreview,
        responseBodyPreview,
        resourceType: req.resourceType(),
      });
    } catch {
      /* ignore listener errors */
    }
  });
}

function writeSummaryMarkdown(opts: {
  outPath: string;
  harPath: string;
  logPath: string;
  verdict: string;
  quoteOk: boolean;
  quoteError?: string;
  prices?: string;
  hosts: Array<{ host: string; count: number; methods: string[] }>;
  candidates: FlockQuoteApiCandidate[];
}): void {
  const lines = [
    "# Flock Network 抓包 POC 结论",
    "",
    `- 时间: ${new Date().toISOString()}`,
    `- HAR: \`${opts.harPath}\``,
    `- 旁路日志: \`${opts.logPath}\``,
    `- DOM 抓价成功: ${opts.quoteOk ? "是" : "否"}${opts.prices ? `（${opts.prices}）` : ""}`,
    opts.quoteError ? `- DOM 失败原因: ${opts.quoteError}` : "",
    `- **裁决: \`${opts.verdict}\`**`,
    "",
    "## 非静态主机 Top",
    "",
    ...opts.hosts.slice(0, 20).map(
      (h) => `- \`${h.host}\` ×${h.count} methods=${h.methods.join(",")}`,
    ),
    "",
    "## quote/rate 候选接口",
    "",
  ];
  if (opts.candidates.length === 0) {
    lines.push(
      "_未发现 score≥4 的 JSON quote/rate 候选。当前路径更可能依赖 DOM SSR/内嵌数据，与 MotherShip `axel/quote` 不同。_",
      "",
    );
  } else {
    for (const [i, c] of opts.candidates.slice(0, 15).entries()) {
      lines.push(
        `### ${i + 1}. score=${c.score} ${c.method} ${c.status}`,
        "",
        `- URL: \`${c.urlWithoutQuery}\``,
        `- source: ${c.source}`,
        c.mimeType ? `- mime: ${c.mimeType}` : "",
        c.requestPreview
          ? `- request preview:\n\`\`\`\n${c.requestPreview}\n\`\`\``
          : "",
        c.responsePreview
          ? `- response preview:\n\`\`\`\n${c.responsePreview}\n\`\`\``
          : "",
        "",
      );
    }
  }
  lines.push(
    "## 含义",
    "",
    "- `stable_json_quote_api_found`: 存在高分 JSON 报价接口，可继续做 Direct 风格 POC",
    "- `json_api_candidates_unclear`: 有 API 候选但契约不明，需人工核对 HAR",
    "- `no_quote_json_api_dom_only`: 未见稳定 quote JSON；继续 DOM RPA 或更深抓包",
    "",
  );
  fs.writeFileSync(
    opts.outPath,
    lines.filter((l) => l !== "").join("\n") + "\n",
    "utf8",
  );
}

async function main(): Promise<void> {
  if (process.env.RPA_MOCK_MODE === "true") {
    console.error("capture:flock-har 需要真实站点；请勿设 RPA_MOCK_MODE=true");
    process.exit(2);
  }

  fs.mkdirSync(OUT_DIR, { recursive: true });
  const id = stamp();
  const harPath = path.join(OUT_DIR, `flock-${id}.har`);
  const logPath = path.join(OUT_DIR, `flock-${id}-network.json`);
  const summaryJson = path.join(OUT_DIR, `flock-${id}-summary.json`);
  const summaryMd = path.join(OUT_DIR, `flock-${id}-summary.md`);

  const input = demoToInput();
  const account = resolveFlockQuoteAccount(input);
  const url = getFlockQuoteUrl();
  const headless = process.env.RPA_HEADLESS !== "false";
  const quoteWaitMs = getFlockQuoteWaitMs();
  const networkLog: FlockNetworkLogEntry[] = [];

  console.log(`[capture-flock-har] url=${url}`);
  console.log(`[capture-flock-har] sample ${input.pickupZip}→${input.deliveryZip} ${input.palletCount}pl ${input.totalWeightLb}lb`);
  console.log(`[capture-flock-har] email=${account.email} headless=${headless}`);
  console.log(`[capture-flock-har] har → ${harPath}`);

  const browser = await launchRpaBrowser({ headless, incognito: true });
  let quoteOk = false;
  let quoteError: string | undefined;
  let prices: string | undefined;

  try {
    const context = await createRpaBrowserContext(browser, {
      recordHar: {
        path: harPath,
        mode: "full",
        content: "embed",
      },
    });
    const page = await context.newPage();
    await attachNetworkLogger(page, networkLog);

    try {
      await gotoWithResilience(page, url);
    } catch (error) {
      const raw = error instanceof Error ? error.message : String(error);
      quoteError = isTransientNavigationError({ message: raw })
        ? formatExternalSiteNavigationError("Flock Freight 官网", {
            message: raw,
          })
        : raw;
      await context.close();
      throw new Error(quoteError);
    }

    if (!(await waitForFlockQuoteForm(page))) {
      quoteError =
        (await flockQuoteLimitMessage(page)) ??
        "QUOTE_ENTRY_UNAVAILABLE：表单未加载";
      await context.close();
      throw new Error(quoteError);
    }

    const filled = await fillFlockQuoteForm(page, input);
    if (!filled.ok) {
      quoteError = filled.error;
      await context.close();
      throw new Error(quoteError ?? "填表失败");
    }
    if (!(await clickFlockQuoteSubmit(page))) {
      quoteError = "STRUCT_CHANGE：未找到提交按钮";
      await context.close();
      throw new Error(quoteError);
    }

    const deadline = Date.now() + quoteWaitMs;
    while (Date.now() < deadline) {
      if (await flockQuoteLimitMessage(page)) break;
      if (await isFlockRegistrationPage(page)) {
        const ok = await completeFlockRegistrationIfNeeded(page, account);
        if (!ok) {
          quoteError =
            (await flockRegistrationErrorMessage(page)) ??
            "进入注册页但无法完成注册";
          break;
        }
      }
      if (await isFlockQuoteResultsPage(page)) break;
      await page.waitForTimeout(800);
    }

    if (!quoteError) {
      const limitMsg = await flockQuoteLimitMessage(page);
      if (limitMsg) {
        quoteError = limitMsg;
      } else if (await isFlockRegistrationPage(page)) {
        quoteError =
          (await flockRegistrationErrorMessage(page)) ??
          "停在注册页，未到结果卡";
      } else {
        const { quotes, reference } = await extractFlockQuoteCards(page);
        if (quotes.length >= 2) {
          quoteOk = true;
          prices = quotes.map((q) => `${q.label}:$${q.totalUsd}`).join(", ");
          console.log(`[capture-flock-har] DOM OK ref=${reference ?? "-"} ${prices}`);
        } else {
          quoteError = `仅抓到 ${quotes.length} 档 DOM 报价（仍可分析 HAR）`;
        }
      }
    }

    // 必须 close context 才能落盘 HAR
    await context.close();
  } finally {
    await browser.close();
  }

  fs.writeFileSync(logPath, JSON.stringify(networkLog, null, 2), "utf8");

  let harEntries: NonNullable<NonNullable<FlockHarFile["log"]>["entries"]> = [];
  if (fs.existsSync(harPath)) {
    const har = JSON.parse(fs.readFileSync(harPath, "utf8")) as FlockHarFile;
    harEntries = har.log?.entries ?? [];
  }

  const fromHar = analyzeFlockHarEntries(harEntries);
  const fromLog = analyzeFlockNetworkLog(networkLog);
  const candidates = mergeAndDedupeCandidates([fromHar, fromLog]);
  const hosts = summarizeFlockHosts(harEntries);
  const verdict = verdictFromCandidates(candidates);

  const payload = {
    capturedAt: new Date().toISOString(),
    harPath,
    logPath,
    quoteOk,
    quoteError: quoteError ?? null,
    prices: prices ?? null,
    harEntryCount: harEntries.length,
    networkLogCount: networkLog.length,
    verdict,
    hosts: hosts.slice(0, 30),
    candidates: candidates.slice(0, 20),
  };
  fs.writeFileSync(summaryJson, JSON.stringify(payload, null, 2), "utf8");
  writeSummaryMarkdown({
    outPath: summaryMd,
    harPath,
    logPath,
    verdict,
    quoteOk,
    quoteError,
    prices,
    hosts,
    candidates,
  });

  console.log(`[capture-flock-har] HAR entries=${payload.harEntryCount} networkLog=${payload.networkLogCount}`);
  console.log(`[capture-flock-har] candidates=${candidates.length} verdict=${verdict}`);
  console.log(`[capture-flock-har] summary → ${summaryMd}`);
  for (const c of candidates.slice(0, 8)) {
    console.log(`  [${c.score}] ${c.method} ${c.status} ${c.urlWithoutQuery}`);
  }

  // 抓包本身成功即 exit 0；无候选时 exit 3 便于脚本判定
  if (!fs.existsSync(harPath) || payload.harEntryCount < 5) {
    process.exit(1);
  }
  if (verdict === "no_quote_json_api_dom_only") {
    process.exit(3);
  }
}

main().catch((err) => {
  console.error("[capture-flock-har] FAIL:", err instanceof Error ? err.message : err);
  process.exit(1);
});
