import {
  type CoordinationObservation,
  type CoordinationResult,
  type CoordinationScope,
  type InteractionCoordinationActivity,
  type InteractionCoordinationPort,
  type Handoff,
  type HandoffReceipt,
  type HandoffStatus,
} from './interaction-coordination.js';

const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000;
const DEFAULT_MAX_HANDOFFS = 1_000;

interface StoredHandoff {
  readonly handoff: Handoff;
  status: HandoffStatus;
  readonly activity: InteractionCoordinationActivity[];
  readonly expiresAt: number;
  result?: CoordinationResult;
}

export interface InMemoryInteractionCoordinationPortOptions {
  now?: () => Date;
  handoffTtlMs?: number;
  maxHandoffs?: number;
}

/**
 * Native deterministic queue/port adapter for the coordination boundary.
 * It intentionally has no fleet/tmux dependency. A future deployment adapter
 * implements InteractionCoordinationPort without changing interaction-plane callers.
 */
export class InMemoryInteractionCoordinationPort implements InteractionCoordinationPort {
  private readonly handoffs = new Map<string, StoredHandoff>();
  private readonly now: () => Date;
  private readonly handoffTtlMs: number;
  private readonly maxHandoffs: number;

  constructor(options: InMemoryInteractionCoordinationPortOptions = {}) {
    this.now = options.now ?? (() => new Date());
    this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS;
    this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
  }

  async handoff(handoff: Handoff): Promise<HandoffReceipt> {
    this.pruneExpiredHandoffs();
    const existing = this.handoffs.get(handoff.handoffId);
    if (existing !== undefined) {
      this.assertSameHandoff(existing.handoff, handoff);
      return this.receipt(existing.handoff, existing.status);
    }

    const stored: StoredHandoff = {
      handoff: snapshotHandoff(handoff),
      status: 'queued',
      activity: [activity('queued', 'Handoff accepted by the native coordination queue', this.now)],
      expiresAt: this.now().getTime() + this.handoffTtlMs,
    };
    this.handoffs.set(handoff.handoffId, stored);
    this.enforceHandoffLimit();
    return this.receipt(stored.handoff, stored.status);
  }

  async observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation> {
    this.pruneExpiredHandoffs();
    const stored = this.requireScopedHandoff(handoffId, scope);
    return {
      handoffId: stored.handoff.handoffId,
      targetAgentId: stored.handoff.targetAgentId,
      status: stored.status,
      correlationId: stored.handoff.scope.correlationId,
      activity: stored.activity.map(copyActivity),
    };
  }

  async result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult> {
    this.pruneExpiredHandoffs();
    const stored = this.requireScopedHandoff(handoffId, scope);
    return (
      stored.result ?? {
        handoffId: stored.handoff.handoffId,
        targetAgentId: stored.handoff.targetAgentId,
        status: 'pending',
        correlationId: stored.handoff.scope.correlationId,
      }
    );
  }

  /** Host-side progression seam; interaction clients never receive this capability. */
  recordActivity(handoffId: string, status: HandoffStatus, summary: string): void {
    this.pruneExpiredHandoffs();
    const stored = this.requireHandoff(handoffId);
    stored.status = status;
    stored.activity.push(activity(status, summary, this.now));
  }

  /** Host-side result seam for deterministic qualification; not an orchestrator consumer. */
  recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void {
    this.pruneExpiredHandoffs();
    const stored = this.requireHandoff(handoffId);
    stored.status = status;
    stored.activity.push(activity(status, summary, this.now));
    stored.result = {
      handoffId: stored.handoff.handoffId,
      targetAgentId: stored.handoff.targetAgentId,
      status,
      correlationId: stored.handoff.scope.correlationId,
      summary,
    };
  }

  private receipt(handoff: Handoff, status: HandoffStatus): HandoffReceipt {
    return {
      handoffId: handoff.handoffId,
      targetAgentId: handoff.targetAgentId,
      status: status === 'accepted' ? 'accepted' : 'queued',
      correlationId: handoff.scope.correlationId,
    };
  }

  private pruneExpiredHandoffs(): void {
    const nowMs = this.now().getTime();
    for (const [handoffId, handoff] of this.handoffs) {
      if (handoff.expiresAt <= nowMs) this.handoffs.delete(handoffId);
    }
  }

  private enforceHandoffLimit(): void {
    while (this.handoffs.size > this.maxHandoffs) {
      const oldest = this.handoffs.keys().next().value;
      if (typeof oldest !== 'string') return;
      this.handoffs.delete(oldest);
    }
  }

  private requireScopedHandoff(handoffId: string, scope: CoordinationScope): StoredHandoff {
    const stored = this.requireHandoff(handoffId);
    if (
      stored.handoff.scope.tenantId !== scope.tenantId ||
      stored.handoff.scope.actorId !== scope.actorId ||
      stored.handoff.scope.requesterAgentId !== scope.requesterAgentId
    ) {
      throw new InMemoryInteractionCoordinationError('forbidden', 'Handoff scope does not match');
    }
    return stored;
  }

  private requireHandoff(handoffId: string): StoredHandoff {
    const stored = this.handoffs.get(handoffId);
    if (stored === undefined) {
      throw new InMemoryInteractionCoordinationError('not_found', 'Handoff was not found');
    }
    return stored;
  }

  private assertSameHandoff(existing: Handoff, incoming: Handoff): void {
    if (
      existing.targetAgentId !== incoming.targetAgentId ||
      existing.request.idempotencyKey !== incoming.request.idempotencyKey ||
      existing.request.summary !== incoming.request.summary ||
      existing.request.context !== incoming.request.context ||
      existing.request.missionId !== incoming.request.missionId ||
      existing.scope.actorId !== incoming.scope.actorId ||
      existing.scope.tenantId !== incoming.scope.tenantId ||
      existing.scope.correlationId !== incoming.scope.correlationId ||
      existing.scope.requesterAgentId !== incoming.scope.requesterAgentId
    ) {
      throw new InMemoryInteractionCoordinationError(
        'conflict',
        'Handoff ID is already bound to different immutable input',
      );
    }
  }
}

export type InMemoryInteractionCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found';

export class InMemoryInteractionCoordinationError extends Error {
  constructor(
    readonly code: InMemoryInteractionCoordinationErrorCode,
    message: string,
  ) {
    super(message);
    this.name = InMemoryInteractionCoordinationError.name;
  }
}

function activity(
  status: HandoffStatus,
  summary: string,
  now: () => Date,
): InteractionCoordinationActivity {
  return { occurredAt: now().toISOString(), status, summary };
}

function copyActivity(entry: InteractionCoordinationActivity): InteractionCoordinationActivity {
  return { ...entry };
}

function snapshotHandoff(handoff: Handoff): Handoff {
  return Object.freeze({
    handoffId: handoff.handoffId,
    targetAgentId: handoff.targetAgentId,
    request: Object.freeze({ ...handoff.request }),
    scope: Object.freeze({ ...handoff.scope }),
  });
}
