import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementRequest,
  RegexReplacementResult,
} from "../model/match";
import { WorkerRequestError, WorkerSupervisor } from "./WorkerSupervisor";
import type {
  EngineWorkerOperation,
  EngineWorkerResult,
} from "./worker-protocol";
import {
  ENGINE_WORKER_REGISTRY,
  type EngineWorkerRegistry,
} from "./engine-registry";
import type { RegexEngineInfo, RegexFlavourId } from "../model/flavour";

export type EngineRuntimeState =
  | { readonly status: "unloaded" }
  | { readonly status: "running"; readonly operation: "execute" | "replace" }
  | { readonly status: "ready" }
  | { readonly status: "recovering"; readonly reason: string }
  | { readonly status: "failed"; readonly error: Error };

export class EngineSupervisor {
  private readonly registry: EngineWorkerRegistry;
  private readonly maximumRetainedWorkers: number;
  private readonly supervisors = new Map<
    RegexFlavourId,
    WorkerSupervisor<EngineWorkerOperation, EngineWorkerResult>
  >();
  private readonly loaded = new Map<
    RegexFlavourId,
    {
      readonly generation: number;
      readonly info: RegexEngineInfo;
    }
  >();
  private readonly loading = new Map<
    RegexFlavourId,
    Promise<RegexEngineInfo>
  >();
  private readonly operationRevisions = new Map<RegexFlavourId, number>();
  private readonly lastUsed = new Map<RegexFlavourId, number>();
  private useRevision = 0;
  private disposed = false;

  constructor(
    registry: EngineWorkerRegistry = ENGINE_WORKER_REGISTRY,
    maximumRetainedWorkers = 2,
  ) {
    if (
      !Number.isSafeInteger(maximumRetainedWorkers) ||
      maximumRetainedWorkers < 1
    ) {
      throw new RangeError("EngineSupervisor must retain at least one worker.");
    }
    this.registry = registry;
    this.maximumRetainedWorkers = maximumRetainedWorkers;
  }

  private touch(flavour: RegexFlavourId): void {
    this.useRevision += 1;
    this.lastUsed.set(flavour, this.useRevision);
  }

  private evictIdleWorker(except: RegexFlavourId): void {
    if (this.supervisors.size < this.maximumRetainedWorkers) return;
    const candidate = [...this.supervisors.entries()]
      .filter(
        ([flavour, supervisor]) =>
          flavour !== except &&
          !supervisor.isRunning &&
          !this.loading.has(flavour),
      )
      .sort(
        ([left], [right]) =>
          (this.lastUsed.get(left) ?? 0) - (this.lastUsed.get(right) ?? 0),
      )[0];
    if (!candidate) return;
    const [flavour, supervisor] = candidate;
    supervisor.dispose();
    this.supervisors.delete(flavour);
    this.loaded.delete(flavour);
    this.lastUsed.delete(flavour);
    this.operationRevisions.delete(flavour);
  }

  private supervisorFor(
    flavour: RegexFlavourId,
  ): WorkerSupervisor<EngineWorkerOperation, EngineWorkerResult> {
    if (this.disposed) {
      throw new Error("Engine supervisor has been disposed.");
    }
    const current = this.supervisors.get(flavour);
    if (current) {
      this.touch(flavour);
      return current;
    }
    this.evictIdleWorker(flavour);
    const registration = this.registry.require(flavour);
    const supervisor = new WorkerSupervisor<
      EngineWorkerOperation,
      EngineWorkerResult
    >(registration.label, registration.createWorker);
    this.supervisors.set(flavour, supervisor);
    this.touch(flavour);
    return supervisor;
  }

  async load(flavour: RegexFlavourId): Promise<RegexEngineInfo> {
    const registration = this.registry.require(flavour);
    const supervisor = this.supervisorFor(flavour);
    const loaded = this.loaded.get(flavour);
    if (
      loaded &&
      supervisor.hasWorker &&
      loaded.generation === supervisor.currentGeneration
    ) {
      this.touch(flavour);
      return loaded.info;
    }
    const current = this.loading.get(flavour);
    if (current && supervisor.hasWorker) return current;
    const loading: Promise<RegexEngineInfo> = supervisor
      .run({ kind: "load" }, registration.startupTimeoutMs)
      .then((response) => {
        if (response.kind !== "load") {
          throw new Error(
            "Engine worker returned the wrong load response kind",
          );
        }
        if (response.info.flavour !== flavour) {
          throw new Error(
            `Engine worker for ${flavour} loaded a ${response.info.flavour} runtime.`,
          );
        }
        this.loaded.set(flavour, {
          generation: supervisor.currentGeneration,
          info: response.info,
        });
        this.touch(flavour);
        return response.info;
      })
      .finally(() => {
        if (this.loading.get(flavour) === loading) {
          this.loading.delete(flavour);
        }
      });
    this.loading.set(flavour, loading);
    return loading;
  }

  private async prepareOperation(flavour: RegexFlavourId): Promise<{
    readonly revision: number;
    readonly supervisor: WorkerSupervisor<
      EngineWorkerOperation,
      EngineWorkerResult
    >;
  }> {
    const revision = (this.operationRevisions.get(flavour) ?? 0) + 1;
    this.operationRevisions.set(flavour, revision);
    const supervisor = this.supervisorFor(flavour);
    if (supervisor.isRunning) supervisor.cancel();
    await this.load(flavour);
    if (this.operationRevisions.get(flavour) !== revision) {
      throw new WorkerRequestError(
        "cancelled",
        `${this.registry.require(flavour).label} request was superseded`,
      );
    }
    return { revision, supervisor };
  }

  async execute(
    request: RegexExecutionRequest,
    timeoutMs: number,
  ): Promise<RegexExecutionResult> {
    const { supervisor } = await this.prepareOperation(request.flavour);
    const response = await supervisor.run(
      { kind: "execute", request },
      timeoutMs,
    );
    if (response.kind !== "execute") {
      throw new Error("Engine worker returned the wrong response kind");
    }
    if (response.result.engine.flavour !== request.flavour) {
      throw new Error(
        `Engine worker for ${request.flavour} returned a ${response.result.engine.flavour} result.`,
      );
    }
    return response.result;
  }

  async replace(
    request: RegexReplacementRequest,
    timeoutMs: number,
  ): Promise<RegexReplacementResult> {
    const { supervisor } = await this.prepareOperation(request.flavour);
    const response = await supervisor.run(
      { kind: "replace", request },
      timeoutMs,
    );
    if (response.kind !== "replace") {
      throw new Error("Engine worker returned the wrong response kind");
    }
    if (response.result.execution.engine.flavour !== request.flavour) {
      throw new Error(
        `Engine worker for ${request.flavour} returned a ${response.result.execution.engine.flavour} result.`,
      );
    }
    return response.result;
  }

  cancel(): void {
    for (const [flavour, supervisor] of this.supervisors) {
      this.operationRevisions.set(
        flavour,
        (this.operationRevisions.get(flavour) ?? 0) + 1,
      );
      supervisor.cancel();
    }
  }

  /**
   * Releases one idle runtime immediately. Active requests are deliberately
   * left alone; callers can cancel first when they intend to interrupt work.
   */
  unload(flavour: RegexFlavourId): boolean {
    const supervisor = this.supervisors.get(flavour);
    if (!supervisor || supervisor.isRunning || this.loading.has(flavour)) {
      return false;
    }
    supervisor.dispose();
    this.supervisors.delete(flavour);
    this.loaded.delete(flavour);
    this.lastUsed.delete(flavour);
    this.operationRevisions.delete(flavour);
    return true;
  }

  dispose(): void {
    if (this.disposed) return;
    this.disposed = true;
    for (const supervisor of this.supervisors.values()) supervisor.dispose();
    this.supervisors.clear();
    this.loaded.clear();
    this.loading.clear();
    this.operationRevisions.clear();
    this.lastUsed.clear();
  }
}
