import type { EventPayloadDef, ListenerDef } from "../events/index.js";
import type { JobDef } from "../jobs/index.js";
import type { OutboxRegistry } from "../outbox/index.js";
import type { ScheduleDef } from "../schedules/index.js";
import type { TaskDef } from "../tasks/index.js";

export type RuntimeIntegrityArtifact =
  | "listener"
  | "schedule"
  | "task"
  | "outboxEvent"
  | "outboxJob";

export type RuntimeIntegrityFindingCode =
  | "missing_listener_registration"
  | "missing_schedule_registration"
  | "missing_task_registration"
  | "missing_outbox_event_registration"
  | "missing_outbox_job_registration"
  | "duplicate_runtime_manifest_entry"
  | "duplicate_runtime_registry_entry";

export type RuntimeIntegrityFindingLocation = "manifest" | "registry";

export interface RuntimeIntegrityFinding {
  readonly code: RuntimeIntegrityFindingCode;
  readonly artifact: RuntimeIntegrityArtifact;
  readonly name: string;
  readonly message: string;
  readonly location?: RuntimeIntegrityFindingLocation;
}

export interface RuntimeIntegrityResult {
  readonly ok: boolean;
  readonly findings: readonly RuntimeIntegrityFinding[];
}

export interface RuntimeOutboxManifest {
  readonly events?: readonly EventPayloadDef[];
  readonly jobs?: readonly JobDef[];
}

export interface RuntimeManifestInput {
  readonly listeners?: readonly ListenerDef[];
  readonly schedules?: readonly ScheduleDef[];
  readonly tasks?: readonly TaskDef[];
  readonly outbox?: RuntimeOutboxManifest;
}

export interface RuntimeManifest {
  readonly listeners: readonly ListenerDef[];
  readonly schedules: readonly ScheduleDef[];
  readonly tasks: readonly TaskDef[];
  readonly outbox: {
    readonly events: readonly EventPayloadDef[];
    readonly jobs: readonly JobDef[];
  };
}

export interface RuntimeRegistriesInput {
  readonly listeners?: readonly ListenerDef[];
  readonly schedules?: readonly ScheduleDef[];
  readonly tasks?: readonly TaskDef[];
  readonly outbox?: OutboxRegistry;
}

export interface RuntimeRegistries {
  readonly listeners: readonly ListenerDef[];
  readonly schedules: readonly ScheduleDef[];
  readonly tasks: readonly TaskDef[];
  readonly outbox?: OutboxRegistry;
}

export type RuntimeIntegrityMode = "error" | "warn";

export interface RuntimeIntegrityLogger {
  warn(message: string): void;
}

export interface CreateRuntimeIntegrityOptions {
  readonly manifest: RuntimeManifest;
  readonly registries: RuntimeRegistries;
  /**
   * How to handle integrity findings.
   *
   * @default "error"
   */
  readonly mode?: RuntimeIntegrityMode;
  /**
   * Logger used when `mode` is `"warn"`.
   *
   * @default console
   */
  readonly logger?: RuntimeIntegrityLogger;
}

export interface RuntimeIntegrityCheck {
  readonly kind: "runtime-integrity";
  check(): RuntimeIntegrityResult;
}

type NamedDefinition = {
  readonly name: string;
};

type IntegrityArtifactConfig = {
  readonly artifact: RuntimeIntegrityArtifact;
  readonly label: string;
  readonly manifestNames: readonly string[];
  readonly registryNames: readonly string[];
  readonly missingCode: RuntimeIntegrityFindingCode;
  readonly registryDescription: string;
};

export class RuntimeIntegrityError extends Error {
  readonly findings: readonly RuntimeIntegrityFinding[];

  constructor(findings: readonly RuntimeIntegrityFinding[]) {
    super(formatRuntimeIntegrityFailure(findings));
    this.name = "RuntimeIntegrityError";
    this.findings = findings;
  }
}

export function defineRuntimeManifest(
  input: RuntimeManifestInput,
): RuntimeManifest {
  return {
    listeners: input.listeners ?? [],
    schedules: input.schedules ?? [],
    tasks: input.tasks ?? [],
    outbox: {
      events: input.outbox?.events ?? [],
      jobs: input.outbox?.jobs ?? [],
    },
  };
}

export function defineRuntimeRegistries(
  input: RuntimeRegistriesInput,
): RuntimeRegistries {
  return {
    listeners: input.listeners ?? [],
    schedules: input.schedules ?? [],
    tasks: input.tasks ?? [],
    outbox: input.outbox,
  };
}

export function inspectRuntimeIntegrity(
  manifest: RuntimeManifest,
  registries: RuntimeRegistries,
): RuntimeIntegrityResult {
  const configs: readonly IntegrityArtifactConfig[] = [
    {
      artifact: "listener",
      label: "listener",
      manifestNames: namesFromDefinitions(manifest.listeners),
      registryNames: namesFromDefinitions(registries.listeners),
      missingCode: "missing_listener_registration",
      registryDescription: "runtime registries.listeners",
    },
    {
      artifact: "schedule",
      label: "schedule",
      manifestNames: namesFromDefinitions(manifest.schedules),
      registryNames: namesFromDefinitions(registries.schedules),
      missingCode: "missing_schedule_registration",
      registryDescription: "runtime registries.schedules",
    },
    {
      artifact: "task",
      label: "task",
      manifestNames: namesFromDefinitions(manifest.tasks),
      registryNames: namesFromDefinitions(registries.tasks),
      missingCode: "missing_task_registration",
      registryDescription: "runtime registries.tasks",
    },
    {
      artifact: "outboxEvent",
      label: "outbox event",
      manifestNames: namesFromDefinitions(manifest.outbox.events),
      registryNames: namesFromMap(registries.outbox?.events),
      missingCode: "missing_outbox_event_registration",
      registryDescription: "runtime registries.outbox.events",
    },
    {
      artifact: "outboxJob",
      label: "outbox job",
      manifestNames: namesFromDefinitions(manifest.outbox.jobs),
      registryNames: namesFromMap(registries.outbox?.jobs),
      missingCode: "missing_outbox_job_registration",
      registryDescription: "runtime registries.outbox.jobs",
    },
  ];

  const findings = configs.flatMap((config) =>
    inspectArtifactIntegrity(config),
  );

  return {
    ok: findings.length === 0,
    findings,
  };
}

export function createRuntimeIntegrity(
  options: CreateRuntimeIntegrityOptions,
): RuntimeIntegrityCheck {
  const mode = options.mode ?? "error";
  const logger = options.logger ?? console;

  return {
    kind: "runtime-integrity",
    check() {
      const result = inspectRuntimeIntegrity(
        options.manifest,
        options.registries,
      );

      if (result.ok) return result;

      if (mode === "warn") {
        logger.warn(
          `[beignet] ${formatRuntimeIntegrityFailure(result.findings)}`,
        );
        return result;
      }

      throw new RuntimeIntegrityError(result.findings);
    },
  };
}

export function runRuntimeIntegrityCheck(
  check: RuntimeIntegrityCheck | undefined,
): RuntimeIntegrityResult | undefined {
  return check?.check();
}

function inspectArtifactIntegrity(
  config: IntegrityArtifactConfig,
): RuntimeIntegrityFinding[] {
  const findings: RuntimeIntegrityFinding[] = [];
  const manifestDuplicates = duplicateNames(config.manifestNames);
  const registryDuplicates = duplicateNames(config.registryNames);

  for (const name of manifestDuplicates) {
    findings.push({
      code: "duplicate_runtime_manifest_entry",
      artifact: config.artifact,
      name,
      location: "manifest",
      message: `Duplicate ${config.label} "${name}" in runtime manifest.`,
    });
  }

  for (const name of registryDuplicates) {
    findings.push({
      code: "duplicate_runtime_registry_entry",
      artifact: config.artifact,
      name,
      location: "registry",
      message: `Duplicate ${config.label} "${name}" in ${config.registryDescription}.`,
    });
  }

  const registryNameSet = new Set(config.registryNames);
  for (const name of uniqueNames(config.manifestNames)) {
    if (registryNameSet.has(name)) continue;
    findings.push({
      code: config.missingCode,
      artifact: config.artifact,
      name,
      message: `Missing ${config.label} registration "${name}": declared in runtime manifest but not present in ${config.registryDescription}.`,
    });
  }

  return findings;
}

function namesFromDefinitions(
  definitions: readonly NamedDefinition[] | undefined,
): string[] {
  return (definitions ?? []).map((definition) => definition.name);
}

function namesFromMap<T extends NamedDefinition>(
  definitions: ReadonlyMap<string, T> | undefined,
): string[] {
  return [...(definitions?.keys() ?? [])];
}

function duplicateNames(names: readonly string[]): string[] {
  const seen = new Set<string>();
  const duplicates = new Set<string>();

  for (const name of names) {
    if (seen.has(name)) {
      duplicates.add(name);
      continue;
    }
    seen.add(name);
  }

  return [...duplicates].sort();
}

function uniqueNames(names: readonly string[]): string[] {
  return [...new Set(names)].sort();
}

function formatRuntimeIntegrityFailure(
  findings: readonly RuntimeIntegrityFinding[],
): string {
  const summary = `Runtime integrity check failed with ${findings.length} finding${findings.length === 1 ? "" : "s"}.`;
  const details = findings
    .map((finding) => `- [${finding.code}] ${finding.message}`)
    .join("\n");

  return `${summary}\n${details}`;
}
