import { createHash } from "node:crypto";
import { type KanboardRpcClient } from "./kanboard-client";
import { createIncidentService, IncidentIdempotencyConflictError, IncidentMutationError, IncidentResolutionArtifactError, IncidentResolutionConflictError, type BriefConfig, type IncidentService } from "./incident-service";

import { checkIncidentsReadiness, type IncidentsReadiness } from "./bootstrap";
import { BriefAssemblyError } from "./dispatch-brief";
import { createDeployAssetDeps, readDeployProvenance } from "./brief-assets";
import { IncidentOptionsError, InvalidIncidentTypeError, serializeIncidentOptions, type IncidentOptionsResponse, type LoadedIncidentOptions } from "./dispatch-options";
import type { IncidentDispatchLauncher } from "./incident-launcher";
import type { IncidentMutationStore } from "./incident-mutation-store";
import { createResolutionLearning, type VerifiedResolutionEvidence } from "./resolution-learning";
import type { IncidentStatusUpdate } from "./status-writer";
import { incidentAssetsDir } from "../paths";

const DEFAULT_READINESS_TTL_MS = 60_000;
const UNAVAILABLE_MESSAGE = "incidents store unavailable";
const MAX_RESOLUTION_RETRIES = 8;
const RESOLUTION_RETRY_BASE_MS = 5_000;

function persistedEvidenceMatches(operation: { incidentId: string; evidenceSha256: string; evidenceJson: string }): boolean {
  try {
    const evidence = JSON.parse(operation.evidenceJson) as Record<string, unknown>;
    return evidence !== null
      && typeof evidence === "object"
      && evidence["incidentId"] === operation.incidentId
      && createHash("sha256").update(operation.evidenceJson).digest("hex") === operation.evidenceSha256;
  } catch {
    return false;
  }
}

export class IncidentsUnavailableError extends Error {}

export interface IncidentsProvider {
  getOptions?(): Promise<IncidentOptionsResponse>;
  listIncidents(query: Parameters<IncidentService["listIncidents"]>[0]): ReturnType<IncidentService["listIncidents"]>;
  getIncident(incidentId: Parameters<IncidentService["getIncident"]>[0]): ReturnType<IncidentService["getIncident"]>;
  fileIncident(request: Parameters<IncidentService["fileIncident"]>[0]): ReturnType<IncidentService["fileIncident"]>;
  dispatchIncident(
    incidentId: Parameters<IncidentService["dispatchIncident"]>[0],
    options?: Parameters<IncidentService["dispatchIncident"]>[1],
  ): ReturnType<IncidentService["dispatchIncident"]>;
  reconcileResolutions?(): Promise<void>;
  stopIncident?(incidentId: Parameters<IncidentService["stopIncident"]>[0]): ReturnType<IncidentService["stopIncident"]>;
  deleteIncident?(incidentId: Parameters<IncidentService["deleteIncident"]>[0]): ReturnType<IncidentService["deleteIncident"]>;
  resolveIncident(
    incidentId: Parameters<IncidentService["resolveIncident"]>[0],
    artifact: Parameters<IncidentService["resolveIncident"]>[1],
    summary?: Parameters<IncidentService["resolveIncident"]>[2],
  ): ReturnType<IncidentService["resolveIncident"]>;
  getDispatchLauncher?(): IncidentDispatchLauncher | null;
}

interface IncidentsProviderOptions {
  client: KanboardRpcClient;
  readinessTtlMs?: number;
  now?: () => number;
  brief?: BriefConfig;
  loadIncidentOptions?: () => Promise<LoadedIncidentOptions>;
  mutationStore?: IncidentMutationStore;
  launcher?: IncidentDispatchLauncher;
  workspaceFor?(incidentId: string): string;
  wrapperFor?(relativeWrapper: string): string;
  recordStatus?(update: IncidentStatusUpdate): Promise<void>;
  verifyResolutionArtifact?(request: { incidentId: string; incidentType: string | null; artifact: string; summary?: string }): Promise<VerifiedResolutionEvidence>;
  recordResolutionLearning?(evidence: VerifiedResolutionEvidence): Promise<void>;

}

function wrapUnavailable(): IncidentsUnavailableError {
  return new IncidentsUnavailableError(UNAVAILABLE_MESSAGE);
}

export function createIncidentsProvider(opts: IncidentsProviderOptions): IncidentsProvider {
  let readinessAt = 0;
  let readiness: IncidentsReadiness | undefined;
  let readinessInFlight: Promise<IncidentsReadiness> | undefined;
  const now = opts.now ?? Date.now;
  const readinessTtlMs = opts.readinessTtlMs ?? DEFAULT_READINESS_TTL_MS;

  async function getReadiness(): Promise<IncidentsReadiness> {
    const currentTime = now();
    if (readiness && currentTime - readinessAt < readinessTtlMs) {
      return readiness;
    }

    if (readinessInFlight) {
      return readinessInFlight;
    }

    const pending = checkIncidentsReadiness(opts.client)
      .then((next) => {
        readiness = next;
        readinessAt = now();
        return next;
      })
      .catch(() => {
        throw wrapUnavailable();
      })
      .finally(() => {
        if (readinessInFlight === pending) {
          readinessInFlight = undefined;
        }
      });
    readinessInFlight = pending;
    return pending;
  }

  const brief = opts.brief ?? { deps: createDeployAssetDeps(), provenance: readDeployProvenance };
  const resolutionLearning = createResolutionLearning({ assetsDir: incidentAssetsDir(), workspaceFor: opts.workspaceFor ?? (() => { throw new Error("incident workspace authority is not configured"); }) });
  const verifyResolutionArtifact = opts.verifyResolutionArtifact ?? resolutionLearning.verify;
  const recordResolutionLearning = opts.recordResolutionLearning ?? resolutionLearning.record;

  async function withService<R>(callback: (service: IncidentService) => Promise<R>): Promise<R> {
    try {
      const resolvedReadiness = await getReadiness();
      const service = createIncidentService({ client: opts.client, readiness: resolvedReadiness, brief, loadIncidentOptions: opts.loadIncidentOptions, mutationStore: opts.mutationStore, launcher: opts.launcher, workspaceFor: opts.workspaceFor, wrapperFor: opts.wrapperFor, recordStatus: opts.recordStatus, verifyResolutionArtifact, recordResolutionLearning });

      return await callback(service);
    } catch (error) {
      if (error instanceof IncidentIdempotencyConflictError) throw error;
      if (error instanceof IncidentMutationError) throw error;
      if (error instanceof IncidentResolutionConflictError) throw error;
      if (error instanceof IncidentResolutionArtifactError) throw error;
      if (error instanceof BriefAssemblyError) throw error;
      if (error instanceof IncidentOptionsError) throw error;
      if (error instanceof InvalidIncidentTypeError) throw error;
      throw wrapUnavailable();
    }
  }

  return {
    async getOptions() {
      if (!opts.loadIncidentOptions) throw new IncidentOptionsError("assets", "incident dispatch authority is not configured");
      return serializeIncidentOptions(await opts.loadIncidentOptions());
    },
    async listIncidents(query) {
      return withService((service) => service.listIncidents(query));
    },
    async getIncident(incidentId) {
      return withService((service) => service.getIncident(incidentId));
    },
    async fileIncident(request) {
      return withService((service) => service.fileIncident(request));
    },
    async dispatchIncident(incidentId, options) {
      return withService((service) => service.dispatchIncident(incidentId, options));
    },
    async stopIncident(incidentId) {
      return withService((service) => service.stopIncident(incidentId));
    },
    async deleteIncident(incidentId) {
      return withService((service) => service.deleteIncident(incidentId));
    },
    async reconcileResolutions() {
      if (!opts.mutationStore) return;
      for (const operation of opts.mutationStore.listPendingResolutions(now())) {
        try {
          if (!persistedEvidenceMatches(operation)) {
            opts.mutationStore.quarantineResolution(operation.incidentId, "persisted resolution identity is invalid");
            continue;
          }
          await withService((service) => service.resumeResolution(operation));
        } catch (error) {
          const attempt = Math.min((operation.attemptCount ?? 0) + 1, MAX_RESOLUTION_RETRIES);
          const message = error instanceof Error ? error.message : String(error);
          const backoff = RESOLUTION_RETRY_BASE_MS * 2 ** (attempt - 1);
          opts.mutationStore.recordResolutionRetry(operation.incidentId, attempt, now() + backoff, message);
        } finally {
          opts.mutationStore.advanceResolutionReconciliationCursor(operation.incidentId);
        }
      }
    },
    async resolveIncident(incidentId, artifact, summary) {
      return withService((service) => service.resolveIncident(incidentId, artifact, summary));
    },
    getDispatchLauncher(): IncidentDispatchLauncher | null {
      return opts.launcher ?? null;
    },
  };
}
