import { randomUUID } from "node:crypto";
import { Buffer } from "node:buffer";
import { mkdir, readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";

import type { CaseRunListItem, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, HarnessRLTemporalGateway } from "./contracts.ts";

const RUN_ID_PATTERN = /^[A-Za-z0-9_.:-]{3,180}$/u;
const CASE_RUN_STATUSES = new Set<CaseRunStatus>(["queued", "running", "completed", "failed", "blocked", "cancel_requested", "canceled"]);
const DEFAULT_RUN_LIST_LIMIT = 20;
const MAX_RUN_LIST_LIMIT = 100;

export function createHarnessRLService(options: { registry: HarnessRLRegistry; temporal: HarnessRLTemporalGateway; cwd: string; caseRepo: string; stateRoot: string }) {
  const { registry, temporal } = options;
  return {
    async health() { return { ok: true, registry: await registry.health() }; },
    async listCases() {
      const root = path.join(options.caseRepo, "cases");
      const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
      const cases = [];
      for (const entry of entries) if (entry.isDirectory()) {
        const item = await loadCase(options.caseRepo, entry.name).catch(() => null);
        if (item) cases.push(item);
      }
      return cases.sort((left, right) => left.caseId.localeCompare(right.caseId));
    },
    async listRuns(input: { status?: string; limit?: string; cursor?: string } = {}) {
      const status = parseStatus(input.status);
      const limit = parseLimit(input.limit);
      const records = await registry.listRuns({ status, limit: limit + 1, before: decodeCursor(input.cursor) });
      const hasMore = records.length > limit;
      const page = hasMore ? records.slice(0, limit) : records;
      const last = page.at(-1);
      return {
        ok: true,
        contractVersion: "hwlab-caserun-runs-v1",
        count: page.length,
        runs: page.map(runListItem),
        nextCursor: hasMore && last ? encodeCursor(last) : null
      };
    },
    async submit(input: { caseId: string; runId?: string; runtimeApiUrl: string }) {
      const caseId = opaqueId(input.caseId, "invalid_case_id");
      const definition = await loadCase(options.caseRepo, caseId).catch(() => null);
      if (!definition) throw codedError("case_not_found", "CaseRun case was not found", { caseId });
      const runId = input.runId ? opaqueId(input.runId, "invalid_run_id") : `${caseId}-${Date.now()}-${randomUUID().slice(0, 8)}`;
      const workflowId = `harnessrl-caserun-${runId}`;
      const runDir = path.join(options.stateRoot, "runs", runId);
      await mkdir(runDir, { recursive: true });
      const record = await registry.createRun({ runId, caseId, workflowId, caseRepo: options.caseRepo, runDir, runtimeApiUrl: input.runtimeApiUrl });
      if (!record.workflowRunId) {
        await registry.appendEvent(runId, "queued", "queued", { caseId });
        const started = await temporal.start({ workflowId, runId });
        await registry.updateRun(runId, { workflowRunId: started.workflowRunId });
        if (started.reused) await registry.appendEvent(runId, "queued", "workflow-reused", { workflowId, workflowRunId: started.workflowRunId });
      }
      return await registry.getRun(runId);
    },
    async getRun(runId: string) {
      const record = await registry.getRun(opaqueId(runId, "invalid_run_id"));
      if (!record) throw codedError("caserun_run_not_found", "CaseRun run was not found", { runId });
      return record;
    },
    async events(runId: string) {
      const record = await this.getRun(runId);
      return { ...record, events: await registry.listEvents(record.runId) };
    },
    async aggregate(runId: string) {
      const record = await this.getRun(runId);
      return { ok: true, contractVersion: "hwlab-caserun-aggregate-v1", runId: record.runId, caseId: record.caseId,
        status: record.status, stage: record.stage, terminal: record.terminal, result: record.result,
        sha256: record.result?.artifactManifestSha256 ?? null };
    },
    async cancel(runId: string) {
      const record = await this.getRun(runId);
      if (record.terminal) return record;
      await registry.updateRun(runId, { status: "cancel_requested", stage: "cancel_requested" });
      await registry.appendEvent(runId, "cancel_requested", "cancel_requested", {});
      await temporal.requestCancel(record.workflowId);
      return await registry.getRun(runId);
    },
    async close() { await temporal.close(); await registry.close(); }
  };
}

function runListItem(record: CaseRunRecord): CaseRunListItem {
  const summary = objectValue(record.result?.summary);
  const evidence = objectValue(record.result?.evidence);
  const artifacts = Array.isArray(summary?.artifacts) ? summary.artifacts : Array.isArray(evidence?.artifacts) ? evidence.artifacts : [];
  return {
    runId: record.runId,
    caseId: record.caseId,
    workflowId: record.workflowId,
    workflowRunId: record.workflowRunId,
    status: record.status,
    stage: record.stage,
    terminal: record.terminal,
    createdAt: record.createdAt,
    updatedAt: record.updatedAt,
    artifactCount: finiteNumber(summary?.artifactCount) ?? artifacts.length,
    evidenceStatus: stringValue(evidence?.status),
    artifactManifestSha256: stringValue(record.result?.artifactManifestSha256)
  };
}

function parseStatus(value?: string): CaseRunStatus | undefined {
  if (value === undefined || value === "") return undefined;
  if (!CASE_RUN_STATUSES.has(value as CaseRunStatus)) throw codedError("invalid_status", "status is invalid", { status: value });
  return value as CaseRunStatus;
}

function parseLimit(value?: string): number {
  if (value === undefined || value === "") return DEFAULT_RUN_LIST_LIMIT;
  const limit = Number(value);
  if (!Number.isInteger(limit) || limit < 1 || limit > MAX_RUN_LIST_LIMIT) {
    throw codedError("invalid_limit", `limit must be an integer between 1 and ${MAX_RUN_LIST_LIMIT}`);
  }
  return limit;
}

function encodeCursor(record: Pick<CaseRunRecord, "createdAt" | "runId">): string {
  return Buffer.from(JSON.stringify({ createdAt: record.createdAt, runId: record.runId }), "utf8").toString("base64url");
}

function decodeCursor(value?: string): { createdAt: string; runId: string } | undefined {
  if (value === undefined || value === "") return undefined;
  try {
    const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
    const createdAt = String(parsed?.createdAt ?? "");
    const runId = opaqueId(parsed?.runId, "invalid_cursor");
    if (!createdAt || Number.isNaN(Date.parse(createdAt))) throw new Error("createdAt is invalid");
    return { createdAt: new Date(createdAt).toISOString(), runId };
  } catch {
    throw codedError("invalid_cursor", "cursor is invalid");
  }
}

function objectValue(value: unknown): Record<string, any> | null {
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : null;
}

function stringValue(value: unknown): string | null {
  return typeof value === "string" && value ? value : null;
}

function finiteNumber(value: unknown): number | null {
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? number : null;
}

async function loadCase(caseRepo: string, caseId: string) {
  const caseDir = path.join(caseRepo, "cases", caseId);
  const definition = JSON.parse(await readFile(path.join(caseDir, "case.json"), "utf8"));
  const mode = String(definition.mode ?? "compile-only").trim() || "compile-only";
  const hardwareRequired = mode !== "software-smoke";
  const hwpodSpec = hardwareRequired ? String(definition.hwpodSpec ?? definition.specPath ?? "hwpod-spec.yaml").trim() || "hwpod-spec.yaml" : null;
  const available = hwpodSpec ? Boolean((await stat(path.resolve(caseDir, hwpodSpec)).catch(() => null))?.isFile()) : true;
  return { caseId: String(definition.caseId ?? caseId), title: String(definition.title ?? caseId), mode, hwpodSpec, hardwareRequired, available,
    subject: definition.subject ?? null, expected: definition.expected ?? null, runtime: definition.runtime ?? null };
}

function opaqueId(value: unknown, code: string) {
  const text = String(value ?? "").trim();
  if (!RUN_ID_PATTERN.test(text)) throw codedError(code, `${code === "invalid_case_id" ? "caseId" : "runId"} is invalid`);
  return text;
}
function codedError(code: string, message: string, details?: unknown) { return Object.assign(new Error(message), { code, details }); }
