import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";

import {
  cancelAgentRunChatTurn,
  describeAgentRunAdapterAvailability,
  submitAgentRunChatTurn,
} from "../cloud/code-agent-agentrun-adapter.ts";
import { createCodeAgentTraceStore } from "../cloud/code-agent-trace-store.ts";
import type { WorkbenchApplication, WorkbenchEventPublisher, WorkbenchTurnInput } from "./contracts.ts";

type NativeState = {
  sessions: Record<string, Record<string, any>>;
  turns: Record<string, Record<string, any>>;
};

export function createNativeAgentRunWorkbenchApplication(options: {
  stateFile: string;
  env: Record<string, string | undefined>;
  fetchImpl?: typeof fetch;
  now?: () => string;
  eventPublisher?: WorkbenchEventPublisher | null;
}): WorkbenchApplication {
  const env = options.env;
  const fetchImpl = options.fetchImpl ?? fetch;
  const now = options.now ?? (() => new Date().toISOString());
  const traceStore = createCodeAgentTraceStore();
  const eventPublisher = options.eventPublisher ?? null;
  const mutate = async <T>(fn: (state: NativeState) => T | Promise<T>) => {
    const state = await load(options.stateFile);
    const result = await fn(state);
    await mkdir(path.dirname(options.stateFile), { recursive: true });
    const temporary = `${options.stateFile}.${process.pid}.tmp`;
    await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, "utf8");
    await rename(temporary, options.stateFile);
    return result;
  };

  return {
    async snapshot() { return load(options.stateFile); },
    async health() {
      const state = await load(options.stateFile);
      const availability = describeAgentRunAdapterAvailability(env, { params: { providerProfile: env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE } });
      return {
        serviceId: "hwlab-workbench-native-agentrun",
        status: availability.ready ? "ready" : "blocked",
        mode: "agentrun-native",
        providerProfile: text(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE),
        agentRun: availability.agentRun,
        sessionCount: Object.keys(state.sessions).length,
        turnCount: Object.keys(state.turns).length,
        valuesPrinted: false,
      };
    },
    async createSession({ actor, params }) {
      return mutate((state) => {
        const sessionId = validOptionalId(params.sessionId, "ses_") ?? `ses_${randomUUID()}`;
        const conversationId = validOptionalId(params.conversationId, "cnv_") ?? `cnv_${randomUUID()}`;
        const timestamp = now();
        const session = {
          sessionId,
          conversationId,
          projectId: text(params.projectId) || "pikasTech/HWLAB",
          ownerUserId: actor.id,
          ownerRole: actor.role ?? "user",
          status: "idle",
          providerProfile: text(params.providerProfile) || text(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE) || "gpt.pika",
          createdAt: timestamp,
          updatedAt: timestamp,
          mode: "agentrun-native",
          messages: [] as Record<string, unknown>[],
        };
        state.sessions[sessionId] = session;
        return { status: "created", session, valuesRedacted: true, secretMaterialStored: false };
      });
    },
    async admitTurn(input) {
      const admitted = await mutate((state) => {
        const session = ownedSession(state, input.sessionId, input.actor.id);
        const timestamp = validTimestamp(input.params.submittedAt) ?? now();
        const message = requiredText(input.params.message, "message");
        state.turns[input.traceId] = {
          traceId: input.traceId,
          sessionId: input.sessionId,
          conversationId: session.conversationId,
          ownerUserId: input.actor.id,
          status: "admitted",
          message,
          createdAt: timestamp,
          updatedAt: timestamp,
          terminal: false,
        };
        session.messages.push({ messageId: `msg_user_${input.traceId.slice(4)}`, role: "user", content: message, traceId: input.traceId, status: "completed", createdAt: timestamp });
        session.status = "running";
        session.lastTraceId = input.traceId;
        session.updatedAt = timestamp;
        return { ...input, params: { ...input.params, message, submittedAt: timestamp, providerProfile: text(input.params.providerProfile) || session.providerProfile } };
      });
      await eventPublisher?.publish({
        traceId: admitted.traceId,
        sessionId: admitted.sessionId,
        event: {
          type: "backend_status",
          phase: "request-admitted",
          status: "running",
          summary: "请求已接纳，正在启动 AgentRun",
          message: "请求已接纳，正在启动 AgentRun",
          createdAt: admitted.params.submittedAt,
          terminal: false,
          valuesPrinted: false,
        },
      });
      return admitted;
    },
    async dispatchTurn(input: WorkbenchTurnInput) {
      const state = await load(options.stateFile);
      const session = ownedSession(state, input.sessionId, input.actor.id);
      const params = {
        ...input.params,
        sessionId: input.sessionId,
        conversationId: session.conversationId,
        projectId: session.projectId,
        ownerUserId: input.actor.id,
        ownerRole: input.actor.role ?? "user",
        providerProfile: text(input.params.providerProfile) || session.providerProfile,
        shortConnection: true,
      };
      const publishingTrace = publishingDispatchTraceStore(traceStore, eventPublisher, input);
      let admitted;
      try {
        admitted = await submitAgentRunChatTurn({ traceId: input.traceId, params, options: { env, fetchImpl }, traceStore: publishingTrace.store });
      } catch (error) {
        await publishingTrace.flush().catch(() => undefined);
        throw error;
      }
      await publishingTrace.flush().catch(() => undefined);
      await projectRunning(admitted, input.traceId);
      return {
        status: "running",
        terminal: false,
        traceId: input.traceId,
        sessionId: input.sessionId,
        agentRun: admitted.agentRun,
        terminalAuthority: "hwlab.event.v1",
        resultSynthesized: false,
        valuesPrinted: false,
      };
    },
    async close() { await eventPublisher?.close?.(); },
    async cancelTurn(input) {
      const state = await load(options.stateFile);
      const turn = state.turns[input.traceId];
      if (!turn || turn.ownerUserId !== input.actor.id) throw codedError("turn_not_found", "turn is not visible to the actor");
      const currentResult = turn.agentRun ? { status: "running", agentRun: turn.agentRun } : null;
      const canceled = await cancelAgentRunChatTurn({ traceId: input.traceId, currentResult, options: { env, fetchImpl }, traceStore });
      if (!canceled) throw codedError("turn_not_running", "turn has no active AgentRun command");
      return {
        status: "cancel-requested",
        terminal: false,
        traceId: input.traceId,
        agentRun: canceled.agentRun,
        terminalAuthority: "hwlab.event.v1",
        resultSynthesized: false,
        valuesPrinted: false,
      };
    },
  };

  async function projectRunning(result: any, traceId: string) {
    return mutate((state) => {
      const turn = state.turns[traceId];
      if (!turn) throw codedError("turn_not_admitted", "turn must be admitted before dispatch");
      Object.assign(turn, {
        status: "running",
        updatedAt: now(),
        runId: result.agentRun?.runId ?? null,
        commandId: result.agentRun?.commandId ?? null,
        runnerJobId: result.agentRun?.runnerJobId ?? null,
        agentRun: result.agentRun ?? null,
      });
    });
  }
}

function publishingDispatchTraceStore(base: ReturnType<typeof createCodeAgentTraceStore>, publisher: WorkbenchEventPublisher | null, input: WorkbenchTurnInput) {
  const pending: Promise<unknown>[] = [];
  const store = {
    ...base,
    append(traceId: string, event: Record<string, any> = {}, meta: Record<string, unknown> = {}) {
      const normalized = base.append(traceId, event, meta);
      if (publisher && dispatchVisibilityEvent(event)) {
        pending.push(publishDispatchVisibility(publisher, input, { ...event, createdAt: normalized?.createdAt ?? event.createdAt }));
      }
      return normalized;
    },
  };
  return { store, async flush() { await Promise.all(pending); } };
}

function dispatchVisibilityEvent(event: Record<string, any>): boolean {
  const label = text(event.label);
  const visible = label.startsWith("agentrun:dispatch-retry:")
    || label.startsWith("agentrun:dispatch-retry-exhausted:")
    || label.startsWith("agentrun:dispatch-failed:");
  return visible && (event.willRetry === true || event.terminal === true);
}

async function publishDispatchVisibility(publisher: WorkbenchEventPublisher, input: WorkbenchTurnInput, event: Record<string, any>) {
  const retrying = event.willRetry === true;
  const exhausted = event.terminal === true;
  const failureKind = text(event.failureKind || event.errorCode) || "agentrun-dispatch-error";
  const summary = dispatchFailureSummary(failureKind);
  await publisher.publish({
    traceId: input.traceId,
    sessionId: input.sessionId,
    event: {
      type: retrying ? "backend_status" : "error",
      phase: retrying ? "dispatch-retry-scheduled" : "dispatch-retry-exhausted",
      failureDomain: "infrastructure",
      component: "agentrun-manager",
      code: failureKind,
      failureKind,
      summary,
      message: summary,
      retryable: event.retryable === true,
      willRetry: retrying,
      retryPhase: retrying ? "retryScheduled" : "retryExhausted",
      attempt: event.retryAttempt,
      maxAttempts: event.retryMax,
      backoffMs: event.retryDelayMs,
      nextRetryAt: event.nextRetryAt,
      createdAt: event.createdAt,
      runId: event.runId ?? null,
      commandId: event.commandId ?? null,
      terminal: false,
      valuesPrinted: false,
    },
  });
  if (!exhausted) return;
  await publisher.publish({
    traceId: input.traceId,
    sessionId: input.sessionId,
    event: {
      type: "terminal_status",
      terminalStatus: "failed",
      status: "failed",
      failureKind,
      errorCode: failureKind,
      message: `${summary}，有限重试已耗尽。`,
      createdAt: event.createdAt,
      runId: event.runId ?? null,
      commandId: event.commandId ?? null,
      terminal: true,
      valuesPrinted: false,
    },
  });
}

function dispatchFailureSummary(failureKind: string): string {
  if (failureKind === "connection-refused" || failureKind === "agentrun-connect-failed") return "AgentRun 服务暂时不可达";
  if (failureKind.includes("timeout")) return "AgentRun 服务响应超时";
  return "AgentRun 调度基础设施暂时不可用";
}

async function load(stateFile: string): Promise<NativeState> {
  const parsed = JSON.parse(await readFile(stateFile, "utf8").catch(() => '{"sessions":{},"turns":{}}'));
  return { sessions: object(parsed.sessions), turns: object(parsed.turns) };
}
function object(value: unknown) { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, Record<string, any>> : {}; }
function text(value: unknown) { return String(value ?? "").trim(); }
function validTimestamp(value: unknown) { const timestamp = text(value); return timestamp && Number.isFinite(Date.parse(timestamp)) ? new Date(Date.parse(timestamp)).toISOString() : null; }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw codedError("invalid_input", `${field} is required`); return result; }
function validOptionalId(value: unknown, prefix: string) { const id = text(value); if (!id) return null; if (!id.startsWith(prefix)) throw codedError("invalid_input", `${prefix.slice(0, -1)} id is invalid`); return id; }
function ownedSession(state: NativeState, sessionId: string, ownerUserId: string) { const session = state.sessions[sessionId]; if (!session || session.ownerUserId !== ownerUserId) throw codedError("session_not_found", "session is not visible to the actor"); return session; }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
