// src/core/actor/ActorBehavior.ts

import type { IMailbox, IActorMessage } from '../../engine/mailbox/IMailbox';
import type { IMatrixContext } from '../../engine/core/IMatrixContext';
import { RequestReplyHelper } from '../messaging/RequestReplyHelper';
import type { AcceptsDefinition } from './AcceptsDefinition';
import type { IActorBehavior } from './IActorBehavior';
import type { IDispatchStrategy } from './IDispatchStrategy';
import type { IMessageMeta } from './IMessageMeta';
import type { IDispatchPriorities } from './IDispatchPriorities';
import { DEFAULT_PRIORITIES } from './IDispatchPriorities';
import type { IBecomePayload } from './IBecomePayload';
import type { MessageDirection } from '../security/ISecurityRealm';

/** @deprecated Use IMessageMeta instead */
type MessageMeta = IMessageMeta;
/** @deprecated Use IDispatchPriorities instead */
type DispatchPriorities = IDispatchPriorities;
/** @deprecated Use IBecomePayload instead */
type BecomePayload<T extends AcceptsDefinition = AcceptsDefinition> = IBecomePayload<T>;

/**
 * Options for ActorBehavior construction.
 */
export interface IActorBehaviorOptions {
  /**
   * Whether to yield between messages (cooperative scheduling).
   * Default: true
   */
  cooperativeYield?: boolean;
}

/** @deprecated Use IActorBehaviorOptions instead */
export type ActorBehaviorOptions = IActorBehaviorOptions;

/**
 * Handler function type.
 */
type Handler = (payload: unknown, meta: MessageMeta) => Promise<unknown>;

/**
 * ActorBehavior - Encapsulates mailbox and handler dispatch.
 *
 * This class implements the composed actor behavior pattern:
 * - MatrixActor composes ActorBehavior (not extends)
 * - Mailbox ownership is exclusive to this behavior
 * - Handlers are registered and dispatched through strategies
 * - Erlang-style become() enables state machine patterns
 *
 * @example
 * ```typescript
 * class MyActor extends MatrixActor {
 *   protected onBootstrap() {
 *     this.enableActorMode({
 *       increment: this.handleIncrement.bind(this),
 *       query: this.handleQuery.bind(this),
 *     });
 *   }
 * }
 * ```
 */
export class ActorBehavior<TAccepts extends AcceptsDefinition = AcceptsDefinition>
  implements IActorBehavior<TAccepts>
{
  private readonly mailbox: IMailbox;
  private readonly context: IMatrixContext;
  private readonly handlers = new Map<string, Handler>();
  private readonly strategies: IDispatchStrategy[] = [];
  private readonly cooperativeYield: boolean;
  private priorities: DispatchPriorities = { ...DEFAULT_PRIORITIES };
  private running = false;

  constructor(context: IMatrixContext, options: IActorBehaviorOptions = {}) {
    this.context = context;
    this.mailbox = context.runtime.createMailbox(context.mount);
    this.cooperativeYield = options.cooperativeYield ?? true;
  }

  get isRunning(): boolean {
    return this.running;
  }

  get address(): string {
    return this.mailbox.address;
  }

  /**
   * Get the underlying mailbox for message queuing.
   *
   * This allows external code (like MatrixActor._bindAccepts) to queue
   * messages to the actor's mailbox. The ActorBehavior maintains ownership
   * and processes messages through its dispatch loop.
   */
  getMailbox(): IMailbox {
    return this.mailbox;
  }

  registerHandler<K extends keyof TAccepts & string>(
    command: K,
    handler: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>
  ): void {
    this.handlers.set(command, handler as Handler);
  }

  unregisterHandler(command: string): void {
    this.handlers.delete(command);
  }

  become(handlers: Partial<{
    [K in keyof TAccepts]: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>
  }>): void {
    this.handlers.clear();
    for (const [cmd, handler] of Object.entries(handlers)) {
      if (handler) {
        this.handlers.set(cmd, handler as Handler);
      }
    }
  }

  addDispatchStrategy(strategy: IDispatchStrategy): void {
    this.strategies.push(strategy);
    this.strategies.sort((a, b) => a.priority - b.priority);
  }

  removeDispatchStrategy(name: string): void {
    const idx = this.strategies.findIndex(s => s.name === name);
    if (idx >= 0) {
      this.strategies.splice(idx, 1);
    }
  }

  /**
   * Configure dispatch priorities.
   * Re-sorts strategies after update.
   */
  configurePriorities(priorities: Partial<DispatchPriorities>): void {
    this.priorities = { ...this.priorities, ...priorities };
    this.strategies.sort((a, b) => a.priority - b.priority);
  }

  /**
   * Check if a handler exists for a command.
   */
  hasHandler(command: string): boolean {
    if (this.handlers.has(command)) return true;
    return this.strategies.some(s => s.canHandle(command));
  }

  /**
   * Get list of registered handler commands.
   */
  listHandlers(): string[] {
    return Array.from(this.handlers.keys());
  }

  start(): void {
    if (this.running) return;
    this.running = true;
    this.mailbox.claim();
    this.runLoop();
  }

  stop(): void {
    this.running = false;
    this.mailbox.close();
  }

  /**
   * Main message processing loop.
   *
   * Dispatch order:
   * 1. System commands ($* prefix) - built-in handling
   * 2. Dispatch strategies (in priority order)
   * 3. TypeScript handlers (fallback)
   */
  private async runLoop(): Promise<void> {
    try {
      for await (const message of this.mailbox) {
        if (!this.running) break;
        await this.dispatch(message);
      }
    } catch (err) {
      if (this.running) {
        // Publish exit signal
        this.context.transport.publish(`${this.context.mount}.$exit`, {
          type: 'exit',
          from: this.context.mount,
          reason: 'error',
          error: { message: err instanceof Error ? err.message : String(err) },
        });
      }
    }
  }

  /**
   * Dispatch a single message.
   *
   * Vision 27 Security: Messages are verified before dispatch.
   * Joe Armstrong: "Every message must be verified at the boundary."
   */
  private async dispatch(message: IActorMessage): Promise<void> {
    // Cooperative yield - allow other tasks to run
    if (this.cooperativeYield) {
      await Promise.resolve();
    }

    const { command, payload } = message;
    const meta: MessageMeta = {
      ts: message.meta.ts,
      from: message.meta.from,
      correlationId: message.meta.correlationId,
      topic: `${this.context.mount}.accepts.${command}`,
    };

    // ═══════════════════════════════════════════════════════════════════
    // PRIORITY 1: System commands ($* prefix) - bypass normal dispatch
    // ═══════════════════════════════════════════════════════════════════
    if (command.startsWith('$')) {
      await this.handleSystemCommand(command, payload, meta);
      return;
    }

    // ═══════════════════════════════════════════════════════════════════
    // PRIORITY 1.5: SECURITY VERIFICATION (Vision 27)
    // Mark Miller: "The receiver decides whether to trust the sender."
    // ═══════════════════════════════════════════════════════════════════
    if (this.context.securityRealm) {
      const direction = this._determineDirection(message.meta.from);
      const capabilityId = message.meta.capabilityId;

      // Look up capability token if ID provided
      // Note: For cross-root verification, capability lookup would need
      // to consult a shared registry. For now, we verify against our security realm.
      const verificationResult = this.context.securityRealm.verifyMessage(
        direction,
        undefined, // TODO: Look up capability by ID from shared registry
        {
          principal: message.meta.from ?? 'unknown',
          target: this.context.mount,
          command,
        }
      );

      if (!verificationResult.allowed) {
        // eslint-disable-next-line no-console -- security rejection diagnostics in kernel behavior layer
        console.warn(
          `[ActorBehavior] Security: Message rejected at ${this.context.mount}`,
          {
            command,
            from: message.meta.from,
            direction,
            reason: verificationResult.reason,
          }
        );
        // Send error reply if this was a request-reply
        if (RequestReplyHelper.isEnvelope(payload)) {
          RequestReplyHelper.sendErrorReply(
            this.context.transport,
            payload,
            new Error(`Security: ${verificationResult.reason}`)
          );
        }
        return; // BLOCK the message
      }
    }

    // ═══════════════════════════════════════════════════════════════════
    // PRIORITY 2-N: Dispatch strategies (in priority order)
    // ═══════════════════════════════════════════════════════════════════
    for (const strategy of this.strategies) {
      if (strategy.canHandle(command)) {
        // Use executeWithReply (throws on error) instead of executeWithReplySafe
        // This allows errors to propagate to runLoop() for $exit signal
        await RequestReplyHelper.executeWithReply(
          this.context.transport,
          payload,
          (params) => strategy.handle(command, params ?? payload, meta)
        );
        return;
      }
    }

    // ═══════════════════════════════════════════════════════════════════
    // FALLBACK: TypeScript handlers
    // ═══════════════════════════════════════════════════════════════════
    const handler = this.handlers.get(command);
    if (handler) {
      // Use executeWithReply (throws on error) instead of executeWithReplySafe
      // This allows errors to propagate to runLoop() for $exit signal
      await RequestReplyHelper.executeWithReply(
        this.context.transport,
        payload,
        (params) => handler(params ?? payload, meta)
      );
      return;
    }

    // No handler found - log warning
    // eslint-disable-next-line no-console -- missing command handlers should remain visible in kernel logs
    console.warn(`[ActorBehavior] No handler for command: ${command} at ${this.context.mount}`);
  }

  /**
   * Type guard for BecomePayload.
   */
  private isBecomePayload(payload: unknown): payload is BecomePayload<TAccepts> {
    return (
      payload !== null &&
      typeof payload === 'object' &&
      'handlers' in payload &&
      typeof (payload as BecomePayload).handlers === 'object'
    );
  }

  /**
   * Handle system commands ($* prefix).
   *
   * System commands:
   * - $close: Stop the actor
   * - $ping: Health check
   * - $introspect: Return handler info
   * - $become: Change behavior at runtime
   */
  private async handleSystemCommand(
    command: string,
    payload: unknown,
    meta: MessageMeta
  ): Promise<void> {
    switch (command) {
      case '$close':
        this.stop();
        break;

      case '$ping':
        if (RequestReplyHelper.isEnvelope(payload)) {
          RequestReplyHelper.sendSuccessReply(this.context.transport, payload, {
            pong: true,
            mount: this.context.mount,
            ts: Date.now(),
          });
        }
        break;

      case '$introspect':
        if (RequestReplyHelper.isEnvelope(payload)) {
          RequestReplyHelper.sendSuccessReply(this.context.transport, payload, {
            mount: this.context.mount,
            handlers: this.listHandlers(),
            strategies: this.strategies.map(s => ({ name: s.name, priority: s.priority })),
            running: this.running,
          });
        }
        break;

      case '$become':
        // Allow runtime behavior change via message
        if (this.isBecomePayload(payload)) {
          this.become(payload.handlers);
        }
        break;

      default:
        // eslint-disable-next-line no-console -- unknown system commands should remain visible in kernel logs
        console.warn(`[ActorBehavior] Unknown system command: ${command}`);
    }
  }

  /**
   * Determine message direction for security verification.
   *
   * Vision 27: Direction determines trust level:
   * - parent-to-child: TRUSTED (supervisor relationship)
   * - child-to-parent: VERIFIED (requires capability)
   * - sibling: VERIFIED (requires capability)
   * - external: UNTRUSTED (requires capability + signature)
   *
   * Joe Armstrong (Erlang, "Programming Erlang"):
   *   "In Erlang, every message knows who sent it."
   *
   * @param from - Sender's mount path
   * @returns Message direction for policy lookup
   */
  private _determineDirection(from: string | undefined): MessageDirection {
    if (!from) {
      // Unknown sender = external (most restrictive)
      return 'external';
    }

    const myMount = this.context.mount;
    const parentMount = this.context.parent?.mount;

    // Parent-to-child: Sender is our parent
    if (parentMount && from === parentMount) {
      return 'parent-to-child';
    }

    // Child-to-parent: Sender's mount starts with our mount
    // e.g., myMount = "app.parent", from = "app.parent.child"
    if (from.startsWith(myMount + '.')) {
      return 'child-to-parent';
    }

    // Sibling: Same parent, different child
    // e.g., parentMount = "app", myMount = "app.a", from = "app.b"
    if (parentMount && from.startsWith(parentMount + '.') && from !== myMount) {
      return 'sibling';
    }

    // Everything else is external
    return 'external';
  }
}
