// src/engine/mailbox/Mailbox.ts

import type { ITransportAdapter } from '../remoting/ITransportAdapter';
import type {
  IMailbox,
  IActorMessage,
  IBackpressurePolicy,
  IMailboxOptions,
} from './IMailbox';

/**
 * Default backpressure policy.
 */
const DEFAULT_BACKPRESSURE: IBackpressurePolicy = {
  highWaterMark: 1000,
  onFull: 'drop-oldest',
};

/**
 * Mailbox - Exclusive message queue for an actor.
 *
 * Implementation notes:
 * - Uses transport.subscribe for a unique topic (the mailbox address)
 * - Only one subscriber allowed (enforced by claim())
 * - Messages queued internally, delivered via receive() / async iterator
 * - Supports backpressure via configurable policy
 *
 * This provides serialized message processing - the core actor guarantee.
 * Messages are processed one at a time, in order, preventing interleaving bugs.
 */
export class Mailbox implements IMailbox {
  private readonly queue: IActorMessage[] = [];
  private waiting: ((msg: IActorMessage) => void) | null = null;
  private _closed = false;
  private _claimed = false;
  private unsubscribe?: () => void;
  private readonly policy: IBackpressurePolicy;
  private readonly copyPayloads: boolean;

  constructor(
    readonly address: string,
    private readonly transport: ITransportAdapter,
    options: IMailboxOptions = {}
  ) {
    this.policy = { ...DEFAULT_BACKPRESSURE, ...options.backpressure };
    // Default to true per Rich Hickey: copy-by-default prevents mutation bugs
    // Performance-critical code can opt out with copyPayloads: false
    this.copyPayloads = options.copyPayloads ?? true;
  }

  get depth(): number {
    return this.queue.length;
  }

  get closed(): boolean {
    return this._closed;
  }

  get claimed(): boolean {
    return this._claimed;
  }

  /**
   * Claim exclusive ownership of this mailbox.
   * Must be called before receiving messages.
   */
  claim(): void {
    if (this._claimed) {
      throw new Error(`[Mailbox] ${this.address} already claimed`);
    }
    if (this._closed) {
      throw new Error(`[Mailbox] ${this.address} is closed`);
    }

    this._claimed = true;

    // Subscribe to the mailbox topic
    this.unsubscribe = this.transport.subscribe(this.address, (payload, meta) => {
      if (this._closed) return;

      // Validate and normalize message
      const msg = this.normalizeMessage(payload, meta);
      if (!msg) return;

      // Check backpressure
      if (this.queue.length >= this.policy.highWaterMark) {
        this.applyBackpressure(msg);
        return;
      }

      this.enqueueOrDeliver(msg);
    });
  }

  /**
   * Normalize incoming payload to IActorMessage format.
   */
  private normalizeMessage(
    payload: unknown,
    meta: { topic: string; ts: number }
  ): IActorMessage | null {
    // If payload is already IActorMessage shape, use it
    if (this.isActorMessage(payload)) {
      const msg = payload;
      return {
        command: msg.command,
        payload: this.copyPayloads ? this.clonePayload(msg.payload) : msg.payload,
        replyTo: msg.replyTo,
        meta: {
          ts: msg.meta?.ts ?? meta.ts,
          correlationId: msg.meta?.correlationId,
          from: msg.meta?.from,
        },
      };
    }

    // Legacy format: raw payload without command wrapper
    // This supports gradual migration from topic-based messaging
    // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for legacy payload warnings
    console.warn(`[Mailbox] ${this.address} received non-IActorMessage payload, wrapping as $raw`);
    return {
      command: '$raw',
      payload: this.copyPayloads ? this.clonePayload(payload) : payload,
      meta: { ts: meta.ts },
    };
  }

  /**
   * Type guard for IActorMessage shape.
   */
  private isActorMessage(payload: unknown): payload is IActorMessage {
    return (
      payload !== null &&
      typeof payload === 'object' &&
      'command' in payload &&
      typeof (payload as IActorMessage).command === 'string'
    );
  }

  /**
   * Clone payload to prevent mutation issues (Hickey mitigation).
   */
  private clonePayload(payload: unknown): unknown {
    if (payload === undefined || payload === null) return payload;
    if (typeof payload !== 'object') return payload;

    try {
      // Use structuredClone if available (modern environments)
      if (typeof structuredClone === 'function') {
        return structuredClone(payload);
      }
      // Fallback to JSON round-trip
      return JSON.parse(JSON.stringify(payload));
    } catch {
      // If cloning fails (circular refs, etc.), return original
      // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for clone fallback diagnostics
      console.warn(`[Mailbox] ${this.address} failed to clone payload, using original`);
      return payload;
    }
  }

  /**
   * Apply backpressure policy when queue is full.
   */
  private applyBackpressure(msg: IActorMessage): void {
    switch (this.policy.onFull) {
      case 'drop-oldest':
        this.queue.shift();
        this.enqueueOrDeliver(msg);
        break;
      case 'drop-newest':
        // Don't enqueue - just drop
        // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for backpressure drop diagnostics
        console.warn(`[Mailbox] ${this.address} queue full, dropping newest message`);
        break;
      case 'error':
        // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for backpressure overflow diagnostics
        console.error(`[Mailbox] ${this.address} queue full (${this.queue.length}), dropping message`);
        break;
      case 'block':
        // In async context we can't truly block, so just warn and continue
        // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for high-water diagnostics
        console.warn(`[Mailbox] ${this.address} at high water mark (${this.queue.length})`);
        this.enqueueOrDeliver(msg);
        break;
    }
  }

  /**
   * Either deliver immediately (if someone waiting) or enqueue.
   */
  private enqueueOrDeliver(msg: IActorMessage): void {
    if (this.waiting) {
      const resolve = this.waiting;
      this.waiting = null;
      resolve(msg);
    } else {
      this.queue.push(msg);
    }
  }

  /**
   * Send a message to this mailbox.
   * Anyone with the address can send.
   */
  send(message: IActorMessage): void {
    if (this._closed) {
      // eslint-disable-next-line no-console -- kernel mailbox infrastructure has no actor context for closed-mailbox send diagnostics
      console.warn(`[Mailbox] Cannot send to closed mailbox: ${this.address}`);
      return;
    }

    // Optionally clone payload before sending
    const msg = this.copyPayloads
      ? { ...message, payload: this.clonePayload(message.payload) }
      : message;

    this.transport.publish(this.address, msg);
  }

  /**
   * Receive the next message (blocks until available).
   * Only the owner can receive.
   */
  async receive(): Promise<IActorMessage> {
    if (this._closed) {
      throw new Error(`[Mailbox] ${this.address} is closed`);
    }
    if (!this._claimed) {
      throw new Error(`[Mailbox] ${this.address} not claimed - call claim() first`);
    }

    // If queue has messages, return immediately
    if (this.queue.length > 0) {
      return this.queue.shift()!;
    }

    // Wait for next message
    return new Promise<IActorMessage>((resolve) => {
      this.waiting = resolve;
    });
  }

  /**
   * Async iterator for message loop.
   * Enables: for await (const msg of mailbox) { ... }
   */
  async *[Symbol.asyncIterator](): AsyncIterableIterator<IActorMessage> {
    while (!this._closed) {
      try {
        const msg = await this.receive();
        // $close is sent internally when mailbox closes
        if (msg.command === '$close') {
          return;
        }
        yield msg;
      } catch (err) {
        // If closed during receive, exit cleanly
        if (this._closed) return;
        throw err;
      }
    }
  }

  /**
   * Close the mailbox.
   * Called on actor termination.
   */
  close(): void {
    if (this._closed) return;
    this._closed = true;
    this._claimed = false; // Allow re-claim after restart

    // Unsubscribe from transport
    if (this.unsubscribe) {
      this.unsubscribe();
      this.unsubscribe = undefined;
    }

    // Resolve any waiting receiver with a close signal
    if (this.waiting) {
      const closeMsg: IActorMessage = {
        command: '$close',
        payload: undefined,
        meta: { ts: Date.now() },
      };
      this.waiting(closeMsg);
      this.waiting = null;
    }

    // Clear queue
    this.queue.length = 0;
  }
}
