import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { ADDRESS_CANDIDATES_TIMEOUT_MS } from "@/lib/constants/rpa";
import { readInfraEnvOverrides } from "@/lib/dev-env";
import type {
  AddressLookupInput,
  MothershipCandidatesResult,
} from "@/modules/address/types";
import { RpaError, type RpaErrorCode } from "@/modules/rpa/errors";

const CHILD_STARTUP_BUFFER_MS = 15_000;

type ChildSuccess = { ok: true; data: MothershipCandidatesResult };
type ChildFailure = {
  ok: false;
  code: RpaErrorCode | "INTERNAL_ERROR";
  message: string;
  retryable?: boolean;
};

function resolveTsxCommand(): { command: string; args: string[]; shell: boolean } {
  const root = process.cwd();
  const script = path.join(root, "scripts/rpa/fetch-address-candidates.ts");
  const tsxCmd = path.join(root, "node_modules", ".bin", "tsx.cmd");
  const tsxBin = path.join(root, "node_modules", ".bin", "tsx");

  if (process.platform === "win32" && fs.existsSync(tsxCmd)) {
    return { command: tsxCmd, args: [script], shell: true };
  }
  if (fs.existsSync(tsxBin)) {
    return { command: tsxBin, args: [script], shell: false };
  }
  return {
    command: process.platform === "win32" ? "npx.cmd" : "npx",
    args: ["tsx", script],
    shell: process.platform === "win32",
  };
}

function parseChildPayload(stdout: string): ChildSuccess | ChildFailure | null {
  const lines = stdout
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean);
  for (let i = lines.length - 1; i >= 0; i -= 1) {
    try {
      const parsed = JSON.parse(lines[i]!) as ChildSuccess | ChildFailure;
      if (typeof parsed === "object" && parsed !== null && "ok" in parsed) {
        return parsed;
      }
    } catch {
      /* try previous line */
    }
  }
  return null;
}

/** 在独立 Node 子进程运行 RPA 地址候选，规避 Next.js 打包 Playwright */
export async function spawnFetchMothershipAddressCandidates(input: {
  pickup: AddressLookupInput;
  delivery: AddressLookupInput;
}): Promise<MothershipCandidatesResult> {
  const { command, args, shell } = resolveTsxCommand();
  const timeoutMs = ADDRESS_CANDIDATES_TIMEOUT_MS + CHILD_STARTUP_BUFFER_MS;

  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd: process.cwd(),
      env: { ...process.env, ...readInfraEnvOverrides() },
      stdio: ["pipe", "pipe", "pipe"],
      shell,
    });

    let stdout = "";
    let stderr = "";
    let settled = false;

    const finish = (fn: () => void) => {
      if (settled) {
        return;
      }
      settled = true;
      clearTimeout(timer);
      fn();
    };

    const timer = setTimeout(() => {
      child.kill("SIGTERM");
      finish(() => {
        reject(
          new RpaError(
            "PAGE_LOAD_TIMEOUT",
            "MotherShip 地址联想超时，请检查网络或稍后重试",
            { retryable: true },
          ),
        );
      });
    }, timeoutMs);

    child.stdout.on("data", (chunk: Buffer | string) => {
      stdout += chunk.toString();
    });
    child.stderr.on("data", (chunk: Buffer | string) => {
      stderr += chunk.toString();
    });

    child.on("error", (error) => {
      finish(() => reject(error));
    });

    child.on("close", (code) => {
      const payload = parseChildPayload(stdout);
      if (payload?.ok) {
        finish(() => resolve(payload.data));
        return;
      }

      if (payload && !payload.ok) {
        finish(() => {
          reject(
            new RpaError(payload.code as RpaErrorCode, payload.message, {
              retryable: payload.retryable ?? false,
            }),
          );
        });
        return;
      }

      const detail = stderr.trim() || stdout.trim() || `exit code ${code ?? "unknown"}`;
      finish(() => {
        if (code === 0) {
          reject(new Error(`地址候选子进程输出无效：${detail.slice(0, 240)}`));
          return;
        }
        reject(new Error(`地址候选 RPA 子进程失败：${detail.slice(0, 240)}`));
      });
    });

    child.stdin.write(JSON.stringify(input));
    child.stdin.end();
  });
}
