import { createRouter } from '../../create-router';
import { readServerSettings, writeServerSettings } from 'officerdb';

type TtsConfig = {
  provider: 'openai' | 'elevenlabs';
  url: string;
  apiKey?: string;
  model: string;
  voice: string;
};

function maskSecret(value: string | undefined): string | undefined {
  if (!value || value.length < 8) return value ? '****' : undefined;
  return value.slice(0, 4) + '****' + value.slice(-4);
}

export async function readTtsConfig(): Promise<TtsConfig | undefined> {
  const settings = await readServerSettings();
  return settings.tts as TtsConfig | undefined;
}

export const ttsRouter = createRouter();

ttsRouter.get('/', async (ctx) => {
  const tts = await readTtsConfig();
  if (!tts) return ctx.json(null);
  return ctx.json({ ...tts, apiKey: maskSecret(tts.apiKey) });
});

ttsRouter.put('/', async (ctx) => {
  const body = await ctx.req.json<TtsConfig>();
  const settings = await readServerSettings();

  const existing: TtsConfig | undefined = settings.tts as TtsConfig | undefined;
  if (existing && body.apiKey?.includes('****')) {
    body.apiKey = existing.apiKey;
  }

  settings.tts = body;
  await writeServerSettings(settings);
  return ctx.json({ success: true });
});

// OpenAI-compatible TTS servers disagree on this payload: some list voices as plain strings, others
// (Kokoro among them) as objects like { id, name }. Everything downstream — the API contract and the
// voice <Select> — expects strings, and an object reaching the picker crashes the settings page, so
// flatten to ids here and drop anything unrecognisable.
const normalizeVoices = (raw: unknown): string[] => {
  if (!Array.isArray(raw)) return [];
  return raw
    .map((entry) => {
      if (typeof entry === 'string') return entry;
      if (entry && typeof entry === 'object') {
        const { id, name, voice_id: voiceId } = entry as Record<string, unknown>;
        for (const candidate of [id, voiceId, name]) if (typeof candidate === 'string' && candidate) return candidate;
      }
      return null;
    })
    .filter((voice): voice is string => voice !== null);
};

ttsRouter.post('/voices', async (ctx) => {
  const body = await ctx.req.json<{ provider: string; url?: string; apiKey?: string; model?: string }>();

  try {
    if (body.provider === 'elevenlabs') {
      if (!body.apiKey) return ctx.json({ error: 'API key required' }, 400);
      const res = await fetch('https://api.elevenlabs.io/v1/voices', {
        headers: { 'xi-api-key': body.apiKey },
      });
      if (!res.ok) return ctx.json({ error: `ElevenLabs error: ${res.status}` }, 500);
      const json = (await res.json().catch(() => null)) as { voices?: unknown } | null;
      return ctx.json({ voices: normalizeVoices(json?.voices) });
    }

    // OpenAI-compatible: try local server first, fallback to HuggingFace
    if (!body.url) return ctx.json({ error: 'URL required' }, 400);
    const headers: Record<string, string> = {};
    if (body.apiKey) headers['Authorization'] = `Bearer ${body.apiKey}`;

    // Try /v1/audio/voices on the local server
    const localRes = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/voices`, { headers }).catch(() => null);
    if (localRes?.ok) {
      const json = (await localRes.json().catch(() => null)) as { voices?: unknown } | null;
      const voices = normalizeVoices(json?.voices);
      if (voices.length > 0) return ctx.json({ voices });
    }

    // Fallback: get model repo from /v1/models, then list voices from HuggingFace
    const modelsRes = await fetch(`${body.url.replace(/\/+$/, '')}/v1/models`, { headers }).catch(() => null);
    if (modelsRes?.ok) {
      const modelsJson = (await modelsRes.json()) as { data?: { id: string }[] };
      const repoId = modelsJson.data?.[0]?.id;
      if (repoId) {
        const result = await fetchHuggingFaceVoices(repoId);
        if (result.flat.length > 0) return ctx.json({ voices: result.flat, groups: result.groups });
      }
    }

    // Last resort: try model field as HuggingFace repo ID directly
    if (body.model && body.model.includes('/')) {
      const result = await fetchHuggingFaceVoices(body.model);
      if (result.flat.length > 0) return ctx.json({ voices: result.flat, groups: result.groups });
    }

    return ctx.json({ error: 'Could not fetch voices from server or HuggingFace' }, 500);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return ctx.json({ error: message }, 500);
  }
});

const VOICE_GROUP_LABELS: Record<string, string> = {
  af: 'American Female',
  am: 'American Male',
  bf: 'British Female',
  bm: 'British Male',
  ef: 'Spanish Female',
  em: 'Spanish Male',
  ff: 'French Female',
  hf: 'Hindi Female',
  hm: 'Hindi Male',
  if: 'Italian Female',
  im: 'Italian Male',
  jf: 'Japanese Female',
  jm: 'Japanese Male',
  pf: 'Brazilian Portuguese Female',
  pm: 'Brazilian Portuguese Male',
  zf: 'Mandarin Chinese Female',
  zm: 'Mandarin Chinese Male',
};

type VoiceGroup = { label: string; voices: string[] };

async function fetchHuggingFaceVoices(repoId: string): Promise<{ flat: string[]; groups: VoiceGroup[] }> {
  const res = await fetch(`https://huggingface.co/api/models/${repoId}/tree/main/voices`);
  if (!res.ok) return { flat: [], groups: [] };
  const files = (await res.json()) as { path: string; type: string }[];
  const names = new Set<string>();
  for (const f of files) {
    if (f.type !== 'file') continue;
    const name = f.path.replace('voices/', '').replace(/\.(pt|safetensors)$/, '');
    names.add(name);
  }
  const sorted = [...names].sort();
  const groupMap = new Map<string, string[]>();
  for (const name of sorted) {
    const prefix = name.slice(0, 2);
    if (!groupMap.has(prefix)) groupMap.set(prefix, []);
    groupMap.get(prefix)!.push(name);
  }
  const groups: VoiceGroup[] = [];
  for (const [prefix, voices] of groupMap) {
    groups.push({ label: VOICE_GROUP_LABELS[prefix] ?? prefix, voices });
  }
  return { flat: sorted, groups };
}

ttsRouter.post('/test', async (ctx) => {
  const body = await ctx.req.json<TtsConfig>();

  const settings = await readServerSettings();
  const saved: TtsConfig | undefined = settings.tts as TtsConfig | undefined;
  if (saved && body.apiKey?.includes('****')) {
    body.apiKey = saved.apiKey;
  }

  try {
    if (body.provider === 'elevenlabs') {
      if (!body.apiKey) return ctx.json({ error: 'API key required for ElevenLabs' }, 400);
      const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(body.voice)}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'xi-api-key': body.apiKey,
        },
        body: JSON.stringify({
          text: 'This is a test of the text to speech system.',
          model_id: body.model,
        }),
      });
      if (!res.ok) {
        const text = await res.text().catch(() => '');
        return ctx.json({ error: `ElevenLabs error: ${res.status} ${text}` }, 500);
      }
      const buffer = await res.arrayBuffer();
      return new Response(buffer, { headers: { 'Content-Type': 'audio/mpeg' } });
    }

    // OpenAI-compatible (Kokoro, OpenAI, etc.)
    const headers: Record<string, string> = { 'Content-Type': 'application/json' };
    if (body.apiKey) headers['Authorization'] = `Bearer ${body.apiKey}`;

    const res = await fetch(`${body.url.replace(/\/+$/, '')}/v1/audio/speech`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        model: body.model,
        input: 'This is a test of the text to speech system.',
        voice: body.voice,
        response_format: 'mp3',
      }),
    });
    if (!res.ok) {
      const text = await res.text().catch(() => '');
      return ctx.json({ error: `TTS error: ${res.status} ${text}` }, 500);
    }
    const buffer = await res.arrayBuffer();
    return new Response(buffer, { headers: { 'Content-Type': 'audio/mpeg' } });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return ctx.json({ error: message }, 500);
  }
});
