/**
 * VoiceboxManager — manages voicemail boxes, message storage, and MWI.
 *
 * Each voicebox corresponds to a device/extension. Messages are stored
 * as WAV files with JSON metadata in .nogit/voicemail/{boxId}/.
 *
 * Supports:
 * - Per-box configurable TTS greetings (text + voice) or uploaded WAV
 * - Message CRUD: save, list, mark heard, delete
 * - Unheard count for MWI (Message Waiting Indicator)
 * - Storage limit (max messages per box)
 */

import fs from 'node:fs';
import path from 'node:path';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface IVoiceboxConfig {
  /** Unique ID — typically matches device ID or extension. */
  id: string;
  /** Whether this voicebox is active. */
  enabled: boolean;
  /** Custom TTS greeting text (overrides default). */
  greetingText?: string;
  /** Kokoro TTS voice ID for the greeting (default 'af_bella'). */
  greetingVoice?: string;
  /** Path to uploaded WAV greeting (overrides TTS). */
  greetingWavPath?: string;
  /** Seconds to wait before routing to voicemail. Defaults to 25 when
   *  absent — both the config loader and `VoiceboxManager.init` apply
   *  the default via `??=`. */
  noAnswerTimeoutSec?: number;
  /** Maximum recording duration in seconds. Defaults to 120. */
  maxRecordingSec?: number;
  /** Maximum stored messages per box. Defaults to 50. */
  maxMessages?: number;
}

export interface IVoicemailMessage {
  /** Unique message ID. */
  id: string;
  /** Which voicebox this message belongs to. */
  boxId: string;
  /** Caller's phone number. */
  callerNumber: string;
  /** Caller's display name (if available from SIP From header). */
  callerName?: string;
  /** Unix timestamp (ms) when the message was recorded. */
  timestamp: number;
  /** Duration in milliseconds. */
  durationMs: number;
  /** Relative path to the WAV file (within the box directory). */
  fileName: string;
  /** Whether the message has been listened to. */
  heard: boolean;
}

// Default greeting text when no custom text is configured.
const DEFAULT_GREETING = 'The person you are trying to reach is not available. Please leave a message after the tone.';

// ---------------------------------------------------------------------------
// VoiceboxManager
// ---------------------------------------------------------------------------

export class VoiceboxManager {
  private boxes = new Map<string, IVoiceboxConfig>();
  private basePath: string;
  private log: (msg: string) => void;

  constructor(log: (msg: string) => void) {
    this.basePath = path.join(process.cwd(), '.nogit', 'voicemail');
    this.log = log;
  }

  // -------------------------------------------------------------------------
  // Initialization
  // -------------------------------------------------------------------------

  /**
   * Load voicebox configurations from the app config.
   */
  init(voiceboxConfigs: IVoiceboxConfig[]): void {
    this.boxes.clear();

    for (const cfg of voiceboxConfigs) {
      // Apply defaults.
      cfg.noAnswerTimeoutSec ??= 25;
      cfg.maxRecordingSec ??= 120;
      cfg.maxMessages ??= 50;
      cfg.greetingVoice ??= 'af_bella';

      this.boxes.set(cfg.id, cfg);
    }

    // Ensure base directory exists.
    fs.mkdirSync(this.basePath, { recursive: true });

    this.log(`[voicebox] initialized ${this.boxes.size} voicebox(es)`);
  }

  // -------------------------------------------------------------------------
  // Box management
  // -------------------------------------------------------------------------

  /** Get config for a specific voicebox. */
  getBox(boxId: string): IVoiceboxConfig | null {
    return this.boxes.get(boxId) ?? null;
  }

  /** Get all configured voicebox IDs. */
  getBoxIds(): string[] {
    return [...this.boxes.keys()];
  }

  /** Get the greeting text for a voicebox. */
  getGreetingText(boxId: string): string {
    const box = this.boxes.get(boxId);
    return box?.greetingText || DEFAULT_GREETING;
  }

  /** Get the greeting voice for a voicebox. */
  getGreetingVoice(boxId: string): string {
    const box = this.boxes.get(boxId);
    return box?.greetingVoice || 'af_bella';
  }

  /** Check if a voicebox has a custom WAV greeting. */
  hasCustomGreetingWav(boxId: string): boolean {
    const box = this.boxes.get(boxId);
    if (!box?.greetingWavPath) return false;
    return fs.existsSync(box.greetingWavPath);
  }

  /** Get the greeting WAV path (custom or null). */
  getCustomGreetingWavPath(boxId: string): string | null {
    const box = this.boxes.get(boxId);
    if (!box?.greetingWavPath) return null;
    return fs.existsSync(box.greetingWavPath) ? box.greetingWavPath : null;
  }

  /** Get the directory path for a voicebox. */
  getBoxDir(boxId: string): string {
    return path.join(this.basePath, boxId);
  }

  // -------------------------------------------------------------------------
  // Message CRUD
  // -------------------------------------------------------------------------

  /**
   * Convenience wrapper around `saveMessage` — used by the `recording_done`
   * event handler, which has a raw recording path + caller info and needs
   * to persist metadata. Generates `id`, sets `timestamp = now`, defaults
   * `heard = false`, and normalizes `fileName` to a basename (the WAV is
   * expected to already live in the box's directory).
   */
  addMessage(
    boxId: string,
    info: {
      callerNumber: string;
      callerName?: string | null;
      fileName: string;
      durationMs: number;
    },
  ): void {
    const msg: IVoicemailMessage = {
      id: crypto.randomUUID(),
      boxId,
      callerNumber: info.callerNumber,
      callerName: info.callerName ?? undefined,
      timestamp: Date.now(),
      durationMs: info.durationMs,
      fileName: path.basename(info.fileName),
      heard: false,
    };
    this.saveMessage(msg);
  }

  /**
   * Save a new voicemail message.
   * The WAV file should already exist at the expected path.
   */
  saveMessage(msg: IVoicemailMessage): void {
    const boxDir = this.getBoxDir(msg.boxId);
    fs.mkdirSync(boxDir, { recursive: true });

    const messages = this.loadMessages(msg.boxId);
    messages.unshift(msg); // newest first

    // Enforce max messages — delete oldest.
    const box = this.boxes.get(msg.boxId);
    const maxMessages = box?.maxMessages ?? 50;
    while (messages.length > maxMessages) {
      const old = messages.pop()!;
      const oldPath = path.join(boxDir, old.fileName);
      try {
        if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
      } catch { /* best effort */ }
    }

    this.writeMessages(msg.boxId, messages);
    this.log(`[voicebox] saved message ${msg.id} in box "${msg.boxId}" (${msg.durationMs}ms from ${msg.callerNumber})`);
  }

  /**
   * List messages for a voicebox (newest first).
   */
  getMessages(boxId: string): IVoicemailMessage[] {
    return this.loadMessages(boxId);
  }

  /**
   * Get a single message by ID.
   */
  getMessage(boxId: string, messageId: string): IVoicemailMessage | null {
    const messages = this.loadMessages(boxId);
    return messages.find((m) => m.id === messageId) ?? null;
  }

  /**
   * Mark a message as heard.
   */
  markHeard(boxId: string, messageId: string): boolean {
    const messages = this.loadMessages(boxId);
    const msg = messages.find((m) => m.id === messageId);
    if (!msg) return false;

    msg.heard = true;
    this.writeMessages(boxId, messages);
    return true;
  }

  /**
   * Delete a message (both metadata and WAV file).
   */
  deleteMessage(boxId: string, messageId: string): boolean {
    const messages = this.loadMessages(boxId);
    const idx = messages.findIndex((m) => m.id === messageId);
    if (idx === -1) return false;

    const msg = messages[idx];
    const boxDir = this.getBoxDir(boxId);
    const wavPath = path.join(boxDir, msg.fileName);

    // Delete WAV file.
    try {
      if (fs.existsSync(wavPath)) fs.unlinkSync(wavPath);
    } catch { /* best effort */ }

    // Remove from list and save.
    messages.splice(idx, 1);
    this.writeMessages(boxId, messages);
    this.log(`[voicebox] deleted message ${messageId} from box "${boxId}"`);
    return true;
  }

  /**
   * Get the full file path for a message's WAV file.
   */
  getMessageAudioPath(boxId: string, messageId: string): string | null {
    const msg = this.getMessage(boxId, messageId);
    if (!msg) return null;
    const filePath = path.join(this.getBoxDir(boxId), msg.fileName);
    return fs.existsSync(filePath) ? filePath : null;
  }

  // -------------------------------------------------------------------------
  // Counts
  // -------------------------------------------------------------------------

  /** Get count of unheard messages for a voicebox. */
  getUnheardCount(boxId: string): number {
    const messages = this.loadMessages(boxId);
    return messages.filter((m) => !m.heard).length;
  }

  /** Get total message count for a voicebox. */
  getTotalCount(boxId: string): number {
    return this.loadMessages(boxId).length;
  }

  /** Get unheard counts for all voiceboxes. */
  getAllUnheardCounts(): Record<string, number> {
    const counts: Record<string, number> = {};
    for (const boxId of this.boxes.keys()) {
      counts[boxId] = this.getUnheardCount(boxId);
    }
    return counts;
  }

  // -------------------------------------------------------------------------
  // Greeting management
  // -------------------------------------------------------------------------

  /**
   * Save a custom greeting WAV file for a voicebox.
   */
  saveCustomGreeting(boxId: string, wavData: Buffer): string {
    const boxDir = this.getBoxDir(boxId);
    fs.mkdirSync(boxDir, { recursive: true });
    const greetingPath = path.join(boxDir, 'greeting.wav');
    fs.writeFileSync(greetingPath, wavData);
    this.log(`[voicebox] saved custom greeting for box "${boxId}"`);
    return greetingPath;
  }

  /**
   * Delete the custom greeting for a voicebox (falls back to TTS).
   */
  deleteCustomGreeting(boxId: string): void {
    const boxDir = this.getBoxDir(boxId);
    const greetingPath = path.join(boxDir, 'greeting.wav');
    try {
      if (fs.existsSync(greetingPath)) fs.unlinkSync(greetingPath);
    } catch { /* best effort */ }
  }

  // -------------------------------------------------------------------------
  // Internal: JSON persistence
  // -------------------------------------------------------------------------

  private messagesPath(boxId: string): string {
    return path.join(this.getBoxDir(boxId), 'messages.json');
  }

  private loadMessages(boxId: string): IVoicemailMessage[] {
    const filePath = this.messagesPath(boxId);
    try {
      if (!fs.existsSync(filePath)) return [];
      const raw = fs.readFileSync(filePath, 'utf8');
      return JSON.parse(raw) as IVoicemailMessage[];
    } catch {
      return [];
    }
  }

  private writeMessages(boxId: string, messages: IVoicemailMessage[]): void {
    const boxDir = this.getBoxDir(boxId);
    fs.mkdirSync(boxDir, { recursive: true });
    fs.writeFileSync(this.messagesPath(boxId), JSON.stringify(messages, null, 2), 'utf8');
  }
}
