import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

import {
  expect,
  type APIRequestContext,
  type Locator,
  type Page,
} from "@playwright/test";

import type {
  ArticleSample,
  ExportValidationResult,
  SampleResult,
} from "./types";

const exportFileNames = [
  "optimized.md",
  "optimized.docx",
  "qa_report.json",
] as const;

type ExportFileName = (typeof exportFileNames)[number];

interface RunSamplePageFlowOptions {
  page: Page;
  request: APIRequestContext;
  sample: ArticleSample;
  baseURL: string;
  apiAccessKey: string;
  reportDir: string;
  timeoutMs: number;
}

interface QaReportJson {
  overall_status?: "pass" | "warn" | "fail";
  checks?: Array<{ rule_id?: string; status?: "pass" | "warn" | "fail" }>;
}

export async function runSamplePageFlow({
  page,
  request,
  sample,
  baseURL,
  apiAccessKey,
  reportDir,
  timeoutMs,
}: RunSamplePageFlowOptions): Promise<SampleResult> {
  const startedAt = Date.now();
  const sampleDir = join(reportDir, "samples", sample.slug);
  const exportsDir = join(sampleDir, "exports");
  mkdirSync(exportsDir, { recursive: true });

  await page.goto(baseURL);
  await fillIfVisible(page, "访问密钥", apiAccessKey);
  const bodyField = await fillFirstAvailable(
    page,
    ["文章内容", "粘贴文章", "正文"],
    sample.input.body,
  );
  await fillIfVisible(page, "图片描述或图片链接", sample.input.image_lines);
  await page.getByLabel("目标平台").selectOption(sample.input.platform);
  await fillIfVisible(page, "用户要求", sample.input.user_instructions);
  await ensureSubmitEnabled(page, bodyField, sample.input.body);

  await page.getByRole("button", { name: "开始优化" }).click();

  const factCardPanel = page.locator("section").filter({ hasText: "事实卡" });
  await expect(factCardPanel.getByText(/可用|待复核/)).toBeVisible({
    timeout: timeoutMs,
  });
  await expect(page.getByText("优化结果")).toBeVisible({ timeout: 30_000 });
  await expect(page.getByText(/优化完成|质检发现需要复核的问题/)).toBeVisible({
    timeout: timeoutMs,
  });
  await expect(page.getByText("质量报告")).toBeVisible();

  for (const fileName of exportFileNames) {
    await expect(page.getByRole("link", { name: fileName })).toBeVisible();
  }

  const jobId = await extractJobIdFromExportLink(page);
  const exportResults = await validateExports({
    request,
    baseURL,
    apiAccessKey,
    jobId,
    exportsDir,
  });
  const qa = readQaReport(exportsDir);
  const finalScreenshot = join(sampleDir, "final.png");
  await page.screenshot({ path: finalScreenshot, fullPage: true });

  const failedExports = exportResults.filter((result) => result.status === "failed");

  return {
    file: sample.filePath,
    name: sample.name,
    slug: sample.slug,
    status: failedExports.length === 0 ? "passed" : "failed",
    duration_ms: Date.now() - startedAt,
    job_id: jobId,
    qa_status: qa.overall_status,
    qa_fail_rules: rulesWithStatus(qa, "fail"),
    qa_warn_rules: rulesWithStatus(qa, "warn"),
    expected_hard_failures: sample.expectedHardFailures,
    expected_warnings: sample.expectedWarnings,
    exports: Object.fromEntries(
      exportResults.map((result) => [result.fileName, result.status]),
    ),
    llm_tasks: [],
    failure_category: failedExports.length > 0 ? "export_failed" : undefined,
    failure_message:
      failedExports
        .map((result) => `${result.fileName}: ${result.error ?? result.statusCode}`)
        .join("; ") || undefined,
    artifacts: {
      final_screenshot: finalScreenshot,
    },
  };
}

async function fillFirstAvailable(page: Page, labels: string[], value: string) {
  for (const label of labels) {
    const locator = page.getByLabel(label);
    if ((await locator.count()) > 0) {
      await locator.fill(value);
      return locator;
    }
  }

  throw new Error(`none of these labels were found: ${labels.join(", ")}`);
}

async function ensureSubmitEnabled(
  page: Page,
  bodyField: Locator,
  body: string,
) {
  const submitButton = page.getByRole("button", { name: "开始优化" });
  for (let attempt = 0; attempt < 4; attempt += 1) {
    await bodyField.fill("");
    await bodyField.fill(body);
    if (await submitButton.isEnabled()) {
      return;
    }
    await page.waitForTimeout(250);
  }
  await expect(bodyField).toHaveValue(body);
  await expect(submitButton).toBeEnabled();
}

async function fillIfVisible(page: Page, label: string, value: string) {
  const locator = page.getByLabel(label);
  if ((await locator.count()) > 0) {
    await locator.fill(value);
  }
}

async function extractJobIdFromExportLink(page: Page) {
  const href = await page
    .getByRole("link", { name: "optimized.md" })
    .getAttribute("href");
  return extractJobIdFromExportHref(href ?? "");
}

export function extractJobIdFromExportHref(href: string) {
  const match = href.match(/\/api\/jobs\/([^/]+)\/exports\//);
  if (!match?.[1]) {
    throw new Error(`could not extract job id from export link: ${href}`);
  }
  return match[1];
}

async function validateExports({
  request,
  baseURL,
  apiAccessKey,
  jobId,
  exportsDir,
}: {
  request: APIRequestContext;
  baseURL: string;
  apiAccessKey: string;
  jobId: string;
  exportsDir: string;
}): Promise<ExportValidationResult[]> {
  const results: ExportValidationResult[] = [];
  const headers: Record<string, string> | undefined = apiAccessKey
    ? { "x-api-key": apiAccessKey }
    : undefined;

  for (const fileName of exportFileNames) {
    const response = await request.get(
      `${baseURL}/api/jobs/${jobId}/exports/${fileName}`,
      { headers },
    );
    const buffer = await response.body();
    const outputPath = join(exportsDir, fileName);
    writeFileSync(outputPath, buffer);

    if (!response.ok()) {
      results.push({
        fileName,
        status: "failed",
        statusCode: response.status(),
        error: response.statusText(),
      });
      continue;
    }

    const error = validateExportBody(fileName, buffer);
    results.push({
      fileName,
      status: error ? "failed" : "passed",
      statusCode: response.status(),
      error,
    });
  }

  return results;
}

export function validateExportBody(fileName: ExportFileName, buffer: Buffer) {
  if (buffer.length === 0) return "empty export body";

  if (fileName === "optimized.md") {
    return buffer.toString("utf8").trim().length > 0
      ? undefined
      : "optimized.md is blank";
  }

  if (fileName === "optimized.docx") {
    return buffer.subarray(0, 2).toString("utf8") === "PK"
      ? undefined
      : "optimized.docx does not look like a zip-based docx";
  }

  try {
    const parsed = JSON.parse(buffer.toString("utf8")) as QaReportJson;
    if (!parsed.overall_status || !Array.isArray(parsed.checks)) {
      return "qa_report.json must contain overall_status and checks";
    }
    return undefined;
  } catch {
    return "qa_report.json is not valid JSON";
  }
}

function readQaReport(exportsDir: string): QaReportJson {
  const text = readFileSync(join(exportsDir, "qa_report.json"), "utf8");
  return JSON.parse(text) as QaReportJson;
}

function rulesWithStatus(report: QaReportJson, status: "warn" | "fail") {
  return (report.checks ?? [])
    .filter((check) => check.status === status && check.rule_id)
    .map((check) => check.rule_id as string);
}
