import { file, serve, type ServerWebSocket } from "bun";
import { Channel, type Track, type WsData, type PersistenceCallback } from "./channel";
import { readdir } from "fs/promises";
import { join, resolve } from "path";
import {
  createUser,
  findUserByUsername,
  validatePassword,
  createSession,
  createGuestSession,
  deleteSession,
  validateSession,
  hasPermission,
  getUserPermissions,
  getAllUsers,
  grantPermission,
  revokePermission,
  findUserById,
  saveChannel,
  updateChannelState,
  loadAllChannels,
  deleteChannelFromDb,
  saveChannelQueue,
  loadChannelQueue,
  removeTrackFromQueues,
} from "./db";
import {
  getUser,
  requireUser,
  requirePermission,
  setSessionCookie,
  clearSessionCookie,
  getClientInfo,
} from "./auth";
import { Library } from "./library";

// Load config
interface Config {
  port: number;
  musicDir: string;
  allowGuests: boolean;
  defaultPermissions: {
    channel?: string[];
  };
}

const CONFIG_PATH = join(import.meta.dir, "config.json");

const DEFAULT_CONFIG: Config = {
  port: 3001,
  musicDir: "./music",
  allowGuests: true
};

// Create default config if missing
const configFile = file(CONFIG_PATH);
if (!(await configFile.exists())) {
  console.log("[Config] Creating default config.json...");
  await Bun.write(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
}

const config: Config = await configFile.json();

const MUSIC_DIR = resolve(import.meta.dir, config.musicDir);
const PUBLIC_DIR = join(import.meta.dir, "public");

console.log(`Config loaded: port=${config.port}, musicDir=${MUSIC_DIR}, allowGuests=${config.allowGuests}`);

// Initialize library
const library = new Library(MUSIC_DIR);

// Auto-discover tracks if queue is empty
async function discoverTracks(): Promise<string[]> {
  try {
    const files = await readdir(MUSIC_DIR);
    return files.filter((f) => /\.(mp3|ogg|flac|wav|m4a|aac)$/i.test(f)).sort();
  } catch {
    return [];
  }
}

// Generate unique channel ID
function generateChannelId(): string {
  return Math.random().toString(36).slice(2, 10);
}

// Initialize channels - create default channel with full library
const channels = new Map<string, Channel>();

// Track all WebSocket connections by user ID for kick functionality
const userConnections = new Map<number, Set<ServerWebSocket<WsData>>>();

// Persistence callback for channels
const persistChannel: PersistenceCallback = (channel, type) => {
  if (type === "state") {
    updateChannelState(channel.id, {
      currentIndex: channel.currentIndex,
      startedAt: channel.startedAt,
      paused: channel.paused,
      pausedAt: channel.pausedAt,
      playbackMode: channel.playbackMode,
    });
  } else if (type === "queue") {
    saveChannelQueue(channel.id, channel.queue.map(t => t.id));
  }
};

// Helper to build Track objects from track IDs using library
function buildTracksFromIds(trackIds: string[], lib: Library): Track[] {
  const tracks: Track[] = [];
  for (const tid of trackIds) {
    const libTrack = lib.getTrack(tid);
    if (libTrack && libTrack.duration > 0) {
      tracks.push({
        id: libTrack.id,
        filename: libTrack.filename,
        title: libTrack.title || libTrack.filename.replace(/\.[^.]+$/, ""),
        duration: libTrack.duration,
      });
    }
  }
  return tracks;
}

// Helper to get all library tracks as Track objects
function getAllLibraryTracks(lib: Library): Track[] {
  return lib.getAllTracks()
    .filter(t => t.duration > 0)
    .map(t => ({
      id: t.id,
      filename: t.filename,
      title: t.title || t.filename.replace(/\.[^.]+$/, ""),
      duration: t.duration,
    }));
}

async function init(): Promise<void> {
  // Scan library first
  await library.scan();
  library.startWatching();
  
  // Broadcast when scan completes
  library.onScanComplete(() => {
    broadcastToAll({ type: "scan_progress", scanning: false });
  });
  
  // Broadcast when tracks are added/updated
  library.on("added", (track) => {
    broadcastToAll({ type: "toast", message: `Added: ${track.title || track.filename}`, toastType: "info" });
    library.logActivity("scan_added", { id: track.id, filename: track.filename, title: track.title });
  });
  library.on("changed", (track) => {
    broadcastToAll({ type: "toast", message: `Updated: ${track.title || track.filename}`, toastType: "info" });
    library.logActivity("scan_updated", { id: track.id, filename: track.filename, title: track.title });
  });

  // Load channels from database
  const savedChannels = loadAllChannels();
  let hasDefault = false;

  for (const row of savedChannels) {
    // Load queue for this channel
    const trackIds = loadChannelQueue(row.id);
    const tracks = buildTracksFromIds(trackIds, library);
    
    // For default channel, if queue is empty, use full library
    const isDefault = row.is_default === 1;
    if (isDefault) {
      hasDefault = true;
    }
    
    const channelTracks = (isDefault && tracks.length === 0) 
      ? getAllLibraryTracks(library) 
      : tracks;

    const channel = new Channel({
      id: row.id,
      name: row.name,
      description: row.description,
      tracks: channelTracks,
      createdBy: row.created_by,
      isDefault,
      currentIndex: row.current_index,
      startedAt: row.started_at,
      paused: row.paused === 1,
      pausedAt: row.paused_at,
      playbackMode: (row.playback_mode as "repeat-all" | "repeat-one" | "shuffle") || "repeat-all",
    });
    
    channel.setPersistenceCallback(persistChannel);
    channels.set(row.id, channel);
    console.log(`Loaded channel "${row.name}" (id=${row.id}) with ${channelTracks.length} tracks`);
  }

  // Create default channel if it doesn't exist
  if (!hasDefault) {
    const tracks = getAllLibraryTracks(library);
    const defaultChannel = new Channel({
      id: "main",
      name: "Main Channel",
      description: "All tracks from the library",
      tracks,
      isDefault: true,
      createdBy: null,
    });
    
    defaultChannel.setPersistenceCallback(persistChannel);
    channels.set("main", defaultChannel);
    
    // Save to database
    saveChannel({
      id: defaultChannel.id,
      name: defaultChannel.name,
      description: defaultChannel.description,
      createdBy: defaultChannel.createdBy,
      isDefault: true,
      currentIndex: defaultChannel.currentIndex,
      startedAt: defaultChannel.startedAt,
      paused: defaultChannel.paused,
      pausedAt: defaultChannel.pausedAt,
      playbackMode: defaultChannel.playbackMode,
    });
    saveChannelQueue(defaultChannel.id, tracks.map(t => t.id));
    
    console.log(`Default channel created: ${tracks.length} tracks`);
  }
}

await init();

// Broadcast to all connected clients across all channels
function broadcastToAll(message: object) {
  const data = JSON.stringify(message);
  let clientCount = 0;
  for (const channel of channels.values()) {
    for (const ws of channel.clients) {
      ws.send(data);
      clientCount++;
    }
  }
  console.log(`[Broadcast] Sent to ${clientCount} clients`);
}

// Broadcast channel list to all clients
function broadcastChannelList() {
  const list = [...channels.values()].map(c => c.getListInfo());
  console.log(`[Broadcast] Sending channel_list to all clients (${list.length} channels)`, JSON.stringify(list.map(c => ({ id: c.id, listeners: c.listeners }))));
  broadcastToAll({ type: "channel_list", channels: list });
}

// Listen for library changes and notify clients
library.on("added", (track) => {
  console.log(`New track detected: ${track.title}`);
  
  const allTracks = library.getAllTracks().map(t => ({
    id: t.id,
    title: t.title,
    duration: t.duration
  }));
  broadcastToAll({ 
    type: "track_added", 
    track: { id: track.id, title: track.title, duration: track.duration },
    library: allTracks
  });
});

library.on("removed", (track) => {
  console.log(`Track removed: ${track.title}`);
  
  // Remove from database queue entries
  removeTrackFromQueues(track.id);
  
  const allTracks = library.getAllTracks().map(t => ({
    id: t.id,
    title: t.title,
    duration: t.duration
  }));
  broadcastToAll({ 
    type: "track_removed", 
    track: { id: track.id, title: track.title },
    library: allTracks
  });
});

// Tick interval: advance tracks when needed, broadcast every 30s
let tickCount = 0;
setInterval(() => {
  tickCount++;
  for (const channel of channels.values()) {
    const changed = channel.tick();
    if (changed) {
      console.log(`[Tick] Channel "${channel.name}" advanced to track ${channel.currentIndex}`);
    }
    if (!changed && tickCount % 30 === 0) {
      console.log(`[Tick] Broadcasting state for channel "${channel.name}" (${channel.clients.size} clients)`);
      channel.broadcast();
    }
  }
  
  // Broadcast scan progress every 2 seconds while scanning
  const scanProgress = library.scanProgress;
  if (scanProgress.scanning && tickCount % 2 === 0) {
    broadcastToAll({ 
      type: "scan_progress", 
      scanning: true,
      processed: scanProgress.processed, 
      total: scanProgress.total 
    });
  } else if (!scanProgress.scanning && tickCount % 30 === 0) {
    // Periodically send "not scanning" to clear any stale UI
    broadcastToAll({ type: "scan_progress", scanning: false });
  }
}, 1000);

// Helper to get or create guest session
function getOrCreateUser(req: Request, server: any): { user: ReturnType<typeof getUser>, headers?: Headers } {
  let user = getUser(req, server);
  if (user) return { user };
  
  if (config.allowGuests) {
    const { userAgent, ipAddress } = getClientInfo(req, server);
    const guest = createGuestSession(userAgent, ipAddress);
    console.log(`[AUTH] Guest session created: user="${guest.user.username}" id=${guest.user.id} ip=${ipAddress}`);
    const headers = new Headers();
    headers.set("Set-Cookie", setSessionCookie(guest.token));
    return { user: guest.user, headers };
  }
  
  return { user: null };
}

// Check if user has permission (including default permissions)
function userHasPermission(user: ReturnType<typeof getUser>, resourceType: string, resourceId: string | null, permission: string): boolean {
  if (!user) return false;
  if (user.is_admin) return true;
  
  // Guests can never control playback
  if (user.is_guest && permission === "control") return false;
  
  // Check default permissions from config
  if (resourceType === "channel" && config.defaultPermissions.channel?.includes(permission)) {
    return true;
  }
  
  // Check user-specific permissions
  return hasPermission(user.id, resourceType, resourceId, permission);
}

serve({
  port: config.port,
  async fetch(req, server) {
    const url = new URL(req.url);
    const path = url.pathname;

    // WebSocket upgrade for channels
    if (path.match(/^\/api\/channels\/([^/]+)\/ws$/)) {
      const id = path.split("/")[3];
      if (!channels.has(id)) return new Response("Channel not found", { status: 404 });
      const { user } = getOrCreateUser(req, server);
      const ok = server.upgrade(req, { data: { channelId: id, userId: user?.id ?? null, username: user?.username ?? 'Guest' } });
      if (ok) return undefined;
      return new Response("WebSocket upgrade failed", { status: 500 });
    }

    // API: server status (public)
    if (path === "/api/status") {
      return Response.json({
        name: "MusicRoom",
        version: "1.0.0",
        allowGuests: config.allowGuests,
        allowSignups: true,
        channelCount: channels.size,
        defaultPermissions: config.defaultPermissions,
      });
    }

    // API: list channels (requires auth or guest)
    if (path === "/api/channels" && req.method === "GET") {
      const { user, headers } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      const list = [...channels.values()].map(c => c.getListInfo());
      return Response.json(list, { headers });
    }

    // API: create channel
    if (path === "/api/channels" && req.method === "POST") {
      const { user } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      if (user.is_guest) {
        return Response.json({ error: "Guests cannot create channels" }, { status: 403 });
      }
      try {
        const { name, description, trackIds } = await req.json();
        if (!name || typeof name !== "string" || name.trim().length === 0) {
          return Response.json({ error: "Name is required" }, { status: 400 });
        }
        
        // Build track list from trackIds or default to full library
        let tracks: Track[];
        if (trackIds && Array.isArray(trackIds) && trackIds.length > 0) {
          tracks = [];
          for (const tid of trackIds) {
            const libTrack = library.getTrack(tid);
            if (libTrack) {
              tracks.push({
                id: libTrack.id,
                filename: libTrack.filename,
                title: libTrack.title || libTrack.filename.replace(/\.[^.]+$/, ""),
                duration: libTrack.duration,
              });
            }
          }
        } else {
          // Default to full library
          tracks = library.getAllTracks()
            .filter(t => t.duration > 0)
            .map(t => ({
              id: t.id,
              filename: t.filename,
              title: t.title || t.filename.replace(/\.[^.]+$/, ""),
              duration: t.duration,
            }));
        }

        const channelId = generateChannelId();
        const channel = new Channel({
          id: channelId,
          name: name.trim(),
          description: description || "",
          tracks,
          createdBy: user.id,
          isDefault: false,
        });
        channel.setPersistenceCallback(persistChannel);
        channels.set(channelId, channel);
        
        // Save to database
        saveChannel({
          id: channel.id,
          name: channel.name,
          description: channel.description,
          createdBy: channel.createdBy,
          isDefault: false,
          currentIndex: channel.currentIndex,
          startedAt: channel.startedAt,
          paused: channel.paused,
          pausedAt: channel.pausedAt,
          playbackMode: channel.playbackMode,
        });
        saveChannelQueue(channel.id, tracks.map(t => t.id));
        
        console.log(`[Channel] Created "${name.trim()}" (id=${channelId}) by user ${user.id}`);
        broadcastChannelList();
        return Response.json(channel.getListInfo(), { status: 201 });
      } catch {
        return Response.json({ error: "Invalid request" }, { status: 400 });
      }
    }

    // API: delete channel
    const channelDeleteMatch = path.match(/^\/api\/channels\/([^/]+)$/);
    if (channelDeleteMatch && req.method === "DELETE") {
      const channelId = channelDeleteMatch[1];
      const { user } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      const channel = channels.get(channelId);
      if (!channel) {
        return Response.json({ error: "Channel not found" }, { status: 404 });
      }
      if (channel.isDefault) {
        return Response.json({ error: "Cannot delete default channel" }, { status: 403 });
      }
      if (!user.is_admin && channel.createdBy !== user.id) {
        return Response.json({ error: "Access denied" }, { status: 403 });
      }
      channels.delete(channelId);
      deleteChannelFromDb(channelId);
      broadcastChannelList();
      return Response.json({ success: true });
    }

    // API: get channel state
    const channelMatch = path.match(/^\/api\/channels\/([^/]+)$/);
    if (channelMatch && req.method === "GET") {
      const channel = channels.get(channelMatch[1]);
      if (!channel) return new Response("Not found", { status: 404 });
      return Response.json(channel.getState());
    }

    // API: get library (all tracks)
    if (path === "/api/library") {
      const { user, headers } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      const tracks = library.getAllTracks().map(t => ({
        id: t.id,
        filename: t.filename,
        title: t.title,
        artist: t.artist,
        album: t.album,
        duration: t.duration,
        available: t.available,
      }));
      return Response.json(tracks, { headers });
    }

    // API: upload audio file
    if (path === "/api/upload" && req.method === "POST") {
      const { user, headers } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      
      try {
        const formData = await req.formData();
        const uploadedFile = formData.get("file");
        
        if (!uploadedFile || !(uploadedFile instanceof File)) {
          library.logActivity("upload_failed", { filename: "unknown" }, { id: user.id, username: user.username });
          return Response.json({ error: "No file provided" }, { status: 400 });
        }
        
        // Validate extension
        const validExts = [".mp3", ".ogg", ".flac", ".wav", ".m4a", ".aac", ".opus", ".wma", ".mp4"];
        const ext = uploadedFile.name.toLowerCase().match(/\.[^.]+$/)?.[0];
        if (!ext || !validExts.includes(ext)) {
          library.logActivity("upload_rejected", { filename: uploadedFile.name }, { id: user.id, username: user.username });
          return Response.json({ error: "Invalid audio format" }, { status: 400 });
        }
        
        // Sanitize filename
        const safeName = uploadedFile.name.replace(/[^a-zA-Z0-9._-]/g, "_");
        const destPath = join(config.musicDir, safeName);
        
        // Check if file already exists
        const existingFile = Bun.file(destPath);
        if (await existingFile.exists()) {
          library.logActivity("upload_duplicate", { filename: safeName }, { id: user.id, username: user.username });
          return Response.json({ error: "File already exists" }, { status: 409 });
        }
        
        // Write file
        const arrayBuffer = await uploadedFile.arrayBuffer();
        await Bun.write(destPath, arrayBuffer);
        
        console.log(`[Upload] ${user.username} uploaded: ${safeName}`);
        library.logActivity("upload", { filename: safeName }, { id: user.id, username: user.username });
        return Response.json({ success: true, filename: safeName }, { headers });
      } catch (e) {
        console.error("[Upload] Error:", e);
        library.logActivity("upload_error", { filename: "unknown" }, { id: user.id, username: user.username });
        return Response.json({ error: "Upload failed" }, { status: 500 });
      }
    }

    // Auth: signup
    if (path === "/api/auth/signup" && req.method === "POST") {
      try {
        const { username, password } = await req.json();
        if (!username || !password) {
          return Response.json({ error: "Username and password required" }, { status: 400 });
        }
        if (username.length < 3 || password.length < 6) {
          return Response.json({ error: "Username min 3 chars, password min 6 chars" }, { status: 400 });
        }
        const existing = findUserByUsername(username);
        if (existing) {
          return Response.json({ error: "Username already taken" }, { status: 400 });
        }
        const user = await createUser(username, password);
        const userAgent = req.headers.get("user-agent") ?? undefined;
        const ipAddress = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() 
          ?? req.headers.get("x-real-ip") 
          ?? server.requestIP(req)?.address
          ?? undefined;
        const token = createSession(user.id, userAgent, ipAddress);
        console.log(`[AUTH] Signup: user="${username}" id=${user.id} admin=${user.is_admin} session=${token} ip=${ipAddress} ua="${userAgent?.slice(0, 50)}..."`);
        library.logActivity("account_created", { title: user.is_admin ? "admin" : "user" }, { id: user.id, username: user.username });
        return Response.json(
          { user: { id: user.id, username: user.username, isAdmin: user.is_admin } },
          { headers: { "Set-Cookie": setSessionCookie(token) } }
        );
      } catch (e) {
        return Response.json({ error: "Signup failed" }, { status: 500 });
      }
    }

    // Auth: login
    if (path === "/api/auth/login" && req.method === "POST") {
      try {
        const { username, password } = await req.json();
        const user = findUserByUsername(username);
        if (!user || !(await validatePassword(user, password))) {
          console.log(`[AUTH] Login failed: user="${username}"`);
          return Response.json({ error: "Invalid username or password" }, { status: 401 });
        }
        const userAgent = req.headers.get("user-agent") ?? undefined;
        const ipAddress = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() 
          ?? req.headers.get("x-real-ip") 
          ?? server.requestIP(req)?.address
          ?? undefined;
        const token = createSession(user.id, userAgent, ipAddress);
        console.log(`[AUTH] Login: user="${username}" id=${user.id} session=${token} ip=${ipAddress} ua="${userAgent?.slice(0, 50)}..."`);
        return Response.json(
          { user: { id: user.id, username: user.username, isAdmin: user.is_admin } },
          { headers: { "Set-Cookie": setSessionCookie(token) } }
        );
      } catch (e) {
        return Response.json({ error: "Login failed" }, { status: 500 });
      }
    }

    // Auth: logout
    if (path === "/api/auth/logout" && req.method === "POST") {
      const token = req.headers.get("cookie")?.match(/musicroom_session=([^;]+)/)?.[1];
      if (token) {
        const user = validateSession(token);
        console.log(`[AUTH] Logout: user="${user?.username ?? "unknown"}" session=${token}`);
        deleteSession(token);
      }
      return Response.json(
        { success: true },
        { headers: { "Set-Cookie": clearSessionCookie() } }
      );
    }

    // Auth: get current user
    if (path === "/api/auth/me") {
      const { user, headers } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ user: null });
      }
      const permissions = getUserPermissions(user.id);
      // Add default permissions for all users (except control for guests)
      const effectivePermissions = [...permissions];
      if (config.defaultPermissions.channel) {
        for (const perm of config.defaultPermissions.channel) {
          // Guests can never have control permission
          if (user.is_guest && perm === "control") continue;
          effectivePermissions.push({
            id: 0,
            user_id: user.id,
            resource_type: "channel",
            resource_id: null,
            permission: perm,
          });
        }
      }
      return Response.json({
        user: { id: user.id, username: user.username, isAdmin: user.is_admin, isGuest: user.is_guest },
        permissions: effectivePermissions,
      }, { headers });
    }

    // Kick all other clients for current user
    if (path === "/api/auth/kick-others" && req.method === "POST") {
      const { user } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Not authenticated" }, { status: 401 });
      }
      
      const connections = userConnections.get(user.id);
      if (!connections || connections.size === 0) {
        return Response.json({ kicked: 0 });
      }
      
      // Get the current request's session to identify which connection NOT to kick
      const token = req.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1];
      let kickedCount = 0;
      
      for (const ws of connections) {
        // Send kick message to all connections (client will handle it)
        ws.send(JSON.stringify({ type: "kick", reason: "Kicked by another session" }));
        kickedCount++;
      }
      
      console.log(`[Kick] User ${user.username} kicked ${kickedCount} other clients`);
      return Response.json({ kicked: kickedCount });
    }

    // Admin: list users
    if (path === "/api/admin/users" && req.method === "GET") {
      try {
        requirePermission(req, "global", null, "admin", server);
        return Response.json(getAllUsers());
      } catch (e) {
        if (e instanceof Response) return e;
        return Response.json({ error: "Failed" }, { status: 500 });
      }
    }

    // Admin: grant permission
    if (path.match(/^\/api\/admin\/users\/(\d+)\/permissions$/) && req.method === "POST") {
      try {
        requirePermission(req, "global", null, "admin", server);
        const userId = parseInt(path.split("/")[4]);
        const { resourceType, resourceId, permission } = await req.json();
        grantPermission(userId, resourceType, resourceId, permission);
        return Response.json({ success: true });
      } catch (e) {
        if (e instanceof Response) return e;
        return Response.json({ error: "Failed" }, { status: 500 });
      }
    }

    // Admin: revoke permission
    if (path.match(/^\/api\/admin\/users\/(\d+)\/permissions$/) && req.method === "DELETE") {
      try {
        requirePermission(req, "global", null, "admin", server);
        const userId = parseInt(path.split("/")[4]);
        const { resourceType, resourceId, permission } = await req.json();
        revokePermission(userId, resourceType, resourceId, permission);
        return Response.json({ success: true });
      } catch (e) {
        if (e instanceof Response) return e;
        return Response.json({ error: "Failed" }, { status: 500 });
      }
    }

    // API: jump to track in queue
    const jumpMatch = path.match(/^\/api\/channels\/([^/]+)\/jump$/);
    if (jumpMatch && req.method === "POST") {
      const channelId = jumpMatch[1];
      const { user } = getOrCreateUser(req, server);
      if (!userHasPermission(user, "channel", channelId, "control")) {
        return new Response("Forbidden", { status: 403 });
      }
      const channel = channels.get(channelId);
      if (!channel) return new Response("Not found", { status: 404 });
      try {
        const body = await req.json();
        if (typeof body.index === "number") {
          channel.jumpTo(body.index);
          return Response.json({ success: true });
        }
        return new Response("Invalid index", { status: 400 });
      } catch {
        return new Response("Invalid JSON", { status: 400 });
      }
    }

    // API: seek in channel
    const seekMatch = path.match(/^\/api\/channels\/([^/]+)\/seek$/);
    if (seekMatch && req.method === "POST") {
      const channelId = seekMatch[1];
      const { user } = getOrCreateUser(req, server);
      if (!userHasPermission(user, "channel", channelId, "control")) {
        return new Response("Forbidden", { status: 403 });
      }
      const channel = channels.get(channelId);
      if (!channel) return new Response("Not found", { status: 404 });
      try {
        const body = await req.json();
        if (typeof body.timestamp === "number") {
          channel.seek(body.timestamp);
          return Response.json({ success: true });
        }
        return new Response("Invalid timestamp", { status: 400 });
      } catch {
        return new Response("Invalid JSON", { status: 400 });
      }
    }

    // API: modify channel queue (add/remove tracks)
    const queueMatch = path.match(/^\/api\/channels\/([^/]+)\/queue$/);
    if (queueMatch && req.method === "PATCH") {
      const channelId = queueMatch[1];
      const { user } = getOrCreateUser(req, server);
      if (!userHasPermission(user, "channel", channelId, "control")) {
        return new Response("Forbidden", { status: 403 });
      }
      const channel = channels.get(channelId);
      if (!channel) return new Response("Not found", { status: 404 });
      
      try {
        const body = await req.json();
        const { add, remove, set } = body;
        
        // If 'set' is provided, replace entire queue
        if (Array.isArray(set)) {
          const tracks = buildTracksFromIds(set, library);
          channel.setQueue(tracks);
          return Response.json({ success: true, queueLength: channel.queue.length });
        }
        
        // Otherwise apply remove then add
        if (Array.isArray(remove) && remove.length > 0) {
          const indices = remove.filter((i: unknown) => typeof i === "number");
          channel.removeTracksByIndex(indices);
        }
        
        if (Array.isArray(add) && add.length > 0) {
          const tracks = buildTracksFromIds(add, library);
          channel.addTracks(tracks);
        }
        
        return Response.json({ success: true, queueLength: channel.queue.length });
      } catch {
        return new Response("Invalid JSON", { status: 400 });
      }
    }

    // API: set channel playback mode
    const modeMatch = path.match(/^\/api\/channels\/([^/]+)\/mode$/);
    if (modeMatch && req.method === "POST") {
      const channelId = modeMatch[1];
      const { user } = getOrCreateUser(req, server);
      if (!userHasPermission(user, "channel", channelId, "control")) {
        return new Response("Forbidden", { status: 403 });
      }
      const channel = channels.get(channelId);
      if (!channel) return new Response("Not found", { status: 404 });
      try {
        const body = await req.json();
        const validModes = ["once", "repeat-all", "repeat-one", "shuffle"];
        if (typeof body.mode === "string" && validModes.includes(body.mode)) {
          channel.setPlaybackMode(body.mode);
          return Response.json({ success: true, playbackMode: channel.playbackMode });
        }
        return new Response("Invalid mode", { status: 400 });
      } catch {
        return new Response("Invalid JSON", { status: 400 });
      }
    }

    // API: serve audio file (requires auth or guest)
    // Supports both filename and track ID (sha256:...)
    const trackMatch = path.match(/^\/api\/tracks\/(.+)$/);
    if (trackMatch) {
      const { user } = getOrCreateUser(req, server);
      if (!user) {
        return Response.json({ error: "Authentication required" }, { status: 401 });
      }
      const identifier = decodeURIComponent(trackMatch[1]);
      if (identifier.includes("..")) return new Response("Forbidden", { status: 403 });

      let filepath: string;
      if (identifier.startsWith("sha256:")) {
        // Track ID - look up in library
        const trackPath = library.getFilePath(identifier);
        if (!trackPath) return new Response("Not found", { status: 404 });
        filepath = trackPath;
      } else {
        // Filename - direct path
        filepath = join(MUSIC_DIR, identifier);
      }

      const f = file(filepath);
      if (!(await f.exists())) return new Response("Not found", { status: 404 });

      const size = f.size;
      const range = req.headers.get("range");

      if (range) {
        const match = range.match(/bytes=(\d+)-(\d*)/);
        if (match) {
          const start = parseInt(match[1]);
          const end = match[2] ? parseInt(match[2]) : size - 1;
          const chunk = f.slice(start, end + 1);
          return new Response(chunk, {
            status: 206,
            headers: {
              "Content-Range": `bytes ${start}-${end}/${size}`,
              "Accept-Ranges": "bytes",
              "Content-Length": String(end - start + 1),
              "Content-Type": f.type || "audio/mpeg",
            },
          });
        }
      }

      return new Response(f, {
        headers: {
          "Accept-Ranges": "bytes",
          "Content-Length": String(size),
          "Content-Type": f.type || "audio/mpeg",
        },
      });
    }

    // Serve static client
    if (path === "/" || path === "/index.html") {
      return new Response(file(join(PUBLIC_DIR, "index.html")), {
        headers: { "Content-Type": "text/html" },
      });
    }
    if (path === "/styles.css") {
      return new Response(file(join(PUBLIC_DIR, "styles.css")), {
        headers: { "Content-Type": "text/css" },
      });
    }
    // Serve JS files from public directory
    if (path.endsWith(".js")) {
      const jsFile = file(join(PUBLIC_DIR, path.slice(1)));
      if (await jsFile.exists()) {
        return new Response(jsFile, {
          headers: { "Content-Type": "application/javascript" },
        });
      }
    }

    return new Response("Not found", { status: 404 });
  },

  websocket: {
    open(ws: ServerWebSocket<WsData>) {
      const channel = channels.get(ws.data.channelId);
      if (channel) {
        channel.addClient(ws);
        // Broadcast updated channel list to all clients
        broadcastChannelList();
      }
      // Track connection by user ID
      const userId = ws.data.userId;
      if (userId) {
        if (!userConnections.has(userId)) {
          userConnections.set(userId, new Set());
        }
        userConnections.get(userId)!.add(ws);
      }
    },
    close(ws: ServerWebSocket<WsData>) {
      const channel = channels.get(ws.data.channelId);
      if (channel) {
        channel.removeClient(ws);
        broadcastChannelList();
      }
      // Remove from user connections tracking
      const userId = ws.data.userId;
      if (userId && userConnections.has(userId)) {
        userConnections.get(userId)!.delete(ws);
        if (userConnections.get(userId)!.size === 0) {
          userConnections.delete(userId);
        }
      }
    },
    message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
      try {
        const data = JSON.parse(String(message));
        
        // Handle channel switching
        if (data.action === "switch" && data.channelId) {
          const oldChannel = channels.get(ws.data.channelId);
          const newChannel = channels.get(data.channelId);
          if (!newChannel) {
            ws.send(JSON.stringify({ type: "error", message: "Channel not found" }));
            return;
          }
          if (oldChannel) oldChannel.removeClient(ws);
          ws.data.channelId = data.channelId;
          newChannel.addClient(ws);
          ws.send(JSON.stringify({ type: "switched", channelId: data.channelId }));
          broadcastChannelList();
          return;
        }
        
        const channel = channels.get(ws.data.channelId);
        if (!channel) {
          console.log("[WS] No channel found for:", ws.data.channelId);
          return;
        }
        
        // Check permission for control actions
        const userId = ws.data.userId;
        if (!userId) {
          console.log("[WS] No userId on connection");
          return;
        }
        
        const user = findUserById(userId);
        if (!user) {
          console.log("[WS] User not found:", userId);
          return;
        }
        
        // Guests can never control playback
        if (user.is_guest) {
          console.log("[WS] Guest cannot control playback");
          return;
        }
        
        // Check default permissions or user-specific permissions
        const canControl = user.is_admin 
          || config.defaultPermissions.channel?.includes("control")
          || hasPermission(userId, "channel", ws.data.channelId, "control");
        if (!canControl) {
          console.log("[WS] User lacks control permission:", user.username);
          return;
        }
        
        console.log("[WS] Control action:", data.action, "from", user.username);
        if (data.action === "pause") channel.pause();
        else if (data.action === "unpause") channel.unpause();
        else if (data.action === "seek" && typeof data.timestamp === "number") channel.seek(data.timestamp);
        else if (data.action === "jump" && typeof data.index === "number") channel.jumpTo(data.index);
      } catch {}
    },
  },
});

console.log(`MusicRoom running on http://localhost:${config.port}`);
