import { commandError, errorPayload, githubPagesTextRequest, isGitHubError, validationError } from "./client";
import { isRecord } from "./notify-claudeqq";

const BENCHMARK_EXPORT_MAX_BYTES = 8 * 1024 * 1024;

interface BenchmarkMeasurement {
  name: string;
  value: number;
  unit: string;
  extra: string | null;
}

interface BenchmarkEntry {
  suite: string;
  commit: {
    id: string;
    message: string;
    timestamp: string | null;
    url: string | null;
  };
  date: number;
  tool: string;
  benches: BenchmarkMeasurement[];
}

function splitRepo(repo: string): [string, string] | null {
  const [owner, name, extra] = repo.split("/");
  return owner && name && extra === undefined ? [owner, name] : null;
}

export function benchmarkDataUrl(repo: string, input?: string): string {
  const parts = splitRepo(repo);
  if (parts === null) throw new Error("--repo must use owner/name format");
  const [owner, name] = parts;
  const page = new URL(input ?? `https://${owner}.github.io/${name}/dev/bench/`);
  if (page.protocol !== "https:" || page.username !== "" || page.password !== "") {
    throw new Error("--url must be an HTTPS GitHub Pages URL without embedded credentials");
  }
  if (page.hostname.toLowerCase() !== `${owner.toLowerCase()}.github.io`) {
    throw new Error(`--url host must be ${owner.toLowerCase()}.github.io for repo ${repo}`);
  }
  const repoPrefix = `/${name.toLowerCase()}/`;
  if (!page.pathname.toLowerCase().startsWith(repoPrefix)) {
    throw new Error(`--url path must start with /${name}/ for repo ${repo}`);
  }
  page.search = "";
  page.hash = "";
  if (page.pathname.endsWith("/data.js")) return page.toString();
  if (!page.pathname.endsWith("/")) page.pathname += "/";
  return new URL("data.js", page).toString();
}

function text(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}

function parseMeasurement(value: unknown): BenchmarkMeasurement | null {
  if (!isRecord(value)) return null;
  const name = text(value.name);
  const unit = text(value.unit);
  if (name === null || unit === null || typeof value.value !== "number" || !Number.isFinite(value.value)) return null;
  return { name, value: value.value, unit, extra: text(value.extra) };
}

function parseEntry(suite: string, value: unknown): BenchmarkEntry | null {
  if (!isRecord(value) || !isRecord(value.commit) || !Array.isArray(value.benches)) return null;
  const id = text(value.commit.id);
  const message = text(value.commit.message);
  if (id === null || message === null || typeof value.date !== "number" || !Number.isFinite(value.date)) return null;
  const benches = value.benches.map(parseMeasurement).filter((item): item is BenchmarkMeasurement => item !== null);
  if (benches.length === 0) return null;
  return {
    suite,
    commit: {
      id,
      message,
      timestamp: text(value.commit.timestamp),
      url: text(value.commit.url),
    },
    date: value.date,
    tool: text(value.tool) ?? "unknown",
    benches,
  };
}

export function parseBenchmarkData(textValue: string): {
  lastUpdate: number | null;
  repoUrl: string | null;
  entries: BenchmarkEntry[];
  suites: string[];
} {
  const prefix = /^\s*window\.BENCHMARK_DATA\s*=\s*/u;
  if (!prefix.test(textValue)) throw new Error("benchmark export must assign window.BENCHMARK_DATA");
  const json = textValue.replace(prefix, "").replace(/;?\s*$/u, "");
  const parsed: unknown = JSON.parse(json);
  if (!isRecord(parsed) || !isRecord(parsed.entries)) throw new Error("benchmark export is missing entries");
  const suites = Object.keys(parsed.entries);
  const entries = suites.flatMap((suite) => {
    const values = parsed.entries[suite];
    return Array.isArray(values)
      ? values.map((value) => parseEntry(suite, value)).filter((entry): entry is BenchmarkEntry => entry !== null)
      : [];
  }).sort((left, right) => left.date - right.date);
  if (entries.length === 0) throw new Error("benchmark export contains no valid entries");
  return {
    lastUpdate: typeof parsed.lastUpdate === "number" && Number.isFinite(parsed.lastUpdate) ? parsed.lastUpdate : null,
    repoUrl: text(parsed.repoUrl),
    entries,
    suites,
  };
}

export async function actionBenchmark(
  repo: string,
  options: { url?: string; commit?: string; limit: number },
): Promise<Record<string, unknown>> {
  let dataUrl: string;
  try {
    dataUrl = benchmarkDataUrl(repo, options.url);
  } catch (error) {
    return validationError("run benchmark", repo, error instanceof Error ? error.message : String(error));
  }
  const fetched = await githubPagesTextRequest(dataUrl, BENCHMARK_EXPORT_MAX_BYTES);
  if (isGitHubError(fetched)) return commandError("run benchmark", repo, fetched, { mutation: false, dataUrl });
  let parsed: ReturnType<typeof parseBenchmarkData>;
  try {
    parsed = parseBenchmarkData(fetched.text);
  } catch (error) {
    return commandError("run benchmark", repo, errorPayload("invalid-response", error instanceof Error ? error.message : String(error), {
      request: { method: "GET", url: dataUrl },
      details: { bytes: fetched.bytes, contentType: fetched.contentType },
    }), { mutation: false, dataUrl });
  }
  const commit = options.commit?.toLowerCase();
  const matching = commit === undefined
    ? parsed.entries
    : parsed.entries.filter((entry) => entry.commit.id.toLowerCase().startsWith(commit));
  if (matching.length === 0) {
    return validationError("run benchmark", repo, `no benchmark entry matched commit ${options.commit}`, {
      dataUrl,
      totalEntries: parsed.entries.length,
    });
  }
  const selectedEntries = matching.slice(-options.limit).reverse();
  const entries = selectedEntries.map((entry) => ({
    suite: entry.suite,
    commit: entry.commit,
    date: entry.date,
    tool: entry.tool,
    measurementCount: entry.benches.length,
  }));
  const measurements = selectedEntries.flatMap((entry) => entry.benches.map((bench) => ({
    suite: entry.suite,
    commit: entry.commit.id,
    date: entry.date,
    name: bench.name,
    value: bench.value,
    unit: bench.unit,
  })));
  return {
    ok: true,
    command: "run benchmark",
    repo,
    mutation: false,
    source: {
      pageUrl: options.url ?? dataUrl.replace(/data\.js$/u, ""),
      dataUrl,
      bytes: fetched.bytes,
      contentType: fetched.contentType,
      lastUpdate: parsed.lastUpdate,
      repoUrl: parsed.repoUrl,
    },
    suites: parsed.suites,
    totalEntries: parsed.entries.length,
    matchingEntries: matching.length,
    returnedEntries: selectedEntries.length,
    limit: options.limit,
    commitFilter: options.commit ?? null,
    entries,
    measurements,
    next: {
      commit: `bun scripts/cli.ts gh run benchmark --repo ${repo} --url ${options.url ?? dataUrl.replace(/data\.js$/u, "")} --commit <sha> --raw`,
    },
  };
}
