import { nanoid } from "nanoid";
import type { RuntimeAdapter } from "./runtimeAdapter";
import type { RunRecord, TaskRecord, TraceEvent } from "../types/accountingAgent";
import { ApiError } from "../utils/http";

export class InMemoryRuntimeAdapter implements RuntimeAdapter {
  private runs: RunRecord[] = [];
  private tasks: TaskRecord[] = [];
  private traces: TraceEvent[] = [];
  private idempotencyCache = new Map<string, unknown>();

  private now(): string {
    return new Date().toISOString();
  }

  private cacheKey(action: string, key?: string): string | null {
    if (!key || !key.trim()) return null;
    return `${action}:${key.trim()}`;
  }

  private readIdempotency<T>(action: string, idempotencyKey?: string): T | null {
    const cache = this.cacheKey(action, idempotencyKey);
    if (!cache) return null;
    return (this.idempotencyCache.get(cache) as T | undefined) ?? null;
  }

  private writeIdempotency(action: string, idempotencyKey: string | undefined, value: unknown): void {
    const cache = this.cacheKey(action, idempotencyKey);
    if (!cache) return;
    this.idempotencyCache.set(cache, value);
  }

  private pushEvent(input: {
    runId: string;
    sessionId: string;
    taskId?: string | null;
    level?: "info" | "warn" | "error";
    eventType: string;
    payload?: Record<string, unknown>;
  }): void {
    this.traces.push({
      timestamp: this.now(),
      level: input.level ?? "info",
      service: "llm_normalizer_backend",
      sessionId: input.sessionId,
      runId: input.runId,
      taskId: input.taskId ?? null,
      eventType: input.eventType,
      payload: input.payload
    });
  }

  public startRun(input: {
    sessionId?: string;
    initiator?: string;
    source?: string;
    metadata?: Record<string, unknown>;
    idempotencyKey?: string;
  }): RunRecord {
    const cached = this.readIdempotency<RunRecord>("startRun", input.idempotencyKey);
    if (cached) return cached;

    const record: RunRecord = {
      sessionId: input.sessionId ?? `session_${nanoid(8)}`,
      runId: `run_${nanoid(10)}`,
      status: "RUNNING",
      initiator: input.initiator ?? "operator",
      source: input.source ?? "gui",
      createdAt: this.now(),
      updatedAt: this.now(),
      metadata: input.metadata ?? {}
    };
    this.runs.unshift(record);
    this.pushEvent({
      runId: record.runId,
      sessionId: record.sessionId,
      eventType: "RUN_STARTED",
      payload: record.metadata as Record<string, unknown>
    });
    this.writeIdempotency("startRun", input.idempotencyKey, record);
    return record;
  }

  public finishRun(input: {
    runId: string;
    status: "DONE" | "ERROR" | "CANCELLED";
    source?: string;
    reason?: string;
    metadata?: Record<string, unknown>;
    idempotencyKey?: string;
  }): RunRecord {
    const cached = this.readIdempotency<RunRecord>("finishRun", input.idempotencyKey);
    if (cached) return cached;

    const record = this.runs.find((item) => item.runId === input.runId);
    if (!record) {
      throw new ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
    }
    record.status = input.status;
    record.updatedAt = this.now();
    record.source = input.source ?? record.source;
    record.metadata = {
      ...(record.metadata ?? {}),
      ...(input.metadata ?? {}),
      reason: input.reason ?? null
    };
    this.pushEvent({
      runId: record.runId,
      sessionId: record.sessionId,
      eventType: `RUN_FINISHED_${input.status}`,
      level: input.status === "ERROR" ? "error" : "info",
      payload: { reason: input.reason ?? null }
    });
    this.writeIdempotency("finishRun", input.idempotencyKey, record);
    return record;
  }

  public listRuns(): RunRecord[] {
    return [...this.runs];
  }

  public getRun(runId: string): RunRecord | null {
    return this.runs.find((item) => item.runId === runId) ?? null;
  }

  public enqueueTask(input: {
    runId: string;
    payload: Record<string, unknown>;
    source?: string;
    idempotencyKey?: string;
  }): TaskRecord {
    const cached = this.readIdempotency<TaskRecord>("enqueueTask", input.idempotencyKey);
    if (cached) return cached;

    const run = this.getRun(input.runId);
    if (!run) {
      throw new ApiError("RUN_NOT_FOUND", `Run not found: ${input.runId}`, 404);
    }

    const task: TaskRecord = {
      taskId: `task_${nanoid(10)}`,
      runId: run.runId,
      status: "QUEUED",
      payload: input.payload,
      source: input.source ?? "gui",
      createdAt: this.now(),
      updatedAt: this.now()
    };
    this.tasks.unshift(task);
    this.pushEvent({
      runId: run.runId,
      sessionId: run.sessionId,
      taskId: task.taskId,
      eventType: "TASK_ENQUEUED",
      payload: task.payload
    });
    this.writeIdempotency("enqueueTask", input.idempotencyKey, task);
    return task;
  }

  public claimTask(): TaskRecord | null {
    const task = this.tasks.find((item) => item.status === "QUEUED");
    if (!task) {
      return null;
    }
    task.status = "RUNNING";
    task.updatedAt = this.now();
    const run = this.getRun(task.runId);
    if (run) {
      this.pushEvent({
        runId: run.runId,
        sessionId: run.sessionId,
        taskId: task.taskId,
        eventType: "TASK_CLAIMED"
      });
    }
    return task;
  }

  public completeTask(input: {
    taskId: string;
    result: Record<string, unknown>;
    source?: string;
    idempotencyKey?: string;
  }): TaskRecord {
    const cached = this.readIdempotency<TaskRecord>("completeTask", input.idempotencyKey);
    if (cached) return cached;

    const task = this.tasks.find((item) => item.taskId === input.taskId);
    if (!task) {
      throw new ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
    }
    task.status = "DONE";
    task.updatedAt = this.now();
    task.result = input.result;
    task.source = input.source ?? task.source;

    const run = this.getRun(task.runId);
    if (run) {
      this.pushEvent({
        runId: run.runId,
        sessionId: run.sessionId,
        taskId: task.taskId,
        eventType: "TASK_DONE",
        payload: input.result
      });
    }
    this.writeIdempotency("completeTask", input.idempotencyKey, task);
    return task;
  }

  public failTask(input: {
    taskId: string;
    error: { code: string; message: string; details?: unknown };
    source?: string;
    idempotencyKey?: string;
  }): TaskRecord {
    const cached = this.readIdempotency<TaskRecord>("failTask", input.idempotencyKey);
    if (cached) return cached;

    const task = this.tasks.find((item) => item.taskId === input.taskId);
    if (!task) {
      throw new ApiError("TASK_NOT_FOUND", `Task not found: ${input.taskId}`, 404);
    }
    task.status = "ERROR";
    task.updatedAt = this.now();
    task.error = input.error;
    task.source = input.source ?? task.source;

    const run = this.getRun(task.runId);
    if (run) {
      this.pushEvent({
        runId: run.runId,
        sessionId: run.sessionId,
        taskId: task.taskId,
        eventType: "TASK_ERROR",
        level: "error",
        payload: {
          errorCode: input.error.code,
          errorMessage: input.error.message
        }
      });
    }
    this.writeIdempotency("failTask", input.idempotencyKey, task);
    return task;
  }

  public getResults(): TaskRecord[] {
    return this.tasks.filter((item) => item.status === "DONE" || item.status === "ERROR");
  }

  public getRunTrace(runId: string): TraceEvent[] {
    return this.traces.filter((item) => item.runId === runId);
  }

  public health(): { ok: boolean; queueDepth: number; runsTotal: number } {
    return {
      ok: true,
      queueDepth: this.tasks.filter((item) => item.status === "QUEUED").length,
      runsTotal: this.runs.length
    };
  }
}
