import { heartbeat } from "@temporalio/activity";
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";

import { buildCaseRun, collectAgentDiff, collectAgentTraceEvidence, collectCaseRun, prepareCaseRun, recoverAgentTaskStage, recoverBuildCaseRun, runAgentTaskStage } from "../../tools/src/hwlab-caserun-lib.ts";
import type { HarnessRLRegistry } from "./contracts.ts";
import { caseRunInternalFetch } from "../cloud/server-caserun-http.ts";

export function createHarnessRLActivities(options: {
  registry: HarnessRLRegistry;
  env?: Record<string, string | undefined>;
  stages?: Partial<{
    prepare(context: any, action: string): Promise<any>;
    agent(context: any, run: any): Promise<any>;
    recoverAgent(context: any, run: any): Promise<any>;
    trace(context: any, run: any): Promise<any>;
    diff(context: any, run: any): Promise<any>;
    build(context: any, run: any): Promise<any>;
    recoverBuild(context: any, run: any): Promise<any>;
    collect(context: any, run: any, evidence: any): Promise<any>;
  }>;
}) {
  const env = options.env ?? process.env;
  const registry = options.registry;
  const stages = {
    prepare: options.stages?.prepare ?? prepareCaseRun,
    agent: options.stages?.agent ?? runAgentTaskStage,
    recoverAgent: options.stages?.recoverAgent ?? recoverAgentTaskStage,
    trace: options.stages?.trace ?? collectAgentTraceEvidence,
    diff: options.stages?.diff ?? collectAgentDiff,
    build: options.stages?.build ?? buildCaseRun,
    recoverBuild: options.stages?.recoverBuild ?? recoverBuildCaseRun,
    collect: options.stages?.collect ?? collectCaseRun
  };
  return {
    async markRunning({ runId }: { runId: string }) {
      await transition(registry, runId, "running", "prepare", { worker: "hwlab-harnessrl-worker" });
    },
    async prepare(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const definition = await loadDefinition(record.caseRepo, record.caseId);
        activityHeartbeat({ runId: input.runId, stage: "prepare" });
        if (definition.mode === "software-smoke") {
          await mkdir(record.runDir, { recursive: true });
          const output = { mode: "software-smoke", run: { runId: record.runId, caseId: record.caseId, mode: "software-smoke", hardwareRequired: false } };
          await transition(registry, input.runId, "running", "prepared", { mode: "software-smoke" });
          return output;
        }
        const context = contextForRecord(record, env, input.identity);
        const prepared = await stages.prepare(context, "harnessrl-worker");
        const output = { mode: definition.mode, prepared } as Record<string, unknown>;
        await transition(registry, input.runId, "running", "prepared", { runDir: prepared.runDir, specPath: prepared.specPath });
        return output;
      });
    },
    async agent(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "prepare", `${input.runId}:prepare:v1`))) as any;
        if (prepared.mode === "software-smoke") return { mode: prepared.mode, run: prepared.run, authority: { agentRun: "not-applicable" } };
        const stage = await stages.agent(contextForRecord(record, env, input.identity), prepared.prepared.run);
        await transition(registry, input.runId, "running", "agent-completed", { traceId: stage.agent?.traceId ?? null, sessionId: stage.agent?.sessionId ?? null });
        return { mode: prepared.mode, run: stage.run, agent: stage.agent };
      }, async () => {
        const record = required(await registry.getRun(input.runId));
        const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "prepare", `${input.runId}:prepare:v1`))) as any;
        const stage = await stages.recoverAgent(contextForRecord(record, env, input.identity), prepared.prepared.run);
        await transition(registry, input.runId, "running", "agent-completed", { traceId: stage.agent?.traceId ?? null, sessionId: stage.agent?.sessionId ?? null, recovered: true });
        return { mode: prepared.mode, run: stage.run, agent: stage.agent };
      });
    },
    async trace(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const agent = activityOutput(required(await registry.getActivityResult(input.runId, "agent", `${input.runId}:agent:v1`))) as any;
        if (agent.mode === "software-smoke") return agent;
        const stage = await stages.trace(contextForRecord(record, env, input.identity), agent.run);
        await transition(registry, input.runId, "running", "trace-completed", { traceId: stage.trace?.traceId ?? null });
        return { ...agent, run: stage.run, trace: stage.trace };
      });
    },
    async diff(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const traced = activityOutput(required(await registry.getActivityResult(input.runId, "trace", `${input.runId}:trace:v1`))) as any;
        if (traced.mode === "software-smoke") return traced;
        const stage = await stages.diff(contextForRecord(record, env, input.identity), traced.run);
        await transition(registry, input.runId, "running", "diff-completed", { diffSha256: stage.diff?.diffPatchSha256 ?? null });
        return { ...traced, run: stage.run, diff: stage.diff };
      });
    },
    async build(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "diff", `${input.runId}:diff:v1`))) as any;
        activityHeartbeat({ runId: input.runId, stage: "build" });
        if (prepared.mode === "software-smoke") {
          const artifact = {
            contractVersion: "hwlab-caserun-software-smoke-v1", caseId: record.caseId, runId: record.runId,
            mode: "software-smoke", status: "completed", hardwareRequired: false,
            checks: { caseDefinitionLoaded: true, runDirectoryWritable: true, temporalWorkerRunning: true }
          };
          const text = `${JSON.stringify(artifact, null, 2)}\n`;
          await writeFile(path.join(record.runDir, "software-smoke.json"), text, "utf8");
          const artifactRef = { path: "software-smoke.json", bytes: Buffer.byteLength(text), sha256: createHash("sha256").update(text).digest("hex") };
          const output = { mode: "software-smoke", run: prepared.run, summary: { status: "completed", mode: "software-smoke", artifactCount: 1, artifacts: [artifactRef] }, evidence: { status: "recorded", mode: "software-smoke", artifacts: [artifactRef] } };
          await transition(registry, input.runId, "running", "build-completed", { artifactSha256: artifactRef.sha256 });
          return output;
        }
        const context = contextForRecord(record, env, input.identity);
        const build = await stages.build(context, prepared.run);
        await transition(registry, input.runId, "running", "build-completed", { jobId: build.summary?.jobId ?? build.evidence?.keilJob?.jobId ?? null });
        return { mode: prepared.mode, run: build.run ?? prepared.run, build, agent: prepared.agent, trace: prepared.trace, diff: prepared.diff };
      }, async () => {
        const record = required(await registry.getRun(input.runId));
        const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "diff", `${input.runId}:diff:v1`))) as any;
        const build = await stages.recoverBuild(contextForRecord(record, env, input.identity), prepared.run);
        await transition(registry, input.runId, "running", "build-completed", { jobId: build.summary?.jobId ?? build.evidence?.keilJob?.jobId ?? null, recovered: true });
        return { mode: prepared.mode, run: build.run ?? prepared.run, build, agent: prepared.agent, trace: prepared.trace, diff: prepared.diff };
      });
    },
    async collect(input: { runId: string; identity: string }) {
      return idempotent(registry, input, async () => {
        const record = required(await registry.getRun(input.runId));
        const built = activityOutput(required(await registry.getActivityResult(input.runId, "build", `${input.runId}:build:v1`))) as any;
        activityHeartbeat({ runId: input.runId, stage: "collect" });
        let result: Record<string, unknown>;
        if (built.mode === "software-smoke") result = { summary: built.summary, evidence: built.evidence, run: built.run };
        else {
          const context = contextForRecord(record, env, input.identity);
          const collected = await stages.collect(context, built.run, built.build.evidence);
          const manifest = await manifestRefs(collected.summary);
          result = {
            summary: built.build.summary, evidence: built.build.evidence, run: collected.run ?? built.run,
            authority: { agentRun: authorityRefs(built), hwpod: built.build.evidence?.operation ?? built.build.evidence?.validation ?? null },
            artifactManifestPath: manifest.path, artifactManifestSha256: manifest.sha256,
            aggregate: manifest.aggregate,
            collected: { status: collected.status, summary: collected.summary ?? null }
          };
        }
        await registry.updateRun(input.runId, { result });
        await transition(registry, input.runId, "running", "collect-completed", {});
        return result;
      });
    },
    async markCompleted({ runId }: { runId: string }) { await transition(registry, runId, "completed", "completed", {}); },
    async markCanceled({ runId }: { runId: string }) { await transition(registry, runId, "canceled", "canceled", {}); },
    async markFailed(input: { runId: string; code: string; message: string }) {
      await registry.updateRun(input.runId, { status: "failed", stage: "failed", error: { code: input.code, message: input.message } });
      await registry.appendEvent(input.runId, "failed", "failed", { code: input.code, message: input.message.slice(0, 500) });
    }
  };
}

async function idempotent(registry: HarnessRLRegistry, input: { runId: string; identity: string }, execute: () => Promise<Record<string, unknown>>, recover?: () => Promise<Record<string, unknown>>) {
  const activityName = input.identity.split(":").at(-2) ?? "activity";
  const existing = await registry.getActivityResult(input.runId, activityName, input.identity);
  if ((existing?.output as any)?.state === "completed") return (existing!.output as any).result;
  if (["started", "accepted"].includes((existing?.output as any)?.state) && recover) {
    let output: Record<string, unknown>;
    try {
      output = await recover();
    } catch (error: any) {
      if (!authorityNotFound(error)) throw error;
      output = await execute();
    }
    const saved = await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "completed", authorityIdentity: input.identity, result: output } });
    return activityOutput(saved);
  }
  if (!existing) await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "started", authorityIdentity: input.identity } });
  const output = await execute();
  const saved = await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "completed", authorityIdentity: input.identity, result: output } });
  return activityOutput(saved);
}

async function transition(registry: HarnessRLRegistry, runId: string, status: any, stage: string, payload: Record<string, unknown>) {
  await registry.updateRun(runId, { status, stage });
  await registry.appendEvent(runId, status, stage, payload);
}

function contextForRecord(record: any, env: Record<string, string | undefined>, identity: string) {
  const apiUrl = String(env.HWLAB_CASERUN_INTERNAL_API_URL ?? env.HWLAB_RUNTIME_INTERNAL_API_URL ?? record.runtimeApiUrl).replace(/\/+$/u, "");
  const traceId = `trc_harnessrl_${createHash("sha256").update(identity).digest("hex").slice(0, 24)}`;
  return {
    parsed: { _: [], caseId: record.caseId, runId: record.runId, runDir: record.runDir, caseRepo: record.caseRepo, apiUrl, traceId, operationIdentity: identity, noCaseRepoRecord: false, stateDir: path.join(path.dirname(record.runDir), "cli") },
    env: { ...process.env, ...env, HWLAB_CASE_REPO: record.caseRepo, HWLAB_RUNTIME_API_URL: apiUrl, HWLAB_CASERUN_PUBLIC_RUNTIME_API_URL: record.runtimeApiUrl, HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_HWPOD_OPERATION_IDENTITY: identity },
    fetchImpl: caseRunInternalFetch(globalThis.fetch, apiUrl, env), cwd: record.caseRepo,
    now: () => new Date().toISOString(), sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), argv: [], rest: ["worker", record.caseId]
  };
}

function activityOutput(result: { output: Record<string, unknown> }) {
  return (result.output as any)?.state === "completed" ? (result.output as any).result : result.output;
}

function authorityNotFound(error: any) {
  const code = String(error?.code ?? "");
  return code === "hwpod_operation_not_found" || code.endsWith("_not_found");
}

async function manifestRefs(summary: any) {
  const manifestPath = String(summary?.artifactManifestPath ?? "");
  if (!manifestPath) return { path: null, sha256: null, aggregate: null };
  const body = await readFile(manifestPath);
  const manifest = JSON.parse(body.toString("utf8"));
  return { path: manifestPath, sha256: createHash("sha256").update(body).digest("hex"), aggregate: manifest.aggregate ?? null };
}

function authorityRefs(built: any) {
  const agent = built.agent ?? built.run?.agent ?? {};
  return { conversationId: agent.conversationId ?? null, sessionId: agent.sessionId ?? null, threadId: agent.threadId ?? null, traceId: agent.traceId ?? null, resultUrl: agent.resultUrl ?? null, commandStatus: agent.commandStatus ?? null, final: agent.finalResponse ?? agent.result ?? null, diffSha256: built.diff?.diffPatchSha256 ?? built.run?.agentDiff?.diffPatchSha256 ?? null };
}

async function loadDefinition(caseRepo: string, caseId: string) {
  return JSON.parse(await readFile(path.join(caseRepo, "cases", caseId, "case.json"), "utf8"));
}
function required<T>(value: T | null): T { if (value === null) throw Object.assign(new Error("required registry record was not found"), { code: "registry_record_not_found" }); return value; }
function activityHeartbeat(details: Record<string, unknown>) { try { heartbeat(details); } catch {} }
