/**
 * Flock 批量随机探针 — 20 组 ZIP + 货物（差距大），统计失败原因
 *
 * 用法:
 *   RPA_BROWSER_CHANNEL=chrome npm run batch:flock-stress
 *   FLOCK_BATCH_ROUNDS=20 FLOCK_BATCH_DELAY_MS=20000 npm run batch:flock-stress
 */
import fs from "node:fs";
import path from "node:path";
import type { Page } from "playwright";
import {
  FLOCK_LIMITS,
  fitsFlockTrailerCargo,
} from "@/lib/constants/flock-limits";
import { createFreshFlockTestAccount } from "@/lib/flock/fresh-account";
import { withFlockExclusiveLock } from "@/lib/flock/exclusive-lock";
import {
  getFlockMinQuotes,
  getFlockQuoteUrl,
  getFlockQuoteWaitMs,
  getFlockQuotesPerEmail,
  type FlockTestAccount,
} from "@/lib/flock/env";
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
import { createRpaBrowserContext } from "@/lib/rpa/browser-context";
import { gotoWithResilience } from "@/lib/rpa/page-goto";
import {
  navigateFlockNewQuote,
  waitForFlockQuoteForm,
} from "@/workers/rpa/flock/form-interactions";
import {
  runFlockQuoteOnPage,
  runFlockVisualChain,
} from "@/workers/rpa/flock/visual-chain";
import type { FlockQuoteInput } from "@/workers/rpa/flock/types";

const US_ZIPS = [
  { zip: "02109", city: "Boston", state: "MA" },
  { zip: "94102", city: "San Francisco", state: "CA" },
  { zip: "60611", city: "Chicago", state: "IL" },
  { zip: "78701", city: "Austin", state: "TX" },
  { zip: "21230", city: "Baltimore", state: "MD" },
  { zip: "19106", city: "Philadelphia", state: "PA" },
  { zip: "15222", city: "Pittsburgh", state: "PA" },
  { zip: "30326", city: "Atlanta", state: "GA" },
  { zip: "90212", city: "Beverly Hills", state: "CA" },
  { zip: "90248", city: "Gardena", state: "CA" },
] as const;

/**
 * 合法样例：字段硬限内 AND 线性英尺 ≤53'
 * （禁止 18×403 / 20×636 等会触发官网 Linear feet 拦截的组合）
 */
const CARGO_PRESETS = [
  { palletCount: 4, totalWeightLb: 2_800, lengthIn: 48, widthIn: 40, heightIn: 48 },
  { palletCount: 6, totalWeightLb: 5_500, lengthIn: 48, widthIn: 40, heightIn: 72 },
  { palletCount: 8, totalWeightLb: 9_200, lengthIn: 48, widthIn: 40, heightIn: 60 },
  { palletCount: 10, totalWeightLb: 14_000, lengthIn: 48, widthIn: 40, heightIn: 72 },
  { palletCount: 12, totalWeightLb: 18_500, lengthIn: 48, widthIn: 48, heightIn: 96 },
  { palletCount: 14, totalWeightLb: 22_000, lengthIn: 48, widthIn: 40, heightIn: 84 },
  { palletCount: 16, totalWeightLb: 28_000, lengthIn: 48, widthIn: 40, heightIn: 96 },
  { palletCount: 18, totalWeightLb: 32_000, lengthIn: 48, widthIn: 40, heightIn: 108 },
  { palletCount: 20, totalWeightLb: 38_000, lengthIn: 48, widthIn: 40, heightIn: 96 },
  { palletCount: 5, totalWeightLb: 45_000, lengthIn: 48, widthIn: 40, heightIn: 108 },
  { palletCount: 4, totalWeightLb: 8_000, lengthIn: 96, widthIn: 48, heightIn: 72 },
  { palletCount: 6, totalWeightLb: 12_000, lengthIn: 96, widthIn: 48, heightIn: 84 },
  { palletCount: 8, totalWeightLb: 16_000, lengthIn: 72, widthIn: 48, heightIn: 72 },
  { palletCount: 4, totalWeightLb: 20_000, lengthIn: 120, widthIn: 96, heightIn: 96 },
  { palletCount: 4, totalWeightLb: 25_000, lengthIn: 144, widthIn: 96, heightIn: 108 },
  { palletCount: 6, totalWeightLb: 15_000, lengthIn: 80, widthIn: 80, heightIn: 80 },
  { palletCount: 8, totalWeightLb: 18_000, lengthIn: 60, widthIn: 48, heightIn: 60 },
  { palletCount: 10, totalWeightLb: 11_000, lengthIn: 60, widthIn: 40, heightIn: 72 },
  { palletCount: 12, totalWeightLb: 9_000, lengthIn: 48, widthIn: 40, heightIn: 48 },
  { palletCount: 7, totalWeightLb: 35_000, lengthIn: 48, widthIn: 102, heightIn: 108 },
] as const;

function mulberry32(seed: number): () => number {
  return () => {
    let t = (seed += 0x6d2b79f5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function pickupDateOffsetDays(offset: number): string {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  return `${mm}/${dd}/${d.getFullYear()}`;
}

function pickZipPair(rng: () => number): {
  pickup: (typeof US_ZIPS)[number];
  delivery: (typeof US_ZIPS)[number];
} {
  const pickupIdx = Math.floor(rng() * US_ZIPS.length);
  let deliveryIdx = Math.floor(rng() * US_ZIPS.length);
  if (deliveryIdx === pickupIdx) {
    deliveryIdx = (deliveryIdx + 1 + Math.floor(rng() * (US_ZIPS.length - 1))) % US_ZIPS.length;
  }
  return { pickup: US_ZIPS[pickupIdx]!, delivery: US_ZIPS[deliveryIdx]! };
}

function clampInt(n: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, Math.round(n)));
}

/**
 * 在 FLOCK 硬限 + 拖车线性英尺内随机；差距大但不会触发官网 Linear feet 报错
 */
export function randomCargo(rng: () => number): {
  palletCount: number;
  totalWeightLb: number;
  lengthIn: number;
  widthIn: number;
  heightIn: number;
} {
  for (let attempt = 0; attempt < 40; attempt += 1) {
    const base = CARGO_PRESETS[Math.floor(rng() * CARGO_PRESETS.length)]!;
    const palletCount = clampInt(
      base.palletCount + Math.floor((rng() - 0.5) * 6),
      FLOCK_LIMITS.palletCount.min,
      FLOCK_LIMITS.palletCount.max,
    );
    const widthIn = clampInt(
      base.widthIn + Math.floor((rng() - 0.5) * 24),
      40,
      FLOCK_LIMITS.dimIn.widthMax,
    );
    const across = widthIn <= 51 ? 2 : 1;
    const maxRows = Math.floor(
      (FLOCK_LIMITS.trailerLinearFeetMax * 12) / 48,
    );
    const maxLengthByTrailer = Math.floor(
      (FLOCK_LIMITS.trailerLinearFeetMax * 12) /
        Math.ceil(palletCount / across),
    );
    const lengthIn = clampInt(
      base.lengthIn + Math.floor((rng() - 0.5) * 40),
      48,
      Math.min(FLOCK_LIMITS.dimIn.lengthMax, maxLengthByTrailer, 144),
    );
    const heightIn = clampInt(
      base.heightIn + Math.floor((rng() - 0.5) * 24),
      48,
      FLOCK_LIMITS.dimIn.heightMax,
    );
    const totalWeightLb = clampInt(
      base.totalWeightLb + Math.floor((rng() - 0.5) * 8_000),
      2_000,
      FLOCK_LIMITS.totalWeightLbMax,
    );

    const cargo = { palletCount, totalWeightLb, lengthIn, widthIn, heightIn };
    if (
      lengthIn >= 48 &&
      maxRows >= 1 &&
      fitsFlockTrailerCargo(cargo)
    ) {
      return cargo;
    }
  }

  // 兜底：标准托盘双排，必过线性英尺
  return {
    palletCount: 6,
    totalWeightLb: 3_500,
    lengthIn: 48,
    widthIn: 40,
    heightIn: 48,
  };
}

export function buildFlockBatchCases(count: number, seed = 20260713): FlockQuoteInput[] {
  const rng = mulberry32(seed);
  const cases: FlockQuoteInput[] = [];
  for (let i = 0; i < count; i += 1) {
    const { pickup, delivery } = pickZipPair(rng);
    const cargo = randomCargo(rng);
    const dayOffset = 1 + Math.floor(rng() * 7);
    cases.push({
      pickupDate: pickupDateOffsetDays(dayOffset),
      pickupZip: pickup.zip,
      deliveryZip: delivery.zip,
      palletCount: cargo.palletCount,
      totalWeightLb: cargo.totalWeightLb,
      lengthIn: cargo.lengthIn,
      widthIn: cargo.widthIn,
      heightIn: cargo.heightIn,
    });
  }
  return cases;
}

export function classifyFlockFailure(message: string | undefined): string {
  const m = (message ?? "").toLowerCase();
  if (!m) return "unknown";
  if (/flock_quote_limit|thank you for your interest|too many|rate limit|try again later|limit.*quote|quota|throttl|达到.*上限|报价.*上限/i.test(
    m,
  )) {
    return "rate_limit";
  }
  if (/quote_entry_unavailable|表单未加载|无法定位/i.test(m)) {
    return "form_unavailable";
  }
  if (/flock_account_rejected|issue creating your account/i.test(m)) {
    return "account_rejected";
  }
  if (/flock_account_exists|already (have|registered)/i.test(m)) {
    return "account_exists";
  }
  if (/struct_change|注册页|get instant quote/i.test(m)) {
    return "registration_blocked";
  }
  if (/rpa_data_invalid|未抓齐|no.*quote|prices from/i.test(m)) {
    return "no_quote_result";
  }
  if (/timeout|timed out|err_timed_out/i.test(m)) {
    return "timeout";
  }
  if (/connection|network|reset/i.test(m)) {
    return "network";
  }
  return "other";
}

type BatchRow = {
  round: number;
  attempt: number;
  ok: boolean;
  email?: string;
  pickupZip: string;
  deliveryZip: string;
  cargo: Omit<FlockQuoteInput, "pickupDate" | "pickupZip" | "deliveryZip">;
  pickupDate: string;
  prices?: string;
  error?: string;
  failureClass?: string;
  durationMs: number;
};

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function resolveOneBrowserMode(): boolean {
  const raw = process.env.FLOCK_BATCH_ONE_BROWSER?.trim().toLowerCase();
  if (raw === "true" || raw === "1" || raw === "yes") {
    return true;
  }
  if (raw === "false" || raw === "0" || raw === "no") {
    return false;
  }
  // qqdna 探针：每轮全新浏览器；其它模式默认可单会话续查
  const mode = process.env.FLOCK_EMAIL_MODE?.trim().toLowerCase();
  if (mode === "qqdna" || mode === "qq_dna") {
    return false;
  }
  return true;
}

async function openFlockBrowserSession(
  url: string,
  headless: boolean,
): Promise<{
  browser: Awaited<ReturnType<typeof launchRpaBrowser>>;
  page: Page;
  account: FlockTestAccount;
}> {
  const account = createFreshFlockTestAccount();
  const browser = await launchRpaBrowser({ headless, incognito: true });
  const context = await createRpaBrowserContext(browser);
  const page = await context.newPage();
  await gotoWithResilience(page, url);
  return { browser, page, account };
}

async function runBatchOneBrowser(
  cases: FlockQuoteInput[],
  delayMs: number,
  maxRetries: number,
  rateLimitDelayMs: number,
): Promise<BatchRow[]> {
  const url = getFlockQuoteUrl();
  const headless = process.env.RPA_HEADLESS !== "false";
  const quotesPerEmail = getFlockQuotesPerEmail();
  const perEmailLabel = Number.isFinite(quotesPerEmail)
    ? String(quotesPerEmail)
    : "不限";
  console.log(
    `[batch-flock] 单浏览器模式 quotesPerEmail=${perEmailLabel}（满额后换新随机邮箱+重注册）`,
  );

  const rows: BatchRow[] = [];
  let browser: Awaited<ReturnType<typeof launchRpaBrowser>> | null = null;
  let page: Page | null = null;
  let account: FlockTestAccount | null = null;
  let sessionQuoteCount = 0;

  const startSession = async (): Promise<void> => {
    if (browser) {
      await browser.close();
      browser = null;
      page = null;
      account = null;
    }
    const session = await openFlockBrowserSession(url, headless);
    browser = session.browser;
    page = session.page;
    account = session.account;
    sessionQuoteCount = 0;
    console.log(
      `[batch-flock] 新会话 email=${account.email}（本邮箱最多 ${perEmailLabel} 次）`,
    );
  };

  try {
    for (let i = 0; i < cases.length; i += 1) {
      const input = cases[i]!;
      const round = i + 1;

      if (sessionQuoteCount >= quotesPerEmail || !page || !account) {
        await startSession();
      } else if (i > 0) {
        if (delayMs > 0) {
          await sleep(delayMs);
        }
        await navigateFlockNewQuote(page, url);
      }

      console.log(
        `\n[batch-flock] === ${round}/${cases.length} === ${account!.email} ` +
          `${input.pickupZip}→${input.deliveryZip} ` +
          `${input.palletCount}pl ${input.totalWeightLb}lb ${input.lengthIn}x${input.widthIn}x${input.heightIn}in`,
      );

      const started = Date.now();
      let attempt = 1;
      let result = await (async () => {
        if (!(await waitForFlockQuoteForm(page!))) {
          return {
            ok: false,
            quotes: [],
            errorMessage: "QUOTE_ENTRY_UNAVAILABLE：查价表单未加载",
          };
        }
        return runFlockQuoteOnPage(page!, input, account!);
      })();

      while (attempt < maxRetries && (!result.ok || result.quotes.length < getFlockMinQuotes())) {
        const cls = classifyFlockFailure(result.errorMessage);
        if (cls === "rate_limit") {
          console.log(
            `[batch-flock] 报价上限，等待 ${rateLimitDelayMs}ms 后重试…`,
          );
          await sleep(rateLimitDelayMs);
        } else {
          console.log(
            `[batch-flock] 重试 ${attempt + 1}/${maxRetries}: [${cls}] ${result.errorMessage}`,
          );
          await sleep(Math.min(delayMs, 10_000));
        }
        attempt += 1;
        if (!(await waitForFlockQuoteForm(page!))) {
          await navigateFlockNewQuote(page!, url);
          await waitForFlockQuoteForm(page!);
        }
        result = await runFlockQuoteOnPage(page!, input, account!);
      }

      const durationMs = Date.now() - started;
      const ok = result.ok && result.quotes.length >= getFlockMinQuotes();
      const failureClass = ok ? undefined : classifyFlockFailure(result.errorMessage);
      rows.push({
        round,
        attempt,
        ok,
        email: account!.email,
        pickupZip: input.pickupZip,
        deliveryZip: input.deliveryZip,
        pickupDate: input.pickupDate,
        cargo: {
          palletCount: input.palletCount,
          totalWeightLb: input.totalWeightLb,
          lengthIn: input.lengthIn,
          widthIn: input.widthIn,
          heightIn: input.heightIn,
        },
        prices: ok
          ? result.quotes.map((q) => `${q.label}:$${q.totalUsd}`).join(", ")
          : undefined,
        error: ok ? undefined : result.errorMessage,
        failureClass,
        durationMs,
      });

      if (ok) {
        console.log(`[batch-flock] OK (${durationMs}ms) ${rows.at(-1)?.prices}`);
        sessionQuoteCount += 1;
      } else {
        console.log(`[batch-flock] FAIL: [${failureClass}] ${result.errorMessage}`);
        if (failureClass === "account_rejected" || failureClass === "account_exists" || failureClass === "rate_limit") {
          sessionQuoteCount = quotesPerEmail;
        } else {
          sessionQuoteCount += 1;
        }
      }
    }
  } finally {
    if (browser) {
      await browser.close();
    }
  }

  return rows;
}

async function main(): Promise<void> {
  process.env.FLOCK_FRESH_SESSION_PER_QUOTE ??= "true";
  process.env.FLOCK_RANDOM_ACCOUNT_PER_QUOTE ??= "true";
  process.env.FLOCK_MIN_QUOTES ??= "1";
  process.env.FLOCK_QUOTE_WAIT_MS ??= "20000";
  process.env.FLOCK_FIELD_PAUSE_MS ??= "1000";
  process.env.FLOCK_SESSION_MAX_ROTATIONS ??= "3";

  const stallsDir = path.join(process.cwd(), ".rpa", "flock-stalls");
  if (fs.existsSync(stallsDir)) {
    for (const name of fs.readdirSync(stallsDir)) {
      fs.unlinkSync(path.join(stallsDir, name));
    }
    console.log(`[batch-flock] 已清空 ${stallsDir}`);
  }

  const rounds = Number(process.env.FLOCK_BATCH_ROUNDS ?? "10");
  const delayMs = Number(process.env.FLOCK_BATCH_DELAY_MS ?? "1000");
  const maxRetries = Number(process.env.FLOCK_BATCH_MAX_RETRIES ?? "1");
  const rateLimitDelayMs = Number(
    process.env.FLOCK_BATCH_RATE_LIMIT_DELAY_MS ?? "45000",
  );
  const seed = Number(process.env.FLOCK_BATCH_SEED ?? String(Date.now()));
  const minQuotes = getFlockMinQuotes();
  const quoteWaitMs = getFlockQuoteWaitMs();
  const quotesPerEmail = getFlockQuotesPerEmail();

  const cases = buildFlockBatchCases(rounds, seed);
  const oneBrowser = resolveOneBrowserMode();
  let rows: BatchRow[] = [];
  const summary: Record<string, number> = {};

  console.log(`[batch-flock] rounds=${rounds} delay=${delayMs}ms seed=${seed}`);
  console.log(
    oneBrowser
      ? "[batch-flock] 单浏览器连续查价（首轮注册，后续 Build another quote）"
      : "[batch-flock] 每轮无痕新浏览器 + 已验证 QQ 短别名",
  );
  console.log(
    `[batch-flock] limits: pallets ${FLOCK_LIMITS.palletCount.min}-${FLOCK_LIMITS.palletCount.max}, weight≤${FLOCK_LIMITS.totalWeightLbMax}lb`,
  );
  console.log(
    `[batch-flock] maxRetries=${maxRetries} rateLimitDelay=${rateLimitDelayMs}ms ` +
      `minQuotes=${minQuotes} quoteWait=${quoteWaitMs}ms ` +
      `quotesPerEmail=${Number.isFinite(quotesPerEmail) ? quotesPerEmail : "不限"}`,
  );

  const sessionStarted = Date.now();

  // 与 embed Worker 互斥：禁止本机两路浏览器同时打 Flock（共 IP 互踩）
  await withFlockExclusiveLock(
    "batch",
    async () => {
  if (oneBrowser) {
    rows = await runBatchOneBrowser(cases, delayMs, maxRetries, rateLimitDelayMs);
    for (const row of rows.filter((r) => !r.ok)) {
      const cls = row.failureClass ?? "unknown";
      summary[cls] = (summary[cls] ?? 0) + 1;
    }
  } else {
  for (let i = 0; i < cases.length; i += 1) {
    const input = cases[i]!;
    const round = i + 1;
    if (i > 0 && delayMs > 0) {
      await sleep(delayMs);
    }

    console.log(
      `\n[batch-flock] === ${round}/${rounds} === ${input.pickupZip}→${input.deliveryZip} ` +
        `${input.palletCount}pl ${input.totalWeightLb}lb ${input.lengthIn}x${input.widthIn}x${input.heightIn}in`,
    );

    const started = Date.now();
    let result = await runFlockVisualChain({ input });
    let attempt = 1;
    while (
      attempt < maxRetries &&
      (!result.ok || result.quotes.length < minQuotes)
    ) {
      const cls = classifyFlockFailure(result.errorMessage);
      if (cls === "rate_limit") {
        console.log(
          `[batch-flock] 报价上限，等待 ${rateLimitDelayMs}ms 后重试 (${attempt + 1}/${maxRetries})…`,
        );
        await sleep(rateLimitDelayMs);
      } else {
        console.log(
          `[batch-flock] 重试 ${attempt + 1}/${maxRetries}: [${cls}] ${result.errorMessage}`,
        );
        await sleep(Math.min(delayMs, 10_000));
      }
      attempt += 1;
      result = await runFlockVisualChain({ input });
    }
    const durationMs = Date.now() - started;
    const ok = result.ok && result.quotes.length >= minQuotes;

    const row: BatchRow = {
      round,
      attempt,
      ok,
      pickupZip: input.pickupZip,
      deliveryZip: input.deliveryZip,
      pickupDate: input.pickupDate,
      cargo: {
        palletCount: input.palletCount,
        totalWeightLb: input.totalWeightLb,
        lengthIn: input.lengthIn,
        widthIn: input.widthIn,
        heightIn: input.heightIn,
      },
      prices: ok
        ? result.quotes.map((q) => `${q.label}:$${q.totalUsd}`).join(", ")
        : undefined,
      error: ok ? undefined : result.errorMessage,
      failureClass: ok ? undefined : classifyFlockFailure(result.errorMessage),
      durationMs,
    };
    rows.push(row);

    if (ok) {
      console.log(`[batch-flock] OK (${durationMs}ms) ${row.prices}`);
    } else {
      console.log(`[batch-flock] FAIL: [${row.failureClass}] ${row.error}`);
      const cls = row.failureClass ?? "unknown";
      summary[cls] = (summary[cls] ?? 0) + 1;
    }
  }
  }
    },
    { waitMs: 60_000, pollMs: 1_000 },
  );

  const sessionMs = Date.now() - sessionStarted;
  console.log(`[batch-flock] session total ${sessionMs}ms`);

  const passed = rows.filter((r) => r.ok).length;
  const report = {
    at: new Date().toISOString(),
    rounds,
    passed,
    failed: rounds - passed,
    failureSummary: summary,
    rows,
  };

  const outDir = path.join(process.cwd(), ".rpa");
  fs.mkdirSync(outDir, { recursive: true });
  const reportPath = path.join(outDir, `flock-batch-${Date.now()}.json`);
  fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");

  console.log(`\n[batch-flock] ========== 汇总 ==========`);
  console.log(`通过: ${passed}/${rounds}`);
  if (passed < rounds) {
    console.log("失败分类:", summary);
    for (const r of rows.filter((x) => !x.ok)) {
      console.log(
        `  #${r.round} ${r.pickupZip}→${r.deliveryZip} [${r.failureClass}] ${r.error}`,
      );
    }
  }
  console.log(`报告: ${reportPath}`);

  if (passed < rounds) {
    process.exitCode = 1;
  }
}

const isDirectRun =
  typeof process.argv[1] === "string" &&
  path.resolve(process.argv[1]) === path.resolve(__filename);

if (isDirectRun) {
  main().catch((error) => {
    console.error("[batch-flock] fatal:", error);
    process.exit(1);
  });
}
