import { WebSocketServer } from 'ws';
import { createServer } from 'http';
import { parse } from 'url';
import { randomUUID } from 'crypto';
import webpush from 'web-push';
import { verifyToken, extractTokenFromURL } from './verify-token.js';

const PORT = process.env.PORT || 3001;
const STALE_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
const LOCATION_REQUEST_INTERVAL_MS = parseInt(process.env.LOCATION_REQUEST_INTERVAL || '60000', 10); // 60 seconds default

// VAPID keys for push notifications
// Generate with: npx web-push generate-vapid-keys
const VAPID_PUBLIC_KEY = process.env.VAPID_PUBLIC_KEY || '';
const VAPID_PRIVATE_KEY = process.env.VAPID_PRIVATE_KEY || '';
const VAPID_SUBJECT = process.env.VAPID_SUBJECT || 'mailto:push@rmaps.online';

// Configure web-push if keys are available
if (VAPID_PUBLIC_KEY && VAPID_PRIVATE_KEY) {
  webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY);
  console.log('Push notifications enabled');
} else {
  console.log('Push notifications disabled (VAPID keys not configured)');
}

// Room state storage: Map<roomSlug, RoomState>
const rooms = new Map();

// Client tracking: Map<WebSocket, { roomSlug, participantId, claims, readOnly }>
const clients = new Map();

// Push subscriptions: Map<roomSlug, Set<subscription>>
const pushSubscriptions = new Map();

function getRoomState(slug) {
  if (!rooms.has(slug)) {
    rooms.set(slug, {
      id: randomUUID(),
      slug: slug,
      name: slug,
      createdAt: new Date().toISOString(),
      participants: {},
      waypoints: [],
      lastActivity: Date.now(),
      visibility: 'public',    // public | public_read | authenticated | members_only
      ownerDID: null,
    });
  }
  const room = rooms.get(slug);
  room.lastActivity = Date.now();
  return room;
}

/**
 * Evaluate whether a connection should be allowed based on room visibility.
 * Returns { allowed, readOnly, reason }
 */
function evaluateRoomAccess(room, claims, isRead = false) {
  const { visibility, ownerDID } = room;
  const isOwner = !!(claims && ownerDID && (claims.sub === ownerDID || claims.did === ownerDID));

  if (visibility === 'public') {
    return { allowed: true, readOnly: false, isOwner };
  }

  if (visibility === 'public_read') {
    // Everyone can connect, but writes require auth
    return { allowed: true, readOnly: !claims, isOwner };
  }

  if (visibility === 'authenticated') {
    if (!claims) {
      return { allowed: false, readOnly: false, isOwner: false, reason: 'Authentication required' };
    }
    return { allowed: true, readOnly: false, isOwner };
  }

  if (visibility === 'members_only') {
    if (!claims) {
      return { allowed: false, readOnly: false, isOwner: false, reason: 'Authentication required' };
    }
    // Further membership checks could be added here
    return { allowed: true, readOnly: false, isOwner };
  }

  return { allowed: false, readOnly: false, isOwner: false, reason: 'Unknown visibility' };
}

function cleanupStaleParticipants(room) {
  const now = Date.now();
  const staleIds = [];

  for (const [id, participant] of Object.entries(room.participants)) {
    if (participant.lastSeen && now - participant.lastSeen > STALE_THRESHOLD_MS) {
      staleIds.push(id);
    }
  }

  for (const id of staleIds) {
    delete room.participants[id];
    console.log(`Cleaned up stale participant: ${id}`);
  }

  return staleIds;
}

function broadcast(roomSlug, message, excludeWs = null) {
  const messageStr = JSON.stringify(message);
  let count = 0;

  for (const [ws, clientInfo] of clients.entries()) {
    if (clientInfo.roomSlug === roomSlug && ws !== excludeWs && ws.readyState === 1) {
      ws.send(messageStr);
      count++;
    }
  }

  return count;
}

// Send push notification to all subscriptions for a room
async function sendPushToRoom(roomSlug, notification, excludeEndpoint = null) {
  if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) return;

  const subs = pushSubscriptions.get(roomSlug);
  if (!subs || subs.size === 0) return;

  const payload = JSON.stringify(notification);
  const failedEndpoints = [];

  for (const sub of subs) {
    if (excludeEndpoint && sub.endpoint === excludeEndpoint) continue;

    try {
      await webpush.sendNotification(sub, payload);
      console.log(`[${roomSlug}] Push sent to ${sub.endpoint.slice(-20)}`);
    } catch (error) {
      console.error(`[${roomSlug}] Push failed:`, error.statusCode || error.message);
      // Remove invalid subscriptions
      if (error.statusCode === 404 || error.statusCode === 410) {
        failedEndpoints.push(sub.endpoint);
      }
    }
  }

  // Clean up failed subscriptions
  for (const endpoint of failedEndpoints) {
    for (const sub of subs) {
      if (sub.endpoint === endpoint) {
        subs.delete(sub);
        console.log(`[${roomSlug}] Removed invalid subscription`);
      }
    }
  }
}

function handleMessage(ws, data) {
  const clientInfo = clients.get(ws);
  if (!clientInfo) return;

  let message;
  try {
    message = JSON.parse(data);
  } catch (e) {
    console.error('Invalid JSON:', e.message);
    return;
  }

  const room = getRoomState(clientInfo.roomSlug);

  // Block write operations from readOnly connections
  const writeOps = ['join', 'leave', 'location', 'status', 'waypoint_add', 'waypoint_remove'];
  if (clientInfo.readOnly && writeOps.includes(message.type)) {
    ws.send(JSON.stringify({ type: 'error', error: 'Read-only access — authenticate to interact' }));
    return;
  }

  switch (message.type) {
    case 'join': {
      const participant = {
        ...message.participant,
        lastSeen: Date.now()
      };
      room.participants[participant.id] = participant;
      clientInfo.participantId = participant.id;

      console.log(`[${clientInfo.roomSlug}] ${participant.name} joined (${participant.id})`);

      // Broadcast join to others
      broadcast(clientInfo.roomSlug, message, ws);

      // Send push notification to others
      sendPushToRoom(clientInfo.roomSlug, {
        title: 'Friend Joined! 👋',
        body: `${participant.name} ${participant.emoji} joined the room`,
        tag: `join-${participant.id}`,
        data: {
          type: 'join',
          roomSlug: clientInfo.roomSlug,
          participantId: participant.id,
          participantName: participant.name,
        },
      });

      // Send current state to the new participant
      ws.send(JSON.stringify({
        type: 'full_state',
        state: room
      }));
      break;
    }

    case 'leave': {
      const leavingParticipant = room.participants[message.participantId];
      delete room.participants[message.participantId];
      console.log(`[${clientInfo.roomSlug}] Participant left: ${message.participantId}`);
      broadcast(clientInfo.roomSlug, message, ws);

      // Send push notification
      if (leavingParticipant) {
        sendPushToRoom(clientInfo.roomSlug, {
          title: 'Friend Left',
          body: `${leavingParticipant.name} ${leavingParticipant.emoji} left the room`,
          tag: `leave-${message.participantId}`,
          data: {
            type: 'leave',
            roomSlug: clientInfo.roomSlug,
            participantId: message.participantId,
          },
        });
      }
      break;
    }

    case 'location': {
      if (room.participants[message.participantId]) {
        room.participants[message.participantId].location = message.location;
        room.participants[message.participantId].lastSeen = Date.now();

        // Broadcast to all OTHER participants
        const count = broadcast(clientInfo.roomSlug, message, ws);
        console.log(`[${clientInfo.roomSlug}] Location update from ${message.participantId} -> ${count} clients`);
      }
      break;
    }

    case 'status': {
      if (room.participants[message.participantId]) {
        room.participants[message.participantId].status = message.status;
        room.participants[message.participantId].lastSeen = Date.now();
        broadcast(clientInfo.roomSlug, message, ws);
      }
      break;
    }

    case 'waypoint_add': {
      room.waypoints.push(message.waypoint);
      console.log(`[${clientInfo.roomSlug}] Waypoint added: ${message.waypoint.id}`);
      broadcast(clientInfo.roomSlug, message, ws);

      // Send push notification for meeting points
      if (message.waypoint.type === 'meetup') {
        const creator = room.participants[clientInfo.participantId];
        sendPushToRoom(clientInfo.roomSlug, {
          title: 'Meeting Point Set! 📍',
          body: `${creator?.name || 'Someone'} set a meeting point: ${message.waypoint.name}`,
          tag: `waypoint-${message.waypoint.id}`,
          requireInteraction: true,
          data: {
            type: 'waypoint',
            roomSlug: clientInfo.roomSlug,
            waypointId: message.waypoint.id,
            waypointName: message.waypoint.name,
          },
          actions: [
            { action: 'view', title: 'View on Map' },
            { action: 'dismiss', title: 'Dismiss' },
          ],
        });
      }
      break;
    }

    case 'waypoint_remove': {
      room.waypoints = room.waypoints.filter(w => w.id !== message.waypointId);
      console.log(`[${clientInfo.roomSlug}] Waypoint removed: ${message.waypointId}`);
      broadcast(clientInfo.roomSlug, message, ws);
      break;
    }

    case 'request_state': {
      cleanupStaleParticipants(room);
      ws.send(JSON.stringify({
        type: 'full_state',
        state: room
      }));
      break;
    }

    default:
      console.log(`Unknown message type: ${message.type}`);
  }
}

function handleClose(ws) {
  const clientInfo = clients.get(ws);
  if (clientInfo) {
    const room = rooms.get(clientInfo.roomSlug);
    if (room && clientInfo.participantId && room.participants[clientInfo.participantId]) {
      // Don't delete - mark as offline and preserve last location
      room.participants[clientInfo.participantId].status = 'offline';
      room.participants[clientInfo.participantId].lastSeen = Date.now();
      broadcast(clientInfo.roomSlug, {
        type: 'status',
        participantId: clientInfo.participantId,
        status: 'offline'
      });
      console.log(`[${clientInfo.roomSlug}] User went offline: ${clientInfo.participantId} (location preserved)`);
    }
    clients.delete(ws);
  }
}

// Periodic cleanup of empty rooms
setInterval(() => {
  const now = Date.now();
  for (const [slug, room] of rooms.entries()) {
    // Clean stale participants
    cleanupStaleParticipants(room);

    // Remove empty rooms older than 24 hours
    if (Object.keys(room.participants).length === 0 &&
        now - room.lastActivity > 24 * 60 * 60 * 1000) {
      rooms.delete(slug);
      pushSubscriptions.delete(slug);
      console.log(`Cleaned up empty room: ${slug}`);
    }
  }
}, 5 * 60 * 1000); // Every 5 minutes

// Automatic location request - periodically ask all clients for location updates via silent push
async function requestLocationFromAllRooms() {
  if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY) return;

  for (const [roomSlug, subs] of pushSubscriptions.entries()) {
    if (subs.size === 0) continue;

    const room = rooms.get(roomSlug);
    // Only request if room has participants (active room)
    if (!room || Object.keys(room.participants).length === 0) continue;

    console.log(`[${roomSlug}] Requesting location from ${subs.size} subscribers`);

    const failedEndpoints = [];
    for (const sub of subs) {
      try {
        await webpush.sendNotification(sub, JSON.stringify({
          silent: true,
          data: { type: 'location_request', roomSlug }
        }));
      } catch (error) {
        if (error.statusCode === 404 || error.statusCode === 410) {
          failedEndpoints.push(sub.endpoint);
        }
      }
    }

    // Clean up failed subscriptions
    for (const endpoint of failedEndpoints) {
      for (const sub of subs) {
        if (sub.endpoint === endpoint) {
          subs.delete(sub);
          console.log(`[${roomSlug}] Removed stale push subscription`);
        }
      }
    }
  }
}

// Start automatic location request interval
if (VAPID_PUBLIC_KEY && VAPID_PRIVATE_KEY && LOCATION_REQUEST_INTERVAL_MS > 0) {
  setInterval(requestLocationFromAllRooms, LOCATION_REQUEST_INTERVAL_MS);
  console.log(`Automatic location requests enabled every ${LOCATION_REQUEST_INTERVAL_MS / 1000}s`);
}

// Parse JSON body from request
async function parseJsonBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      try {
        resolve(JSON.parse(body));
      } catch (e) {
        reject(new Error('Invalid JSON'));
      }
    });
    req.on('error', reject);
  });
}

// Add CORS headers
function addCorsHeaders(res) {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}

// Create HTTP server for health checks and push subscription endpoints
const server = createServer(async (req, res) => {
  const { pathname } = parse(req.url);
  addCorsHeaders(res);

  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  if (pathname === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      status: 'ok',
      rooms: rooms.size,
      clients: clients.size,
      pushEnabled: !!(VAPID_PUBLIC_KEY && VAPID_PRIVATE_KEY),
      uptime: process.uptime()
    }));
  } else if (pathname === '/stats') {
    const roomStats = {};
    for (const [slug, room] of rooms.entries()) {
      roomStats[slug] = {
        participants: Object.keys(room.participants).length,
        waypoints: room.waypoints.length,
        pushSubscriptions: pushSubscriptions.get(slug)?.size || 0,
        lastActivity: room.lastActivity
      };
    }
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ rooms: roomStats, totalClients: clients.size }));
  } else if (pathname === '/push/vapid-public-key') {
    // Return VAPID public key for client
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ publicKey: VAPID_PUBLIC_KEY }));
  } else if (pathname === '/push/subscribe' && req.method === 'POST') {
    // Subscribe to push notifications
    try {
      const { subscription, roomSlug } = await parseJsonBody(req);

      if (!subscription || !subscription.endpoint) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Invalid subscription' }));
        return;
      }

      // Store subscription for the room
      if (roomSlug) {
        if (!pushSubscriptions.has(roomSlug)) {
          pushSubscriptions.set(roomSlug, new Set());
        }
        pushSubscriptions.get(roomSlug).add(subscription);
        console.log(`[${roomSlug}] Push subscription added`);
      }

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ success: true }));
    } catch (error) {
      console.error('Push subscribe error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Failed to subscribe' }));
    }
  } else if (pathname === '/push/unsubscribe' && req.method === 'POST') {
    // Unsubscribe from push notifications
    try {
      const { endpoint, roomSlug } = await parseJsonBody(req);

      if (roomSlug && pushSubscriptions.has(roomSlug)) {
        const subs = pushSubscriptions.get(roomSlug);
        for (const sub of subs) {
          if (sub.endpoint === endpoint) {
            subs.delete(sub);
            console.log(`[${roomSlug}] Push subscription removed`);
            break;
          }
        }
      }

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ success: true }));
    } catch (error) {
      console.error('Push unsubscribe error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Failed to unsubscribe' }));
    }
  } else if (pathname === '/push/test' && req.method === 'POST') {
    // Send test push notification
    try {
      const { roomSlug } = await parseJsonBody(req);

      if (roomSlug) {
        await sendPushToRoom(roomSlug, {
          title: 'Test Notification 🔔',
          body: 'Push notifications are working!',
          tag: 'test',
          data: { type: 'test', roomSlug },
        });
      }

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ success: true }));
    } catch (error) {
      console.error('Push test error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Failed to send test' }));
    }
  } else if (pathname?.startsWith('/room/') && pathname.endsWith('/config') && req.method === 'POST') {
    // Set room visibility and config (requires auth + ownership)
    try {
      const roomSlug = pathname.replace('/room/', '').replace('/config', '');
      const body = await parseJsonBody(req);
      const authHeader = req.headers['authorization'] || '';
      const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
      const claims = token ? await verifyToken(token) : null;

      if (!claims) {
        res.writeHead(401, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Authentication required' }));
        return;
      }

      const room = getRoomState(roomSlug);

      // Only the owner (or first authenticated user) can change room config
      if (room.ownerDID && room.ownerDID !== claims.sub && room.ownerDID !== claims.did) {
        res.writeHead(403, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Only the room owner can change settings' }));
        return;
      }

      // Set owner if not set
      if (!room.ownerDID) {
        room.ownerDID = claims.sub || claims.did;
      }

      const validVisibilities = ['public', 'public_read', 'authenticated', 'members_only'];
      if (body.visibility && validVisibilities.includes(body.visibility)) {
        room.visibility = body.visibility;
      }
      if (body.name) {
        room.name = body.name;
      }

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        success: true,
        room: { slug: room.slug, visibility: room.visibility, ownerDID: room.ownerDID, name: room.name }
      }));
    } catch (error) {
      console.error('Room config error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Failed to update room config' }));
    }
  } else if (pathname === '/push/request-location' && req.method === 'POST') {
    // Manually trigger location request for a room or specific participant
    // Uses WebSocket for online clients, push for offline/background
    try {
      const { roomSlug, participantId } = await parseJsonBody(req);

      if (!roomSlug) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'roomSlug required' }));
        return;
      }

      let wsSent = 0;
      let pushSent = 0;
      let pushFailed = 0;

      // Get room to access participant names for deduplication
      const room = rooms.get(roomSlug);

      // If targeting a specific participant, get their name for deduplication
      let targetName = null;
      if (participantId && room?.participants?.[participantId]) {
        targetName = room.participants[participantId].name;
      }

      // Send WebSocket message to connected clients, deduplicated by name
      const locationRequestMsg = JSON.stringify({ type: 'request_location' });
      const pingedNames = new Set();

      for (const [ws, clientInfo] of clients.entries()) {
        if (clientInfo.roomSlug === roomSlug && ws.readyState === 1) {
          // Get participant info for this client
          const participant = room?.participants?.[clientInfo.participantId];
          const name = participant?.name;

          // If targeting specific participant, only ping that name
          if (targetName && name !== targetName) {
            continue;
          }

          // Skip if we've already pinged this name
          if (name && pingedNames.has(name)) {
            console.log(`[${roomSlug}] Skipping duplicate ping for: ${name}`);
            continue;
          }

          ws.send(locationRequestMsg);
          wsSent++;

          if (name) {
            pingedNames.add(name);
          }
        }
      }
      console.log(`[${roomSlug}] Location request via WebSocket: ${wsSent} clients${targetName ? ` (target: ${targetName})` : ' (deduped by name)'}`);

      // Then, send push notifications to offline subscribers
      const subs = pushSubscriptions.get(roomSlug);
      if (subs && subs.size > 0) {
        const failedEndpoints = [];
        for (const sub of subs) {
          try {
            await webpush.sendNotification(sub, JSON.stringify({
              silent: true,
              data: { type: 'location_request', roomSlug }
            }));
            pushSent++;
          } catch (error) {
            pushFailed++;
            if (error.statusCode === 404 || error.statusCode === 410) {
              failedEndpoints.push(sub.endpoint);
            }
          }
        }

        // Clean up failed subscriptions
        for (const endpoint of failedEndpoints) {
          for (const sub of subs) {
            if (sub.endpoint === endpoint) {
              subs.delete(sub);
            }
          }
        }
      }

      console.log(`[${roomSlug}] Manual location request: ${wsSent} WebSocket, ${pushSent} push sent, ${pushFailed} push failed`);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        success: true,
        websocket: wsSent,
        push: pushSent,
        pushFailed,
        total: wsSent + pushSent
      }));
    } catch (error) {
      console.error('Location request error:', error);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Failed to request location' }));
    }
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

// Create WebSocket server
const wss = new WebSocketServer({ server });

wss.on('connection', async (ws, req) => {
  const { pathname } = parse(req.url);

  // Extract room slug from path: /room/{slug}
  const match = pathname?.match(/^\/room\/([^/]+)$/);
  if (!match) {
    console.log(`Invalid path: ${pathname}`);
    ws.close(4000, 'Invalid room path');
    return;
  }

  const roomSlug = decodeURIComponent(match[1]);

  // Extract and verify auth token from query string
  const token = extractTokenFromURL(req.url);
  let claims = null;
  if (token) {
    claims = await verifyToken(token);
    if (claims) {
      console.log(`[${roomSlug}] Authenticated connection: ${claims.username || claims.sub}`);
    }
  }

  // Check room access based on visibility
  const room = getRoomState(roomSlug);
  const access = evaluateRoomAccess(room, claims);

  if (!access.allowed) {
    console.log(`[${roomSlug}] Connection rejected: ${access.reason}`);
    ws.close(4001, access.reason || 'Access denied');
    return;
  }

  console.log(`New connection to room: ${roomSlug}${access.readOnly ? ' (read-only)' : ''}`);

  // Register client with auth info
  clients.set(ws, { roomSlug, participantId: null, claims, readOnly: access.readOnly });

  // If authenticated and room has no owner, set this user as owner
  if (claims && !room.ownerDID && Object.keys(room.participants).length === 0) {
    room.ownerDID = claims.sub || claims.did;
    console.log(`[${roomSlug}] Room owner set to: ${room.ownerDID}`);
  }

  // Set up handlers
  ws.on('message', (data) => handleMessage(ws, data.toString()));
  ws.on('close', () => handleClose(ws));
  ws.on('error', (err) => {
    console.error('WebSocket error:', err.message);
    handleClose(ws);
  });

  // Send ping every 30 seconds to keep connection alive
  const pingInterval = setInterval(() => {
    if (ws.readyState === 1) {
      ws.ping();
    } else {
      clearInterval(pingInterval);
    }
  }, 30000);

  ws.on('close', () => clearInterval(pingInterval));
});

server.listen(PORT, () => {
  console.log(`rmaps sync server listening on port ${PORT}`);
  console.log(`WebSocket: ws://localhost:${PORT}/room/{slug}`);
  console.log(`Health check: http://localhost:${PORT}/health`);
  console.log(`Push API: http://localhost:${PORT}/push/subscribe`);
});
