import type { IncidentsReadiness } from "./bootstrap";
import type { KanboardRpcClient } from "./kanboard-client";
import { randomUUID } from "node:crypto";
import { IncidentStatusStore } from "./status-store";

export type IncidentTerminalState =
  | "resolved"
  | "needs-attention"
  | "rate-limited"
  | "timed-out"
  | "engine-down"
  | "interrupted"
  | "invalid-result";
export type IncidentStatusState = "running" | IncidentTerminalState;

export interface IncidentStatusUpdate {
  incidentId: string;
  taskId: number;
  dispatchId: string;
  revision: number;
  expectedRevision?: number;
  state: IncidentStatusState;
  at: string;
  startedAt?: string;
  heartbeatAt?: string;
  completedAt?: string;
  exitCode?: number;
  failureClass?: string;
  resultSummary?: string;
  resolutionArtifact?: string;
  resolutionSummary?: string;
  resolutionOperatorNote?: string;
  comment?: string;
}

export class IncidentStatusProjectionError extends Error {
  constructor(public readonly durableRevision: number, cause: unknown) {
    super(`status revision ${durableRevision} committed durably but projection failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
    this.name = "IncidentStatusProjectionError";
  }
}

export class IncidentStatusRevisionError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "IncidentStatusRevisionError";
  }
}

const STATUS_KEYS = [
  "overdeck.dispatch_id", "overdeck.dispatch_state", "overdeck.status_revision",
  "overdeck.started_at", "overdeck.heartbeat_at", "overdeck.completed_at",
  "overdeck.exit_code", "overdeck.failure_class", "overdeck.result_summary",
  "overdeck.resolution_artifact", "overdeck.resolution_summary", "overdeck.resolution_operator_note",
] as const;

function valuesFor(update: IncidentStatusUpdate): Record<string, string> {
  return {
    "overdeck.dispatch_id": update.dispatchId,
    "overdeck.dispatch_state": update.state,
    "overdeck.status_revision": String(update.revision),
    "overdeck.started_at": update.startedAt ?? "",
    "overdeck.heartbeat_at": update.heartbeatAt ?? "",
    "overdeck.completed_at": update.completedAt ?? "",
    "overdeck.exit_code": update.exitCode === undefined ? "" : String(update.exitCode),
    "overdeck.failure_class": update.failureClass ?? "",
    "overdeck.result_summary": update.resultSummary ?? "",
    "overdeck.resolution_artifact": update.resolutionArtifact ?? "",
    "overdeck.resolution_summary": update.resolutionSummary ?? "",
    "overdeck.resolution_operator_note": update.resolutionOperatorNote ?? "",
  };
}

function sameRevision(metadata: Record<string, string>, values: Record<string, string>): boolean {
  return STATUS_KEYS.every((key) => (metadata[key] ?? "") === values[key]);
}

function columnFor(state: IncidentStatusState, readiness: IncidentsReadiness): number {
  if (state === "running") return readiness.columnIds.Running;
  if (state === "resolved") return readiness.columnIds.Resolved;
  return readiness.columnIds["Needs attention"];
}

export interface IncidentStatusWriter {
  (update: IncidentStatusUpdate): Promise<void>;
  reconcile(): Promise<void>;
}

export function createIncidentStatusWriter(client: KanboardRpcClient, readiness: IncidentsReadiness, store: IncidentStatusStore, now = Date.now): IncidentStatusWriter {
  const recordStatus = async (update: IncidentStatusUpdate): Promise<void> => {
    if (!Number.isSafeInteger(update.revision) || update.revision < 1) {
      throw new IncidentStatusRevisionError("status revision must be a positive integer");
    }
    store.commit(update);
    const token = randomUUID();
    for (;;) {
      const receipt = store.claim(update.incidentId, token, now());
      if (!receipt) return;
      try {
        await project(receipt.update, receipt.operationId, token);
        store.projected(receipt.operationId, token, new Date().toISOString(), now());
      } catch (error) {
        store.release(receipt.operationId, token);
        throw new IncidentStatusProjectionError(update.revision, error);
      }
    }
  };

  recordStatus.reconcile = async () => {
    const failures: unknown[] = [];
    for (const incidentId of store.pendingIncidentIds()) {
      const token = randomUUID();
      for (;;) {
        const receipt = store.claim(incidentId, token, Date.now());
        if (!receipt) break;
        try {
          await project(receipt.update, receipt.operationId, token);
          store.projected(receipt.operationId, token, new Date().toISOString(), now());
        } catch (error) {
          store.release(receipt.operationId, token);
          failures.push(error);
          break;
        }
      }
    }
    if (failures.length > 0) throw new AggregateError(failures, "incident status reconciliation incomplete");
  };

  async function project(update: IncidentStatusUpdate, operationId: string, token: string): Promise<void> {
    const next = valuesFor(update);
    const expectedRevision = update.expectedRevision ?? update.revision - 1;
    const authoritative = async () => Promise.all([
      client.call("getTask", { task_id: update.taskId }),
      client.call("getTaskMetadata", { task_id: update.taskId }),
    ]);
    const validate = (task: Awaited<ReturnType<typeof client.call<"getTask">>>, metadata: Record<string, string>) => {
      if (task.id !== update.taskId || task.project_id !== readiness.projectId || metadata["overdeck.incident_id"] !== update.incidentId) {
        throw new IncidentStatusRevisionError("authoritative task does not match projection identity");
      }
      if (metadata["overdeck.dispatch_id"] !== update.dispatchId) {
        throw new IncidentStatusRevisionError("authoritative task belongs to a different dispatch");
      }
      const revision = Number(metadata["overdeck.status_revision"]);
      if (!Number.isSafeInteger(revision) || revision < 0) {
        throw new IncidentStatusRevisionError("authoritative task has an invalid status revision");
      }
      const state = metadata["overdeck.dispatch_state"];
      const terminal = state !== undefined && state !== "running" && state !== "starting";
      if (task.is_active === false && (update.state !== "resolved" || state !== "resolved")) {
        throw new IncidentStatusRevisionError("authoritative task is terminal in a conflicting state");
      }
      if (terminal && state !== update.state) {
        throw new IncidentStatusRevisionError(`authoritative task is terminal as ${state}`);
      }
      if (revision === update.revision) {
        if (!sameRevision(metadata, next)) throw new IncidentStatusRevisionError("authoritative status revision conflicts with projection");
        return "applied" as const;
      }
      if (revision !== expectedRevision) {
        throw new IncidentStatusRevisionError(`authoritative status revision ${revision} does not match expected ${expectedRevision}`);
      }
      if (terminal) throw new IncidentStatusRevisionError(`authoritative task is terminal as ${state}`);
      return "pending" as const;
    };
    const beforeMutation = async () => {
      const [task, metadata] = await authoritative();
      const status = validate(task, metadata);
      store.renew(operationId, token, now());
      return { task, metadata, status };
    };
    const mutate = async <T>(action: () => Promise<T>): Promise<T> => {
      const result = await action();
      store.renew(operationId, token, now());
      return result;
    };

    const initial = await beforeMutation();
    if (initial.status === "pending") {
      await mutate(() => client.call("saveTaskMetadata", { task_id: update.taskId, values: { ...initial.metadata, ...next } }));
    }

    const targetColumn = columnFor(update.state, readiness);
    if (update.state !== "running" || update.comment !== undefined) {
      const current = await beforeMutation();
      if (current.task.column_id !== targetColumn) {
        await mutate(() => client.call("moveTaskPosition", {
          project_id: readiness.projectId,
          task_id: update.taskId,
          column_id: targetColumn,
          position: 1,
          swimlane_id: current.task.swimlane_id,
        }));
      }
    }

    if (update.comment) {
      const reference = `overdeck:${update.dispatchId}:${update.revision}`;
      const comments = await client.call("getAllComments", { task_id: update.taskId });
      if (!comments.some((item) => item.comment.replace(/\r\n/g, "\n").split("\n", 1)[0] === reference)) {
        await beforeMutation();
        await mutate(() => client.call("createComment", {
          task_id: update.taskId,
          content: `${reference}\n${update.comment}`,
          user_id: readiness.agentUserId,
        }));
      }
    }

    if (update.state === "resolved") {
      const current = await beforeMutation();
      if (current.task.is_active !== false) {
        const closed = await mutate(() => client.call("closeTask", { task_id: update.taskId }));
        if (closed === false) {
          const authoritativeTask = await client.call("getTask", { task_id: update.taskId });
          if (authoritativeTask.is_active !== false) throw new IncidentStatusRevisionError("resolved task remained active after closeTask");
        }
      }
    }
  }
  return recordStatus;
}
