import type express from 'express';
import { WebSocketServer, WebSocket } from 'ws';
import crypto from 'node:crypto';
import type { Plugin, PluginContext } from '../../core/plugin.js';
import { sseBroadcast } from '../../core/sse.js';

// ── Types ──

interface QueueItem {
  url: string;
  title: string;
  addedBy: string;
}

interface HistoryItem {
  url: string;
  title: string;
  addedBy: string;
  playedAt: string; // ISO timestamp
}

interface RoomMember {
  id: string;
  name: string;
}

interface Room {
  id: string;
  name: string;
  password: string;
  hostId: string;
  createdAt: string;
  members: Map<string, RoomMember>;
  currentVideo: { url: string; title: string } | null;
  playing: boolean;
  currentTime: number;
  lastSyncAt: number;
  queue: QueueItem[];
  history: HistoryItem[];
}

interface WtClient {
  id: string;
  ws: WebSocket;
  name: string;
  roomId: string | null;
  isAlive: boolean;
}

// ── State ──

const rooms = new Map<string, Room>();
const wsClients = new Map<string, WtClient>();
const cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();

let wss: WebSocketServer | null = null;
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
let syncPulseInterval: ReturnType<typeof setInterval> | null = null;
const HEARTBEAT_MS = 5_000;
const SYNC_PULSE_MS = 2_500;
const ROOM_CLEANUP_MS = 30_000;

// ── Helpers ──

function sendTo(client: WtClient, data: Record<string, any>): void {
  if (client.ws.readyState === WebSocket.OPEN) {
    client.ws.send(JSON.stringify(data));
  }
}

function sendToRoom(roomId: string, data: Record<string, any>, excludeId?: string): void {
  const room = rooms.get(roomId);
  if (!room) return;
  for (const member of room.members.values()) {
    if (excludeId && member.id === excludeId) continue;
    const client = wsClients.get(member.id);
    if (client) sendTo(client, data);
  }
}

function getPlaybackState(room: Room): Record<string, any> {
  const currentTime = room.currentTime + (room.playing ? (Date.now() - room.lastSyncAt) / 1000 : 0);
  return {
    type: 'playback_state',
    currentVideo: room.currentVideo ? { url: room.currentVideo.url, title: room.currentVideo.title } : null,
    playing: room.playing,
    currentTime,
    updatedAt: Date.now(),
    history: room.history,
  };
}

function serializeRoom(room: Room): Record<string, any> {
  return {
    id: room.id,
    name: room.name,
    hostId: room.hostId,
    createdAt: room.createdAt,
    members: [...room.members.values()],
    currentVideo: room.currentVideo,
    playing: room.playing,
    currentTime: room.currentTime + (room.playing ? (Date.now() - room.lastSyncAt) / 1000 : 0),
    queue: room.queue,
    history: room.history,
  };
}

function getRoomList(): Record<string, any>[] {
  return [...rooms.values()].map((room) => {
    const host = room.members.get(room.hostId);
    return {
      id: room.id,
      name: room.name,
      hasPassword: room.password.length > 0,
      memberCount: room.members.size,
      hostName: host?.name ?? 'Unbekannt',
      currentVideo: room.currentVideo,
      playing: room.playing,
    };
  });
}

function broadcastRoomList(): void {
  sseBroadcast({ type: 'watch_together_status', plugin: 'watch-together', rooms: getRoomList() });
}

function scheduleRoomCleanup(roomId: string): void {
  cancelRoomCleanup(roomId);
  const timer = setTimeout(() => {
    cleanupTimers.delete(roomId);
    const room = rooms.get(roomId);
    if (room && room.members.size === 0) {
      rooms.delete(roomId);
      broadcastRoomList();
      console.log(`[WatchTogether] Raum "${room.name}" gelöscht (leer nach Timeout)`);
    }
  }, ROOM_CLEANUP_MS);
  cleanupTimers.set(roomId, timer);
}

function cancelRoomCleanup(roomId: string): void {
  const timer = cleanupTimers.get(roomId);
  if (timer) {
    clearTimeout(timer);
    cleanupTimers.delete(roomId);
  }
}

function transferHost(room: Room): void {
  const nextMember = room.members.values().next().value as RoomMember | undefined;
  if (nextMember) {
    room.hostId = nextMember.id;
    sendToRoom(room.id, { type: 'members_updated', members: [...room.members.values()], hostId: room.hostId });
    console.log(`[WatchTogether] Host in "${room.name}" übertragen an ${nextMember.name}`);
  } else {
    scheduleRoomCleanup(room.id);
  }
}

function leaveRoom(client: WtClient): void {
  if (!client.roomId) return;
  const room = rooms.get(client.roomId);
  if (!room) {
    client.roomId = null;
    return;
  }

  room.members.delete(client.id);
  const roomId = client.roomId;
  client.roomId = null;

  if (room.members.size === 0) {
    scheduleRoomCleanup(roomId);
  } else if (room.hostId === client.id) {
    transferHost(room);
  } else {
    sendToRoom(roomId, { type: 'members_updated', members: [...room.members.values()], hostId: room.hostId });
  }

  broadcastRoomList();
}

function handleDisconnect(client: WtClient): void {
  leaveRoom(client);
}

// ── WebSocket Message Handler ──

async function handleMessage(client: WtClient, msg: any): Promise<void> {
  switch (msg.type) {
    case 'create_room': {
      if (client.roomId) {
        sendTo(client, { type: 'error', code: 'ALREADY_IN_ROOM', message: 'Du bist bereits in einem Raum.' });
        return;
      }
      const roomName = String(msg.name || 'Neuer Raum').slice(0, 32);
      const password = String(msg.password ?? '').trim();
      const userName = String(msg.userName || 'Anon').slice(0, 32);
      const roomId = crypto.randomUUID();

      client.name = userName;
      client.roomId = roomId;

      const room: Room = {
        id: roomId,
        name: roomName,
        password,
        hostId: client.id,
        createdAt: new Date().toISOString(),
        members: new Map([[client.id, { id: client.id, name: userName }]]),
        currentVideo: null,
        playing: false,
        currentTime: 0,
        lastSyncAt: Date.now(),
        queue: [],
        history: [],
      };

      rooms.set(roomId, room);
      sendTo(client, { type: 'room_created', roomId, room: serializeRoom(room) });
      broadcastRoomList();
      console.log(`[WatchTogether] ${userName} hat Raum "${roomName}" erstellt (${roomId.slice(0, 8)})`);
      break;
    }

    case 'join_room': {
      if (client.roomId) {
        sendTo(client, { type: 'error', code: 'ALREADY_IN_ROOM', message: 'Du bist bereits in einem Raum. Verlasse zuerst den aktuellen Raum.' });
        return;
      }
      const room = rooms.get(msg.roomId);
      if (!room) {
        sendTo(client, { type: 'error', code: 'ROOM_NOT_FOUND', message: 'Raum nicht gefunden.' });
        return;
      }
      const joinPw = String(msg.password ?? '').trim();
      if (room.password && joinPw !== room.password) {
        sendTo(client, { type: 'error', code: 'WRONG_PASSWORD', message: 'Falsches Passwort.' });
        return;
      }

      const userName = String(msg.userName || 'Anon').slice(0, 32);
      client.name = userName;
      client.roomId = room.id;

      room.members.set(client.id, { id: client.id, name: userName });
      cancelRoomCleanup(room.id);

      sendTo(client, { type: 'room_joined', room: serializeRoom(room) });
      sendToRoom(room.id, { type: 'members_updated', members: [...room.members.values()], hostId: room.hostId }, client.id);
      broadcastRoomList();
      console.log(`[WatchTogether] ${userName} ist Raum "${room.name}" beigetreten`);
      break;
    }

    case 'leave_room': {
      leaveRoom(client);
      break;
    }

    case 'add_to_queue': {
      if (!client.roomId) {
        sendTo(client, { type: 'error', code: 'NOT_IN_ROOM', message: 'Du bist in keinem Raum.' });
        return;
      }
      const room = rooms.get(client.roomId);
      if (!room) return;

      const url = String(msg.url || '').trim();
      if (!url) {
        sendTo(client, { type: 'error', code: 'INVALID_URL', message: 'URL darf nicht leer sein.' });
        return;
      }
      let title = String(msg.title || '').trim();
      if (!title || title === url) {
        try {
          const resp = await fetch(`https://noembed.com/embed?url=${encodeURIComponent(url)}`);
          if (resp.ok) {
            const data = await resp.json() as Record<string, any>;
            if (data.title) title = String(data.title).slice(0, 128);
          }
        } catch {}
      }
      if (!title) title = url;

      room.queue.push({ url, title, addedBy: client.name });
      sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });

      // Auto-play first item if nothing is currently playing
      if (!room.currentVideo) {
        const item = room.queue.shift()!;
        room.currentVideo = { url: item.url, title: item.title };
        room.playing = true;
        room.currentTime = 0;
        room.lastSyncAt = Date.now();
        sendToRoom(room.id, getPlaybackState(room));
        sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
      }
      break;
    }

    case 'remove_from_queue': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Warteschlange verwalten.' });
        return;
      }
      const index = Number(msg.index);
      if (index < 0 || index >= room.queue.length) {
        sendTo(client, { type: 'error', code: 'INVALID_INDEX', message: 'Ungültiger Index.' });
        return;
      }
      room.queue.splice(index, 1);
      sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
      break;
    }

    case 'move_in_queue': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Warteschlange verwalten.' });
        return;
      }
      const from = Number(msg.from);
      const to = Number(msg.to);
      if (from < 0 || from >= room.queue.length || to < 0 || to >= room.queue.length) {
        sendTo(client, { type: 'error', code: 'INVALID_INDEX', message: 'Ungültiger Index.' });
        return;
      }
      const [item] = room.queue.splice(from, 1);
      room.queue.splice(to, 0, item);
      sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
      break;
    }

    case 'play_video': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
        return;
      }

      // Track current video in history before replacing
      if (room.currentVideo) {
        room.history.push({
          url: room.currentVideo.url,
          title: room.currentVideo.title,
          addedBy: '',
          playedAt: new Date().toISOString(),
        });
        if (room.history.length > 50) room.history = room.history.slice(-50);
      }

      const index = msg.index != null ? Number(msg.index) : undefined;
      if (index !== undefined) {
        if (index < 0 || index >= room.queue.length) {
          sendTo(client, { type: 'error', code: 'INVALID_INDEX', message: 'Ungültiger Index.' });
          return;
        }
        const [item] = room.queue.splice(index, 1);
        room.currentVideo = { url: item.url, title: item.title };
      } else {
        // No index — play from queue head or error if nothing available
        if (!room.currentVideo && room.queue.length === 0) {
          sendTo(client, { type: 'error', code: 'QUEUE_EMPTY', message: 'Warteschlange ist leer und kein Video ausgewählt.' });
          return;
        }
        if (!room.currentVideo && room.queue.length > 0) {
          const item = room.queue.shift()!;
          room.currentVideo = { url: item.url, title: item.title };
        }
      }

      room.playing = true;
      room.currentTime = 0;
      room.lastSyncAt = Date.now();
      sendToRoom(room.id, getPlaybackState(room));
      sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
      break;
    }

    case 'pause': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
        return;
      }
      // Calculate drift before pausing
      room.currentTime = room.currentTime + (room.playing ? (Date.now() - room.lastSyncAt) / 1000 : 0);
      room.playing = false;
      room.lastSyncAt = Date.now();
      sendToRoom(room.id, getPlaybackState(room));
      break;
    }

    case 'resume': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
        return;
      }
      room.playing = true;
      room.lastSyncAt = Date.now();
      sendToRoom(room.id, getPlaybackState(room));
      break;
    }

    case 'seek': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
        return;
      }
      room.currentTime = Number(msg.time) || 0;
      room.lastSyncAt = Date.now();
      sendToRoom(room.id, getPlaybackState(room));
      break;
    }

    case 'skip': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann die Wiedergabe steuern.' });
        return;
      }
      // Track current video in history before skipping
      if (room.currentVideo) {
        room.history.push({
          url: room.currentVideo.url,
          title: room.currentVideo.title,
          addedBy: '',
          playedAt: new Date().toISOString(),
        });
        // Keep history max 50 items
        if (room.history.length > 50) room.history = room.history.slice(-50);
      }
      if (room.queue.length > 0) {
        const item = room.queue.shift()!;
        room.currentVideo = { url: item.url, title: item.title };
      } else {
        room.currentVideo = null;
      }
      room.playing = room.currentVideo !== null;
      room.currentTime = 0;
      room.lastSyncAt = Date.now();
      sendToRoom(room.id, getPlaybackState(room));
      sendToRoom(room.id, { type: 'queue_updated', queue: room.queue });
      break;
    }

    case 'report_time': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) return;
      room.currentTime = Number(msg.time) || 0;
      room.lastSyncAt = Date.now();
      break;
    }

    case 'transfer_host': {
      if (!client.roomId) return;
      const room = rooms.get(client.roomId);
      if (!room) return;
      if (room.hostId !== client.id) {
        sendTo(client, { type: 'error', code: 'NOT_HOST', message: 'Nur der Host kann den Host übertragen.' });
        return;
      }
      const targetId = String(msg.userId || '');
      if (!room.members.has(targetId)) {
        sendTo(client, { type: 'error', code: 'USER_NOT_FOUND', message: 'Benutzer nicht im Raum gefunden.' });
        return;
      }
      room.hostId = targetId;
      sendToRoom(room.id, { type: 'members_updated', members: [...room.members.values()], hostId: room.hostId });
      const targetMember = room.members.get(targetId);
      console.log(`[WatchTogether] Host in "${room.name}" übertragen an ${targetMember?.name ?? targetId.slice(0, 8)}`);
      break;
    }

    default:
      break;
  }
}

// ── Plugin ──

const watchTogetherPlugin: Plugin = {
  name: 'watch-together',
  version: '1.0.0',
  description: 'Watch Together',

  async init(_ctx) {
    console.log('[WatchTogether] Initialized');
  },

  registerRoutes(app: express.Application, _ctx: PluginContext) {
    app.get('/api/watch-together/rooms', (_req, res) => {
      res.json({ rooms: getRoomList() });
    });

    // Fetch video title via noembed.com (CORS-friendly oEmbed proxy)
    app.get('/api/watch-together/video-info', async (req, res) => {
      const url = String(req.query.url || '');
      if (!url) {
        res.json({ title: '' });
        return;
      }
      try {
        const response = await fetch(`https://noembed.com/embed?url=${encodeURIComponent(url)}`);
        if (response.ok) {
          const data = await response.json() as Record<string, any>;
          if (data.title) {
            res.json({ title: data.title, provider: data.provider_name || '' });
            return;
          }
        }
      } catch { /* ignore fetch errors */ }
      // Fallback: return empty title (frontend will use URL)
      res.json({ title: '' });
    });

    // Beacon cleanup endpoint — called via navigator.sendBeacon() on page unload
    app.post('/api/watch-together/disconnect', (req, res) => {
      let body = '';
      req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
      req.on('end', () => {
        try {
          const { clientId } = JSON.parse(body);
          const client = wsClients.get(clientId);
          if (client) {
            console.log(`[WatchTogether] Beacon disconnect für ${client.name || clientId.slice(0, 8)}`);
            handleDisconnect(client);
            wsClients.delete(clientId);
            client.ws.terminate();
          }
        } catch { /* ignore malformed */ }
        res.status(204).end();
      });
    });
  },

  getSnapshot(_ctx) {
    return {
      'watch-together': { rooms: getRoomList() },
    };
  },

  async destroy() {
    if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; }
    if (syncPulseInterval) { clearInterval(syncPulseInterval); syncPulseInterval = null; }
    for (const timer of cleanupTimers.values()) { clearTimeout(timer); }
    cleanupTimers.clear();
    if (wss) {
      for (const client of wsClients.values()) {
        client.ws.close(1001, 'Server shutting down');
      }
      wsClients.clear();
      rooms.clear();
      wss.close();
      wss = null;
    }
    console.log('[WatchTogether] Destroyed');
  },
};

/** Attach WebSocket to a pre-created WebSocketServer (noServer mode) */
export function attachWatchTogetherWs(existingWss: WebSocketServer): void {
  wss = existingWss;

  wss.on('connection', (ws) => {
    const clientId = crypto.randomUUID();
    const client: WtClient = { id: clientId, ws, name: '', roomId: null, isAlive: true };
    wsClients.set(clientId, client);

    sendTo(client, { type: 'welcome', clientId, rooms: getRoomList() });

    ws.on('pong', () => { client.isAlive = true; });

    ws.on('message', (raw) => {
      client.isAlive = true;
      let msg: any;
      try { msg = JSON.parse(raw.toString()); } catch { return; }
      handleMessage(client, msg);
    });

    ws.on('close', () => {
      handleDisconnect(client);
      wsClients.delete(clientId);
    });

    ws.on('error', () => {
      handleDisconnect(client);
      wsClients.delete(clientId);
    });
  });

  // ── Heartbeat: detect dead connections ──
  heartbeatInterval = setInterval(() => {
    for (const [id, client] of wsClients) {
      if (!client.isAlive) {
        console.log(`[WatchTogether] Heartbeat timeout für ${client.name || id.slice(0, 8)}`);
        handleDisconnect(client);
        wsClients.delete(id);
        client.ws.terminate();
        continue;
      }
      client.isAlive = false;
      try { client.ws.ping(); } catch { /* ignore */ }
    }
  }, HEARTBEAT_MS);

  // ── Sync pulse: broadcast playback state to playing rooms ──
  syncPulseInterval = setInterval(() => {
    for (const room of rooms.values()) {
      if (room.playing && room.members.size > 0) {
        sendToRoom(room.id, getPlaybackState(room));
      }
    }
  }, SYNC_PULSE_MS);

  console.log('[WatchTogether] WebSocket attached at /ws/watch-together');
}

export default watchTogetherPlugin;
