/**
 * VoiceboxManager — manages voicemail boxes, message metadata, and audio objects.
 */

import fs from 'node:fs';
import * as fsPromises from 'node:fs/promises';
import path from 'node:path';

import type { SiprouterStorage } from './storage.ts';

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 cached uploaded WAV greeting (overrides TTS). */
  greetingWavPath?: string;
  /** Seconds to wait before routing to voicemail. */
  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;
  /** Display file name. */
  fileName: string;
  /** SmartBucket object key for the WAV payload. */
  objectKey?: string;
  /** Whether the message has been listened to. */
  heard: boolean;
}

const DEFAULT_GREETING = 'The person you are trying to reach is not available. Please leave a message after the tone.';

export class VoiceboxManager {
  private boxes = new Map<string, IVoiceboxConfig>();
  private messagesByBox = new Map<string, IVoicemailMessage[]>();
  private readonly basePath: string;
  private readonly log: (msg: string) => void;
  private readonly storage: SiprouterStorage;

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

  async init(voiceboxConfigs: IVoiceboxConfig[]): Promise<void> {
    this.boxes.clear();

    for (const cfg of voiceboxConfigs) {
      cfg.noAnswerTimeoutSec ??= 25;
      cfg.maxRecordingSec ??= 120;
      cfg.maxMessages ??= 50;
      cfg.greetingVoice ??= 'af_bella';
      this.boxes.set(cfg.id, cfg);
      this.messagesByBox.set(cfg.id, await this.loadMessages(cfg.id));
    }

    await fsPromises.mkdir(this.basePath, { recursive: true });
    this.log(`[voicebox] initialized ${this.boxes.size} voicebox(es)`);
  }

  getBox(boxId: string): IVoiceboxConfig | null {
    return this.boxes.get(boxId) ?? null;
  }

  getBoxIds(): string[] {
    return [...this.boxes.keys()];
  }

  getGreetingText(boxId: string): string {
    const box = this.boxes.get(boxId);
    return box?.greetingText || DEFAULT_GREETING;
  }

  getGreetingVoice(boxId: string): string {
    const box = this.boxes.get(boxId);
    return box?.greetingVoice || 'af_bella';
  }

  hasCustomGreetingWav(boxId: string): boolean {
    const box = this.boxes.get(boxId);
    if (!box?.greetingWavPath) return false;
    return fs.existsSync(box.greetingWavPath);
  }

  getCustomGreetingWavPath(boxId: string): string | null {
    const box = this.boxes.get(boxId);
    if (!box?.greetingWavPath) return null;
    return fs.existsSync(box.greetingWavPath) ? box.greetingWavPath : null;
  }

  getBoxDir(boxId: string): string {
    return path.join(this.basePath, boxId);
  }

  async addMessage(
    boxId: string,
    info: {
      callerNumber: string;
      callerName?: string | null;
      fileName: string;
      durationMs: number;
    },
  ): Promise<void> {
    const id = crypto.randomUUID();
    const localPath = path.isAbsolute(info.fileName) ? info.fileName : path.join(process.cwd(), info.fileName);
    const objectKey = await this.storage.putFileObject(`voicemail/${boxId}/${id}.wav`, localPath);

    const msg: IVoicemailMessage = {
      id,
      boxId,
      callerNumber: info.callerNumber,
      callerName: info.callerName ?? undefined,
      timestamp: Date.now(),
      durationMs: info.durationMs,
      fileName: path.basename(localPath),
      objectKey,
      heard: false,
    };

    const messages = this.getMessages(boxId);
    messages.unshift(msg);
    await this.enforceLimit(boxId, messages);
    await this.writeMessages(boxId, messages);

    await fsPromises.rm(localPath, { force: true }).catch(() => {});
    this.log(`[voicebox] saved message ${msg.id} in box "${msg.boxId}" (${msg.durationMs}ms from ${msg.callerNumber})`);
  }

  getMessages(boxId: string): IVoicemailMessage[] {
    return [...(this.messagesByBox.get(boxId) || [])];
  }

  getMessage(boxId: string, messageId: string): IVoicemailMessage | null {
    const messages = this.messagesByBox.get(boxId) || [];
    return messages.find((m) => m.id === messageId) ?? null;
  }

  async markHeard(boxId: string, messageId: string): Promise<boolean> {
    const messages = this.messagesByBox.get(boxId) || [];
    const msg = messages.find((m) => m.id === messageId);
    if (!msg) return false;

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

  async deleteMessage(boxId: string, messageId: string): Promise<boolean> {
    const messages = this.messagesByBox.get(boxId) || [];
    const idx = messages.findIndex((m) => m.id === messageId);
    if (idx === -1) return false;

    const msg = messages[idx];
    await this.storage.removeObject(msg.objectKey);
    if (!msg.objectKey) {
      await fsPromises.rm(path.join(this.getBoxDir(boxId), msg.fileName), { force: true }).catch(() => {});
    }

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

  async getMessageAudioPath(boxId: string, messageId: string): Promise<string | null> {
    const msg = this.getMessage(boxId, messageId);
    if (!msg) return null;
    if (msg.objectKey) {
      return await this.storage.getObjectAsCachedFile(msg.objectKey, msg.fileName);
    }
    const filePath = path.join(this.getBoxDir(boxId), msg.fileName);
    return fs.existsSync(filePath) ? filePath : null;
  }

  getUnheardCount(boxId: string): number {
    const messages = this.messagesByBox.get(boxId) || [];
    return messages.filter((m) => !m.heard).length;
  }

  getTotalCount(boxId: string): number {
    return (this.messagesByBox.get(boxId) || []).length;
  }

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

  async saveCustomGreeting(boxId: string, wavData: Buffer): Promise<string> {
    const objectKey = await this.storage.putBufferObject(`voicemail/${boxId}/greeting.wav`, wavData);
    const greetingPath = await this.storage.getObjectAsCachedFile(objectKey, `voicemail-${boxId}-greeting.wav`);
    this.log(`[voicebox] saved custom greeting for box "${boxId}"`);
    return greetingPath || '';
  }

  async deleteCustomGreeting(boxId: string): Promise<void> {
    await this.storage.removeObject(`voicemail/${boxId}/greeting.wav`);
  }

  private async enforceLimit(boxId: string, messages: IVoicemailMessage[]): Promise<void> {
    const box = this.boxes.get(boxId);
    const maxMessages = box?.maxMessages ?? 50;
    while (messages.length > maxMessages) {
      const old = messages.pop()!;
      await this.storage.removeObject(old.objectKey);
      if (!old.objectKey) {
        await fsPromises.rm(path.join(this.getBoxDir(boxId), old.fileName), { force: true }).catch(() => {});
      }
    }
  }

  private async loadMessages(boxId: string): Promise<IVoicemailMessage[]> {
    const storedMessages = await this.storage.getVoicemailMessages(boxId);
    if (storedMessages.length) return await this.ensureMessageObjects(boxId, storedMessages);

    const filePath = path.join(this.getBoxDir(boxId), 'messages.json');
    try {
      if (!fs.existsSync(filePath)) return [];
      const raw = await fsPromises.readFile(filePath, 'utf8');
      const legacyMessages = await this.ensureMessageObjects(boxId, JSON.parse(raw) as IVoicemailMessage[]);
      await this.storage.writeVoicemailMessages(boxId, legacyMessages);
      return legacyMessages;
    } catch {
      return [];
    }
  }

  private async ensureMessageObjects(boxId: string, messages: IVoicemailMessage[]): Promise<IVoicemailMessage[]> {
    let changed = false;

    for (const msg of messages) {
      if (!msg.id) {
        msg.id = crypto.randomUUID();
        changed = true;
      }
      if (msg.objectKey) continue;

      const localPath = path.isAbsolute(msg.fileName) ? msg.fileName : path.join(this.getBoxDir(boxId), msg.fileName);
      if (!fs.existsSync(localPath)) continue;

      const extension = path.extname(localPath) || '.wav';
      msg.objectKey = await this.storage.putFileObject(`voicemail/${boxId}/${msg.id}${extension}`, localPath);
      msg.fileName = path.basename(localPath);
      changed = true;
    }

    if (changed) {
      await this.storage.writeVoicemailMessages(boxId, messages);
      this.log(`[voicebox] migrated legacy messages for box "${boxId}" to smartbucket`);
    }

    return messages;
  }

  private async writeMessages(boxId: string, messages: IVoicemailMessage[]): Promise<void> {
    this.messagesByBox.set(boxId, [...messages]);
    await this.storage.writeVoicemailMessages(boxId, messages);
  }
}
