import type express from 'express';
import { spawn, type ChildProcess } from 'node:child_process';
import { PassThrough } from 'node:stream';
import {
  joinVoiceChannel, createAudioPlayer, createAudioResource,
  VoiceConnectionStatus, StreamType, getVoiceConnection, entersState,
} from '@discordjs/voice';
import type { VoiceBasedChannel } from 'discord.js';
import { ChannelType } from 'discord.js';
import type { Plugin, PluginContext } from '../../core/plugin.js';
import { sseBroadcast } from '../../core/sse.js';
import { getState, setState } from '../../core/persistence.js';
import {
  fetchPlaces, fetchPlaceChannels, searchStations,
  resolveStreamUrl, getPlacesCount,
} from './api.js';

// ── Tile proxy cache (in-memory) ──
const TILE_CDN = 'https://rg-tiles.b-cdn.net';
const tileCache = new Map<string, { data: Buffer; contentType: string }>();
const TILE_CACHE_MAX = 500; // max cached tiles (~15-25 MB)

// ── Voice connection tracking ──
const connectedSince = new Map<string, string>();
let voiceStatsInterval: ReturnType<typeof setInterval> | undefined;

// ── Types ──
interface GuildRadioState {
  stationId: string;
  stationName: string;
  placeName: string;
  country: string;
  streamUrl: string;
  startedAt: string;
  ffmpeg: ChildProcess;
  player: ReturnType<typeof createAudioPlayer>;
  resource: ReturnType<typeof createAudioResource>;
  channelId: string;
  channelName: string;
  volume: number;
  prebufferTimeout?: ReturnType<typeof setTimeout>;
}

interface Favorite {
  stationId: string;
  stationName: string;
  placeName: string;
  country: string;
  placeId: string;
}

// ── State ──
const guildRadioState = new Map<string, GuildRadioState>();

function getVolume(guildId: string): number {
  const vols = getState<Record<string, number>>('radio_volumes', {});
  return vols[guildId] ?? 0.5;
}

function setVolume(guildId: string, vol: number): void {
  const vols = getState<Record<string, number>>('radio_volumes', {});
  vols[guildId] = vol;
  setState('radio_volumes', vols);
}

function getFavorites(): Favorite[] {
  return getState<Favorite[]>('radio_favorites', []);
}

function setFavorites(favs: Favorite[]): void {
  setState('radio_favorites', favs);
}

// ── Streaming ──

/** Stop only audio (ffmpeg + player), keep voice connection alive */
function stopAudio(guildId: string): void {
  const state = guildRadioState.get(guildId);
  if (!state) return;
  if (state.prebufferTimeout) clearTimeout(state.prebufferTimeout);
  try { state.ffmpeg.kill('SIGKILL'); } catch {}
  try { state.player.stop(true); } catch {}
  guildRadioState.delete(guildId);
}

/** Full stop: audio + voice connection + broadcast */
function stopStream(guildId: string): void {
  stopAudio(guildId);
  try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {}
  connectedSince.delete(guildId);
  broadcastState(guildId);
  console.log(`[Radio] Stopped stream in guild ${guildId}`);
}

async function startStream(
  ctx: PluginContext, guildId: string, voiceChannelId: string,
  stationId: string, stationName: string, placeName: string, country: string,
): Promise<{ ok: boolean; error?: string }> {
  // Check if we can reuse the existing voice connection
  const prev = guildRadioState.get(guildId);
  const existingConn = getVoiceConnection(guildId, 'radio');
  const sameChannel = prev && existingConn && prev.channelId === voiceChannelId
    && existingConn.state.status === VoiceConnectionStatus.Ready;

  // Stop only audio (keep connection if same channel)
  stopAudio(guildId);

  // Stream-URL auflösen
  const streamUrl = await resolveStreamUrl(stationId);
  if (!streamUrl) {
    // No stream → full disconnect if nothing playing
    if (!sameChannel) try { existingConn?.destroy(); } catch {}
    broadcastState(guildId);
    return { ok: false, error: 'Stream-URL konnte nicht aufgelöst werden' };
  }

  // Guild + Channel finden
  const guild = ctx.client.guilds.cache.get(guildId);
  if (!guild) return { ok: false, error: 'Guild nicht gefunden' };
  const channel = guild.channels.cache.get(voiceChannelId) as VoiceBasedChannel | undefined;
  if (!channel) return { ok: false, error: 'Voice Channel nicht gefunden' };

  // Reuse or create voice connection
  let connection = sameChannel ? existingConn! : null;
  if (!connection) {
    // Different channel or no connection → full join
    try { existingConn?.destroy(); } catch {}
    connectedSince.delete(guildId);

    connection = joinVoiceChannel({
      channelId: voiceChannelId,
      guildId,
      adapterCreator: guild.voiceAdapterCreator,
      selfDeaf: true,
      group: 'radio',
    });

    try {
      await entersState(connection, VoiceConnectionStatus.Ready, 10_000);
    } catch {
      connection.destroy();
      return { ok: false, error: 'Voice-Verbindung fehlgeschlagen' };
    }
  }

  // ffmpeg spawnen – Radio-Stream → raw PCM
  const ffmpeg = spawn('ffmpeg', [
    '-reconnect', '1',
    '-reconnect_streamed', '1',
    '-reconnect_delay_max', '5',
    '-i', streamUrl,
    '-f', 's16le',
    '-ar', '48000',
    '-ac', '2',
    '-loglevel', 'error',
    'pipe:1',
  ], { stdio: ['ignore', 'pipe', 'pipe'] });

  ffmpeg.stderr?.on('data', (d: Buffer) => {
    const msg = d.toString().trim();
    if (msg) console.error(`[Radio:ffmpeg] ${msg}`);
  });

  ffmpeg.on('close', (code) => {
    if (guildRadioState.has(guildId)) {
      console.log(`[Radio] ffmpeg exited (code ${code}), cleaning up`);
      guildRadioState.delete(guildId);
      try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {}
      connectedSince.delete(guildId);
      broadcastState(guildId);
    }
  });

  // ── Pre-buffer: 2 Sekunden PCM puffern bevor Wiedergabe startet ──
  // 48kHz × 2ch × 2byte = 192.000 B/s → 384 KB ≈ 2s
  const PRE_BUFFER_BYTES = 384 * 1024;
  const bufferedStream = new PassThrough({ highWaterMark: PRE_BUFFER_BYTES });
  let buffered = 0;
  let prebufferDone = false;

  ffmpeg.stdout!.on('data', (chunk: Buffer) => {
    bufferedStream.write(chunk);
    if (!prebufferDone) {
      buffered += chunk.length;
      if (buffered >= PRE_BUFFER_BYTES) {
        prebufferDone = true;
        startPlayback();
      }
    }
  });
  ffmpeg.stdout!.on('end', () => bufferedStream.end());
  ffmpeg.stdout!.on('error', (err) => bufferedStream.destroy(err));

  // AudioResource + Player (inlineVolume für Lautstärkeregelung)
  const vol = getVolume(guildId);
  const resource = createAudioResource(bufferedStream, {
    inputType: StreamType.Raw,
    inlineVolume: true,
  });
  resource.volume?.setVolume(vol);

  const player = createAudioPlayer();
  connection.subscribe(player);

  // Starte Wiedergabe erst wenn Pre-Buffer voll ist (oder nach 3s Timeout)
  function startPlayback() {
    if (player.state.status !== 'idle') return; // Already started
    player.play(resource);
    console.log(`[Radio] Pre-buffer filled (${Math.round(buffered / 1024)} KB), starting playback`);
  }

  // Fallback: starte nach 3s auch bei langsamen Streams
  const prebufferTimeout = setTimeout(() => {
    if (!prebufferDone) {
      prebufferDone = true;
      startPlayback();
      console.log(`[Radio] Pre-buffer timeout, starting with ${Math.round(buffered / 1024)} KB`);
    }
  }, 3000);

  player.on('error', (err) => {
    console.error(`[Radio] Player error:`, err.message);
    stopStream(guildId);
  });

  // State tracken
  const channelName = 'name' in channel ? (channel as any).name : voiceChannelId;
  guildRadioState.set(guildId, {
    stationId, stationName, placeName, country,
    streamUrl, startedAt: new Date().toISOString(),
    ffmpeg, player, resource, channelId: voiceChannelId, channelName,
    volume: vol, prebufferTimeout,
  });

  broadcastState(guildId);
  console.log(`[Radio] ${sameChannel ? '\u{1F504}' : '\u25B6'} "${stationName}" (${placeName}, ${country}) → ${guild.name}/#${channelName}`);
  return { ok: true };
}

function broadcastState(guildId: string): void {
  const state = guildRadioState.get(guildId);
  sseBroadcast({
    type: 'radio',
    plugin: 'radio',
    guildId,
    volume: state?.volume ?? getVolume(guildId),
    playing: state ? {
      stationId: state.stationId,
      stationName: state.stationName,
      placeName: state.placeName,
      country: state.country,
      startedAt: state.startedAt,
      channelName: state.channelName,
    } : null,
  });
}

// ── Plugin ──
const radioPlugin: Plugin = {
  name: 'radio',
  version: '1.0.0',
  description: 'World Radio – Radiosender aus aller Welt streamen',

  async init() {
    fetchPlaces()
      .then(p => console.log(`[Radio] ${p.length} Orte gecached`))
      .catch(e => console.error('[Radio] Places-Fetch fehlgeschlagen:', e));
  },

  async onReady(ctx) {
    console.log(`[Radio] Discord ready – ${ctx.client.guilds.cache.size} Guild(s)`);

    // Voice stats broadcast every 5s
    voiceStatsInterval = setInterval(() => {
      if (guildRadioState.size === 0) return;
      for (const [gId, st] of guildRadioState) {
        const conn = getVoiceConnection(gId, 'radio');
        const status = conn?.state?.status ?? 'unknown';
        if (status === 'ready' && !connectedSince.has(gId)) connectedSince.set(gId, new Date().toISOString());
        const ch = ctx.client.channels.cache.get(st.channelId);
        sseBroadcast({
          type: 'radio_voicestats', plugin: 'radio', guildId: gId,
          voicePing: (conn?.ping as any)?.ws ?? null,
          gatewayPing: ctx.client.ws.ping,
          status,
          channelName: ch && 'name' in ch ? (ch as any).name : null,
          connectedSince: connectedSince.get(gId) ?? null,
        });
      }
    }, 5_000);
  },

  registerRoutes(app: express.Application, ctx: PluginContext) {

    // ── Alle Orte (für Globe) ──
    app.get('/api/radio/places', async (_req, res) => {
      try {
        const places = await fetchPlaces();
        res.json(places);
      } catch (e: any) {
        res.status(502).json({ error: e.message });
      }
    });

    // ── Sender an einem Ort ──
    app.get('/api/radio/place/:id/channels', async (req, res) => {
      try {
        const channels = await fetchPlaceChannels(req.params.id);
        res.json(channels);
      } catch (e: any) {
        res.status(502).json({ error: e.message });
      }
    });

    // ── Suche ──
    app.get('/api/radio/search', async (req, res) => {
      const q = (req.query.q as string) ?? '';
      if (!q.trim()) return res.json([]);
      try {
        const results = await searchStations(q);
        res.json(results);
      } catch (e: any) {
        res.status(502).json({ error: e.message });
      }
    });

    // ── Tile Proxy (Radio Garden CDN requires Referer) ──
    app.get('/api/radio/tile/:z/:x/:y', async (req, res) => {
      const { z, x, y } = req.params;
      const key = `${z}/${x}/${y}`;

      // Serve from cache
      const cached = tileCache.get(key);
      if (cached) {
        res.set('Content-Type', cached.contentType);
        res.set('Cache-Control', 'public, max-age=86400');
        res.send(cached.data);
        return;
      }

      try {
        const resp = await fetch(`${TILE_CDN}/${key}.jpg`, {
          headers: { Referer: 'https://radio.garden/' },
        });
        if (!resp.ok) { res.status(resp.status).end(); return; }

        const buf = Buffer.from(await resp.arrayBuffer());
        const ct = resp.headers.get('content-type') ?? 'image/jpeg';

        // Cache (evict oldest if full)
        if (tileCache.size >= TILE_CACHE_MAX) {
          const first = tileCache.keys().next().value;
          if (first) tileCache.delete(first);
        }
        tileCache.set(key, { data: buf, contentType: ct });

        res.set('Content-Type', ct);
        res.set('Cache-Control', 'public, max-age=86400');
        res.send(buf);
      } catch {
        res.status(502).end();
      }
    });

    // ── Verfügbare Guilds + Voice Channels ──
    app.get('/api/radio/guilds', (_req, res) => {
      const guilds = ctx.client.guilds.cache.map(g => ({
        id: g.id,
        name: g.name,
        icon: g.iconURL({ size: 64 }),
        voiceChannels: g.channels.cache
          .filter(c => c.type === ChannelType.GuildVoice || c.type === ChannelType.GuildStageVoice)
          .map(c => ({
            id: c.id,
            name: c.name,
            members: ('members' in c)
              ? (c as VoiceBasedChannel).members.filter(m => !m.user.bot).size
              : 0,
          })),
      }));
      res.json(guilds);
    });

    // ── Play ──
    app.post('/api/radio/play', async (req, res) => {
      const { guildId, voiceChannelId, stationId, stationName, placeName, country } = req.body ?? {};
      if (!guildId || !voiceChannelId || !stationId) {
        return res.status(400).json({ error: 'guildId, voiceChannelId, stationId required' });
      }
      const result = await startStream(
        ctx, guildId, voiceChannelId, stationId,
        stationName ?? '', placeName ?? '', country ?? '',
      );
      res.json(result);
    });

    // ── Stop ──
    app.post('/api/radio/stop', (req, res) => {
      const { guildId } = req.body ?? {};
      if (!guildId) return res.status(400).json({ error: 'guildId required' });
      stopStream(guildId);
      res.json({ ok: true });
    });

    // ── Volume ──
    app.post('/api/radio/volume', (req, res) => {
      const { guildId, volume } = req.body ?? {};
      if (!guildId || volume == null) return res.status(400).json({ error: 'guildId, volume required' });
      const vol = Math.max(0, Math.min(1, Number(volume)));
      setVolume(guildId, vol);
      const state = guildRadioState.get(guildId);
      if (state) {
        state.volume = vol;
        state.resource.volume?.setVolume(vol);
      }
      sseBroadcast({ type: 'radio_volume', plugin: 'radio', guildId, volume: vol });
      res.json({ ok: true, volume: vol });
    });

    // ── Favoriten lesen ──
    app.get('/api/radio/favorites', (_req, res) => {
      res.json(getFavorites());
    });

    // ── Favorit togglen ──
    app.post('/api/radio/favorites', (req, res) => {
      const { stationId, stationName, placeName, country, placeId } = req.body ?? {};
      if (!stationId) return res.status(400).json({ error: 'stationId required' });

      const favs = getFavorites();
      const idx = favs.findIndex(f => f.stationId === stationId);
      if (idx >= 0) {
        favs.splice(idx, 1);
      } else {
        favs.push({
          stationId,
          stationName: stationName ?? '',
          placeName: placeName ?? '',
          country: country ?? '',
          placeId: placeId ?? '',
        });
      }
      setFavorites(favs);
      sseBroadcast({ type: 'radio_favorites', plugin: 'radio', favorites: favs });
      res.json({ favorites: favs });
    });

    // ── Status ──
    app.get('/api/radio/status', (_req, res) => {
      const status: Record<string, any> = {};
      for (const [gId, st] of guildRadioState) {
        status[gId] = {
          stationId: st.stationId,
          stationName: st.stationName,
          placeName: st.placeName,
          country: st.country,
          startedAt: st.startedAt,
          channelName: st.channelName,
        };
      }
      res.json(status);
    });
  },

  getSnapshot() {
    const playing: Record<string, any> = {};
    const volumes: Record<string, number> = {};
    for (const [gId, st] of guildRadioState) {
      playing[gId] = {
        stationId: st.stationId,
        stationName: st.stationName,
        placeName: st.placeName,
        country: st.country,
        startedAt: st.startedAt,
        channelName: st.channelName,
      };
      volumes[gId] = st.volume;
    }
    return {
      radio: {
        playing,
        volumes,
        favorites: getFavorites(),
        placesCount: getPlacesCount(),
      },
    };
  },

  async destroy() {
    if (voiceStatsInterval) clearInterval(voiceStatsInterval);
    for (const guildId of guildRadioState.keys()) {
      stopStream(guildId);
    }
    console.log('[Radio] Destroyed');
  },
};

export default radioPlugin;
