import type { OperationId } from "@awp/contracts";
import type { Clock } from "./ports/runtime.js";
import {
  WorkflowStepOutcomeConflictError,
  type ApplicationTransaction,
  type UnitOfWork,
  type WorkflowStepOutcome,
} from "./ports/repositories.js";

export interface WorkflowStepIdentity {
  readonly operationId: OperationId;
  readonly stepName: string;
  readonly stepKey: string;
}

export interface WorkflowStepExecution<T> {
  readonly outcome: T;
  readonly replayed: boolean;
}

export interface WorkflowStepCommit {
  readonly identity: WorkflowStepIdentity;
  readonly outcome: unknown;
  readonly recordedAt: string;
}

export type WorkflowStepAfterCommit = (commit: WorkflowStepCommit) => Promise<void> | void;

function assertIdentity(identity: WorkflowStepIdentity): void {
  if (!String(identity.operationId).trim()) throw new Error("Workflow step requires operationId");
  if (!identity.stepName.trim()) throw new Error("Workflow step requires a stable stepName");
  if (!identity.stepKey.trim()) throw new Error("Workflow step requires a stable stepKey");
}

export class WorkflowStepTransactionRunner {
  constructor(
    private readonly uow: UnitOfWork,
    private readonly clock: Clock,
    private readonly afterCommit?: WorkflowStepAfterCommit,
  ) {}

  async run<T>(
    identity: WorkflowStepIdentity,
    work: (tx: ApplicationTransaction) => Promise<T>,
  ): Promise<WorkflowStepExecution<T>> {
    assertIdentity(identity);
    try {
      const committed = await this.uow.transaction(async (tx) => {
        const existing = await tx.workflowStepOutcomes.get(
          identity.operationId,
          identity.stepName,
          identity.stepKey,
        );
        if (existing) {
          return { outcome: existing.outcome as T, replayed: true, marker: existing };
        }

        const outcome = await work(tx);
        const marker: WorkflowStepOutcome = {
          ...identity,
          outcome,
          recordedAt: this.clock.now().toISOString(),
        };
        await tx.workflowStepOutcomes.insert(marker);
        return { outcome, replayed: false, marker };
      });

      if (!committed.replayed && this.afterCommit) {
        await this.afterCommit({
          identity,
          outcome: committed.marker.outcome,
          recordedAt: committed.marker.recordedAt,
        });
      }
      return { outcome: committed.outcome, replayed: committed.replayed };
    } catch (error) {
      if (!(error instanceof WorkflowStepOutcomeConflictError)) throw error;
      const winner = await this.uow.transaction((tx) =>
        tx.workflowStepOutcomes.get(identity.operationId, identity.stepName, identity.stepKey),
      );
      if (!winner) {
        throw new Error(
          `Workflow step marker race for ${identity.operationId}/${identity.stepName}/${identity.stepKey} rolled back without a committed winner`,
          { cause: error },
        );
      }
      return { outcome: winner.outcome as T, replayed: true };
    }
  }
}

export interface DurableWorkflowStepRunner {
  run<T>(stepName: string, stepKey: string, work: () => Promise<T>): Promise<T>;
  afterDomainCommit?(identity: WorkflowStepIdentity): Promise<void> | void;
}

export const immediateWorkflowStepRunner: DurableWorkflowStepRunner = {
  run: async <T>(_stepName: string, _stepKey: string, work: () => Promise<T>) => work(),
};

export async function runMarkedWorkflowTransaction<T>(
  workflowSteps: DurableWorkflowStepRunner,
  transactions: WorkflowStepTransactionRunner,
  identity: WorkflowStepIdentity,
  work: (tx: ApplicationTransaction) => Promise<T>,
): Promise<T> {
  return workflowSteps.run(identity.stepName, identity.stepKey, async () => {
    const execution = await transactions.run(identity, work);
    if (!execution.replayed && workflowSteps.afterDomainCommit) {
      await workflowSteps.afterDomainCommit(identity);
    }
    return execution.outcome;
  });
}
