import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type { EventEmitter } from 'node:events';

/**
 * A pending human interaction, typically from an escalation.
 */
export interface PendingInteraction {
  /** Unique interaction ID */
  id: string;
  /** ID of the related issue */
  issueId: string;
  /** Interaction type */
  type: 'needs-human';
  /** Reasons for escalation */
  reasons: string[];
  /** Context for the human */
  context: {
    issueTitle: string;
    issueDescription: string | null;
    specPath: string | null;
    planPath: string | null;
    relatedFiles: string[];
    /** Enriched spec from intelligence pipeline, if available */
    enrichedSpec?: {
      intent: string;
      summary: string;
      affectedSystems: unknown[];
      unknowns: string[];
      ambiguities: string[];
      riskSignals: string[];
    };
    /** Complexity score from intelligence pipeline, if available */
    complexityScore?: {
      overall: number;
      confidence: number;
      riskLevel: string;
      blastRadius: {
        services: number;
        modules: number;
        filesEstimated: number;
        testFilesAffected: number;
      };
      dimensions: {
        structural: number;
        semantic: number;
        historical: number;
      };
      reasoning: string[];
      recommendedRoute: string;
    };
  };
  /** ISO timestamp of creation */
  createdAt: string;
  /** Current status */
  status: 'pending' | 'claimed' | 'resolved';
}

/**
 * Persistent queue of pending human interactions.
 * Each interaction is stored as a separate JSON file in the configured directory.
 */
export class InteractionQueue {
  private dir: string;
  private pushListeners: Array<(interaction: PendingInteraction) => void> = [];
  private emitter: EventEmitter | null;

  /**
   * @param dir - Directory path for storing interaction JSON files
   * @param emitter - Optional event bus that receives `interaction.created`
   *   and `interaction.resolved` events. When omitted, the queue behaves as
   *   it did pre-Phase-2 (no emission). Phase 2 Task 8 wires the
   *   orchestrator (itself an EventEmitter) in as the bus so the SSE
   *   handler (`GET /api/v1/events`) can fan these out to clients.
   */
  constructor(dir: string, emitter?: EventEmitter) {
    this.dir = dir;
    this.emitter = emitter ?? null;
  }

  /**
   * Register a listener that fires after each push.
   */
  onPush(listener: (interaction: PendingInteraction) => void): void {
    this.pushListeners.push(listener);
  }

  /**
   * Push a new interaction to the queue.
   * If a pending interaction for the same issueId already exists, it is
   * replaced (upsert) so the directory never accumulates duplicates.
   */
  async push(interaction: PendingInteraction): Promise<void> {
    await fs.mkdir(this.dir, { recursive: true });

    // Remove any existing pending interaction for the same issueId
    const existing = await this.list();
    for (const prev of existing) {
      if (prev.issueId === interaction.issueId && prev.status === 'pending') {
        const oldPath = path.join(this.dir, `${prev.id}.json`);
        await fs.unlink(oldPath).catch(() => {});
      }
    }

    const filePath = path.join(this.dir, `${interaction.id}.json`);
    await fs.writeFile(filePath, JSON.stringify(interaction, null, 2), 'utf-8');
    for (const listener of this.pushListeners) {
      listener(interaction);
    }
    // Phase 2 Task 8: fan the new interaction out onto the orchestrator's
    // event bus so SSE subscribers (`GET /api/v1/events`) receive it.
    //
    // Phase 2 review-fix cycle 1 (IMP-1): emit an allow-listed metadata
    // payload only. The read-telemetry scope (required for SSE) historically
    // means "observability metadata", not "issue corpus" — emitting the full
    // PendingInteraction here would leak issueDescription, enrichedSpec,
    // relatedFiles, and complexityScore to every dashboard token. Matches
    // the allow-list pattern used by `interaction.resolved` below.
    //
    // Bridges that need full context can fetch it via the authenticated
    // GET /api/v1/interactions/{id} read path under a tighter scope.
    this.emitter?.emit('interaction.created', {
      id: interaction.id,
      issueId: interaction.issueId,
      type: interaction.type,
      status: interaction.status,
      createdAt: interaction.createdAt,
    });
  }

  /**
   * List all interactions (regardless of status).
   */
  async list(): Promise<PendingInteraction[]> {
    try {
      const files = await fs.readdir(this.dir);
      const jsonFiles = files.filter((f) => f.endsWith('.json'));
      const interactions: PendingInteraction[] = [];

      for (const file of jsonFiles) {
        const filePath = path.join(this.dir, file);
        const raw = await fs.readFile(filePath, 'utf-8');
        interactions.push(JSON.parse(raw) as PendingInteraction);
      }

      return interactions;
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
        return [];
      }
      throw err;
    }
  }

  /**
   * List only pending interactions.
   */
  async listPending(): Promise<PendingInteraction[]> {
    const all = await this.list();
    return all.filter((i) => i.status === 'pending');
  }

  /**
   * Update the status of an interaction.
   */
  async updateStatus(id: string, status: PendingInteraction['status']): Promise<void> {
    const filePath = path.join(this.dir, `${id}.json`);
    let raw: string;
    try {
      raw = await fs.readFile(filePath, 'utf-8');
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
        throw new Error(`Interaction ${id} not found`, { cause: err });
      }
      throw err;
    }
    const interaction = JSON.parse(raw) as PendingInteraction;
    interaction.status = status;
    await fs.writeFile(filePath, JSON.stringify(interaction, null, 2), 'utf-8');
    // Phase 2 Task 8: emit `interaction.resolved` on resolution so SSE
    // subscribers can clear stale UI without polling.
    if (status === 'resolved') {
      this.emitter?.emit('interaction.resolved', {
        id,
        status: 'resolved',
        resolvedAt: new Date().toISOString(),
      });
    }
  }
}
