import {
  type EventPublishOptions,
  parseEventPayload,
} from "../events/index.js";
import type {
  DomainEventDef,
  EventBusPort,
  InferEventPayload,
} from "./events.js";

type MaybePromise<T> = T | Promise<T>;

/**
 * Work function executed inside a Unit of Work.
 *
 * `tx` contains transaction-scoped app ports, usually repositories plus
 * side-effect recorders such as `events`, `jobs`, or `audit`.
 */
export type UnitOfWorkCallback<TxPorts, Result> = (
  tx: TxPorts,
) => MaybePromise<Result>;

/**
 * A transaction boundary for application workflows.
 *
 * The transaction ports are app-owned. A database adapter can provide
 * transaction-scoped repositories, while test or in-memory adapters can use
 * `createNoopUnitOfWork`.
 */
export interface UnitOfWorkPort<TxPorts> {
  /**
   * Run application work with transaction-scoped ports.
   *
   * Durable implementations should commit only if this callback resolves.
   */
  transaction<Result>(
    work: UnitOfWorkCallback<TxPorts, Result>,
  ): Promise<Result>;
}

/**
 * Hooks for `createNoopUnitOfWork(...)`.
 */
export interface NoopUnitOfWorkOptions<TxPorts> {
  /**
   * Runs after the callback completes successfully.
   *
   * Use this to flush buffered domain events after the work has committed.
   */
  afterCommit?: (tx: TxPorts) => MaybePromise<void>;

  /**
   * Runs after the callback throws.
   *
   * Use this to clear buffers or release test resources. The original error is
   * rethrown after this hook runs.
   */
  afterRollback?: (error: unknown, tx: TxPorts) => MaybePromise<void>;
}

/**
 * Best-effort observers for `createObservedUnitOfWork(...)`.
 */
export interface ObservedUnitOfWorkOptions {
  /**
   * Runs after the wrapped Unit of Work resolves successfully.
   *
   * Observer failures are isolated and never reject the committed operation.
   */
  afterCommit: () => MaybePromise<void>;

  /**
   * Receives an `afterCommit` failure. Failures from this observer are also
   * isolated.
   */
  onObserverError?: (error: unknown) => MaybePromise<void>;
}

/**
 * Domain event captured by a buffered event recorder.
 */
export interface RecordedDomainEvent {
  /**
   * Event definition used to validate the payload before publishing.
   */
  event: DomainEventDef;
  /**
   * Stable event name.
   */
  eventName: string;
  /**
   * Unparsed payload recorded during the transaction.
   */
  payload: unknown;
  /** Optional metadata propagated when the event is flushed. */
  options?: EventPublishOptions;
}

/**
 * Transaction-scoped port used to record domain events.
 *
 * Use cases record events here during the Unit of Work. The adapter decides
 * whether to publish after commit, enqueue through an outbox, or buffer for a
 * test assertion.
 */
export interface DomainEventRecorderPort {
  /**
   * Record a domain event payload.
   */
  record<E extends DomainEventDef>(
    event: E,
    payload: InferEventPayload<E>,
    options?: EventPublishOptions,
  ): Promise<void> | void;
}

/**
 * In-memory event recorder that can be inspected, cleared, or flushed.
 */
export interface BufferedDomainEventRecorder extends DomainEventRecorderPort {
  /**
   * Return recorded events without clearing them.
   */
  entries(): readonly RecordedDomainEvent[];
  /**
   * Remove all recorded events.
   */
  clear(): void;
  /**
   * Validate and publish all recorded events to an event bus in FIFO order.
   */
  flush(eventBus: EventBusPort): Promise<void>;
}

/**
 * Create a simple Unit of Work implementation for tests, in-memory adapters,
 * and infrastructure that already handles transactions elsewhere.
 *
 * This helper does not create database transactions. It gives applications the
 * same UOW shape everywhere and runs commit/rollback hooks around the callback.
 *
 * @param txPortsOrFactory - Transaction-scoped ports or a factory that creates
 * them per transaction call.
 * @param options - Optional commit and rollback hooks.
 * @returns A Unit of Work port with no durable transaction semantics.
 */
export function createNoopUnitOfWork<TxPorts>(
  txPortsOrFactory: TxPorts | (() => TxPorts),
  options: NoopUnitOfWorkOptions<TxPorts> = {},
): UnitOfWorkPort<TxPorts> {
  const createTxPorts =
    typeof txPortsOrFactory === "function"
      ? (txPortsOrFactory as () => TxPorts)
      : () => txPortsOrFactory;

  return {
    async transaction<Result>(
      work: UnitOfWorkCallback<TxPorts, Result>,
    ): Promise<Result> {
      const tx = createTxPorts();

      let result: Result;

      try {
        result = await work(tx);
      } catch (error) {
        try {
          await options.afterRollback?.(error, tx);
        } catch {
          // Preserve the application error that caused the rollback path.
        }

        throw error;
      }

      await options.afterCommit?.(tx);
      return result;
    },
  };
}

/**
 * Decorate a Unit of Work with an isolated post-commit observer.
 *
 * The observer runs only after the wrapped transaction resolves. Its failure
 * cannot turn a committed operation into an apparent transaction failure.
 * Use the observer to schedule best-effort follow-up work; durable side
 * effects still belong inside the transaction through an outbox.
 */
export function createObservedUnitOfWork<TxPorts>(
  options: {
    unitOfWork: UnitOfWorkPort<TxPorts>;
  } & ObservedUnitOfWorkOptions,
): UnitOfWorkPort<TxPorts> {
  return {
    async transaction<Result>(
      work: UnitOfWorkCallback<TxPorts, Result>,
    ): Promise<Result> {
      const result = await options.unitOfWork.transaction(work);

      try {
        await options.afterCommit();
      } catch (error) {
        try {
          await options.onObserverError?.(error);
        } catch {
          // Preserve the successful transaction result when reporting fails.
        }
      }

      return result;
    },
  };
}

/**
 * Create a recorder that buffers domain events until the caller flushes them.
 *
 * Unit of Work adapters commonly flush this recorder from an `afterCommit`
 * hook so events are not published when the work rolls back.
 *
 * @returns A buffered domain event recorder for tests or Unit of Work adapters.
 */
export function createDomainEventRecorder(): BufferedDomainEventRecorder {
  const records: RecordedDomainEvent[] = [];

  return {
    record(event, payload, options) {
      records.push({
        event,
        eventName: event.name,
        payload,
        ...(options ? { options } : {}),
      });
    },

    entries() {
      return records;
    },

    clear() {
      records.length = 0;
    },

    async flush(eventBus) {
      while (records.length > 0) {
        const record = records[0];
        await parseEventPayload(record.event, record.payload);
        await eventBus.publish(
          record.event,
          record.payload as never,
          record.options,
        );
        records.shift();
      }
    },
  };
}
