import { defineCommand } from "citty";
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { Example } from "./_examples.js";
import { c } from "../ui/colors.js";
import { resolveProject } from "../utils/project.js";
import { serveStaticProjectHtml } from "../utils/staticProjectServer.js";
import { withMeta } from "../utils/updateCheck.js";
import {
  buildLayoutSampleTimes,
  collapseStaticLayoutIssues,
  dedupeLayoutIssues,
  formatLayoutIssue,
  limitLayoutIssues,
  summarizeLayoutIssues,
  type LayoutIssue,
} from "../utils/layoutAudit.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SEEK_SETTLE_MS = 120;
const INSPECT_SCHEMA_VERSION = 1;

export const examples: Example[] = [
  ["Inspect visual layout across the current composition", "hyperframes layout"],
  ["Inspect a specific project", "hyperframes layout ./my-video"],
  ["Output agent-readable JSON", "hyperframes layout --json"],
  ["Use explicit hero-frame timestamps", "hyperframes layout --at 1.5,4.0,7.25"],
];

interface LayoutAuditResult {
  duration: number;
  samples: number[];
  rawIssues: LayoutIssue[];
}

async function getCompositionDuration(page: import("puppeteer-core").Page): Promise<number> {
  return page.evaluate(() => {
    const win = window as unknown as {
      __hf?: { duration?: number };
      __player?: { duration?: number | (() => number) };
      __timelines?: Record<string, { duration?: number | (() => number) }>;
    };
    if (typeof win.__hf?.duration === "number" && win.__hf.duration > 0) return win.__hf.duration;
    const playerDuration = win.__player?.duration;
    if (typeof playerDuration === "function") return playerDuration();
    if (typeof playerDuration === "number" && playerDuration > 0) return playerDuration;

    const root = document.querySelector("[data-composition-id][data-duration]");
    const attrDuration = root ? parseFloat(root.getAttribute("data-duration") ?? "0") : 0;
    if (attrDuration > 0) return attrDuration;

    const timelines = win.__timelines;
    if (timelines) {
      for (const timeline of Object.values(timelines)) {
        const duration = timeline.duration;
        if (typeof duration === "function") return duration();
        if (typeof duration === "number" && duration > 0) return duration;
      }
    }

    return 0;
  });
}

async function seekTo(page: import("puppeteer-core").Page, time: number): Promise<void> {
  await page.evaluate((t: number) => {
    const win = window as unknown as {
      __hf?: { seek?: (time: number) => void };
      __player?: { seek?: (time: number) => void };
      __timelines?: Record<string, { pause?: () => void; seek?: (time: number) => void }>;
    };
    if (typeof win.__hf?.seek === "function") {
      win.__hf.seek(t);
      return;
    }
    if (typeof win.__player?.seek === "function") {
      win.__player.seek(t);
      return;
    }
    const timelines = win.__timelines;
    if (timelines) {
      for (const timeline of Object.values(timelines)) {
        if (typeof timeline.pause === "function") timeline.pause();
        if (typeof timeline.seek === "function") timeline.seek(t);
      }
    }
  }, time);
  await page.evaluate(
    () =>
      new Promise<void>((resolveFrame) =>
        requestAnimationFrame(() => requestAnimationFrame(() => resolveFrame())),
      ),
  );
  await page
    .evaluate(() => {
      const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
      if (!fonts?.ready) return Promise.resolve();
      return Promise.race([
        fonts.ready.then(() => undefined),
        new Promise<void>((resolve) => setTimeout(resolve, 500)),
      ]);
    })
    .catch(() => {});
  await new Promise((resolveSettle) => setTimeout(resolveSettle, SEEK_SETTLE_MS));
}

async function bundleProjectHtml(projectDir: string): Promise<string> {
  // `bundleToSingleHtml` now inlines the runtime IIFE by default, so the
  // previous post-bundle runtime substitution is no longer needed.
  const { bundleToSingleHtml } = await import("@hyperframes/core/compiler");
  return bundleToSingleHtml(projectDir);
}

async function alignViewportToComposition(
  page: import("puppeteer-core").Page,
  url: string,
): Promise<void> {
  const size = await page.evaluate(() => {
    const root = document.querySelector("[data-composition-id][data-width][data-height]");
    const width = root ? parseInt(root.getAttribute("data-width") ?? "", 10) : 0;
    const height = root ? parseInt(root.getAttribute("data-height") ?? "", 10) : 0;
    return {
      width: Number.isFinite(width) && width > 0 ? Math.min(width, 4096) : 1920,
      height: Number.isFinite(height) && height > 0 ? Math.min(height, 4096) : 1080,
    };
  });

  await page.setViewport(size);
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
}

async function runLayoutAudit(
  projectDir: string,
  opts: { samples: number; at?: number[]; timeout: number; tolerance: number },
): Promise<LayoutAuditResult> {
  const { ensureBrowser } = await import("../browser/manager.js");
  const puppeteer = await import("puppeteer-core");
  const html = await bundleProjectHtml(projectDir);
  const server = await serveStaticProjectHtml(
    projectDir,
    html,
    "Failed to bind local layout audit server",
  );
  let chromeBrowser: import("puppeteer-core").Browser | undefined;

  try {
    const browser = await ensureBrowser();
    chromeBrowser = await puppeteer.default.launch({
      headless: true,
      executablePath: browser.executablePath,
      args: [
        "--no-sandbox",
        "--disable-gpu",
        "--disable-dev-shm-usage",
        "--enable-webgl",
        "--use-gl=angle",
        "--use-angle=swiftshader",
      ],
    });

    const page = await chromeBrowser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });
    await page.goto(server.url, { waitUntil: "domcontentloaded", timeout: 10000 });
    await alignViewportToComposition(page, server.url);
    await page
      .waitForFunction(() => !!(window as unknown as { __timelines?: unknown }).__timelines, {
        timeout: opts.timeout,
      })
      .catch(() => {});
    await page
      .evaluate(() => {
        const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
        if (!fonts?.ready) return Promise.resolve();
        return Promise.race([
          fonts.ready.then(() => undefined),
          new Promise<void>((resolve) => setTimeout(resolve, 750)),
        ]);
      })
      .catch(() => {});
    await new Promise((resolveSettle) => setTimeout(resolveSettle, 250));

    const duration = await getCompositionDuration(page);
    const samples = buildLayoutSampleTimes({ duration, samples: opts.samples, at: opts.at });
    if (samples.length === 0) return { duration, samples, rawIssues: [] };

    await page.addScriptTag({ content: loadLayoutAuditScript() });

    const issues: LayoutIssue[] = [];
    for (const time of samples) {
      await seekTo(page, time);
      const sampleIssues = await page.evaluate(
        (auditOptions: { time: number; tolerance: number }) => {
          const win = window as unknown as {
            __hyperframesLayoutAudit?: (options: { time: number; tolerance: number }) => unknown[];
          };
          return win.__hyperframesLayoutAudit?.(auditOptions) ?? [];
        },
        { time, tolerance: opts.tolerance },
      );
      issues.push(...(sampleIssues as LayoutIssue[]));
    }

    return {
      duration,
      samples,
      rawIssues: dedupeLayoutIssues(issues),
    };
  } finally {
    await chromeBrowser?.close().catch(() => {});
    await server.close();
  }
}

function loadLayoutAuditScript(): string {
  const candidates = [
    join(__dirname, "layout-audit.browser.js"),
    join(__dirname, "commands", "layout-audit.browser.js"),
  ];

  for (const candidate of candidates) {
    if (existsSync(candidate)) return readFileSync(candidate, "utf-8");
  }

  throw new Error("Missing layout audit browser script");
}

function parseAt(value: unknown): number[] | undefined {
  if (!value) return undefined;
  const times = String(value)
    .split(",")
    .map((entry) => parseFloat(entry.trim()))
    .filter((time) => Number.isFinite(time) && time >= 0);
  return times.length > 0 ? times : undefined;
}

export function createInspectCommand(commandName: "inspect" | "layout") {
  return defineCommand({
    meta: {
      name: commandName,
      description: "Inspect rendered composition layout for text and container overflow",
    },
    args: {
      dir: { type: "positional", description: "Project directory", required: false },
      json: { type: "boolean", description: "Output agent-readable JSON", default: false },
      samples: {
        type: "string",
        description: "Number of midpoint samples across the duration (default: 9)",
        default: "9",
      },
      at: {
        type: "string",
        description: "Comma-separated timestamps in seconds (e.g., --at 1.5,4,7.25)",
      },
      tolerance: {
        type: "string",
        description: "Allowed pixel overflow before reporting an issue (default: 2)",
        default: "2",
      },
      timeout: {
        type: "string",
        description: "Ms to wait for runtime to initialize (default: 5000)",
        default: "5000",
      },
      "max-issues": {
        type: "string",
        description: "Maximum issues to print or return after static collapse (default: 80)",
        default: "80",
      },
      "collapse-static": {
        type: "boolean",
        description: "Collapse repeated static issues across samples (default: true)",
        default: true,
      },
      strict: {
        type: "boolean",
        description: "Exit non-zero on warnings too",
        default: false,
      },
    },
    async run({ args }) {
      const project = resolveProject(args.dir);
      const samples = Math.max(1, parseInt(args.samples as string, 10) || 9);
      const tolerance = Math.max(0, parseFloat(args.tolerance as string) || 2);
      const timeout = Math.max(500, parseInt(args.timeout as string, 10) || 5000);
      const maxIssues = Math.max(1, parseInt(args["max-issues"] as string, 10) || 80);
      const at = parseAt(args.at);
      const strict = !!args.strict;
      const collapseStatic = args["collapse-static"] !== false;

      if (!args.json) {
        const sampleLabel = at
          ? `${at.length} explicit timestamp(s)`
          : `${samples} timeline samples`;
        console.log(
          `${c.accent("◆")}  Inspecting layout for ${c.accent(project.name)} (${sampleLabel})`,
        );
      }

      try {
        const result = await runLayoutAudit(project.dir, {
          samples,
          at,
          timeout,
          tolerance,
        });
        const allIssues = collapseStatic
          ? collapseStaticLayoutIssues(result.rawIssues)
          : result.rawIssues;
        const limited = limitLayoutIssues(allIssues, maxIssues);
        const summary = summarizeLayoutIssues(allIssues);
        const ok = summary.errorCount === 0 && (!strict || summary.warningCount === 0);

        if (args.json) {
          console.log(
            JSON.stringify(
              withMeta({
                schemaVersion: INSPECT_SCHEMA_VERSION,
                duration: result.duration,
                samples: result.samples,
                tolerance,
                strict,
                collapseStatic,
                ...summary,
                totalIssueCount: limited.totalIssueCount,
                truncated: limited.truncated,
                ok,
                issues: limited.issues,
              }),
              null,
              2,
            ),
          );
          process.exit(ok ? 0 : 1);
        }

        if (result.samples.length === 0) {
          console.log();
          console.log(
            `${c.error("✗")} Could not determine composition duration — no layout samples run`,
          );
          process.exit(1);
        }

        console.log();
        if (limited.issues.length === 0) {
          console.log(
            `${c.success("◇")}  0 layout issues across ${result.samples.length} sample(s)`,
          );
          return;
        }

        for (const issue of limited.issues) {
          const icon =
            issue.severity === "error"
              ? c.error("✗")
              : issue.severity === "warning"
                ? c.warn("⚠")
                : c.dim("ℹ");
          const formatted = formatLayoutIssue(issue).replace(/\n/g, "\n    ");
          console.log(`  ${icon} ${c.dim(formatted)}`);
        }

        console.log();
        const parts = [
          `${summary.errorCount} error(s)`,
          `${summary.warningCount} warning(s)`,
          `${summary.infoCount} info(s)`,
        ];
        const suffix = limited.truncated ? c.dim(`, truncated at ${maxIssues} issue(s)`) : "";
        console.log(`${ok ? c.success("◇") : c.error("◇")}  ${parts.join(", ")}${suffix}`);

        process.exit(ok ? 0 : 1);
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (args.json) {
          console.log(
            JSON.stringify(
              withMeta({
                schemaVersion: INSPECT_SCHEMA_VERSION,
                ok: false,
                error: message,
                issues: [],
                errorCount: 0,
                warningCount: 0,
                infoCount: 0,
                issueCount: 0,
              }),
              null,
              2,
            ),
          );
          process.exit(1);
        }
        console.error(`${c.error("✗")} Inspect failed: ${message}`);
        process.exit(1);
      }
    },
  });
}

export default createInspectCommand("layout");
