import { file, serve, type ServerWebSocket } from "bun";
import { parseFile } from "music-metadata";
import { Stream, type Track } from "./stream";
import { readdir, stat } from "fs/promises";
import { join } from "path";

const MUSIC_DIR = join(import.meta.dir, "music");
const PLAYLIST_PATH = join(import.meta.dir, "playlist.json");
const PUBLIC_DIR = join(import.meta.dir, "public");

// Load track metadata
async function loadTrack(filename: string): Promise<Track> {
  const filepath = join(MUSIC_DIR, filename);
  try {
    const metadata = await parseFile(filepath, { duration: true });
    const duration = metadata.format.duration ?? 0;
    const title = metadata.common.title ?? filename.replace(/\.[^.]+$/, "");
    console.log(`Track: ${filename} | duration: ${duration}s | title: ${title}`);
    return { filename, title, duration };
  } catch (e) {
    console.warn(`Could not read metadata for ${filename}, skipping`);
    return { filename, title: filename.replace(/\.[^.]+$/, ""), duration: 0 };
  }
}

// Auto-discover tracks if playlist 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 [];
  }
}

// Initialize streams
async function init(): Promise<Map<string, Stream>> {
  const playlistData = await file(PLAYLIST_PATH).json();
  const streams = new Map<string, Stream>();

  for (const cfg of playlistData.streams) {
    let trackFiles: string[] = cfg.tracks;
    if (trackFiles.length === 0) {
      trackFiles = await discoverTracks();
      console.log(`Stream "${cfg.id}": auto-discovered ${trackFiles.length} tracks`);
    }
    const tracks = await Promise.all(trackFiles.map(loadTrack));
    const validTracks = tracks.filter((t) => t.duration > 0);
    if (validTracks.length === 0) {
      console.warn(`Stream "${cfg.id}" has no valid tracks, skipping`);
      continue;
    }
    const stream = new Stream({ id: cfg.id, name: cfg.name, tracks: validTracks });
    streams.set(cfg.id, stream);
    console.log(`Stream "${cfg.id}": ${validTracks.length} tracks loaded`);
  }

  return streams;
}

const streams = await init();

// Tick interval: advance tracks when needed, broadcast every 30s
let tickCount = 0;
setInterval(() => {
  tickCount++;
  for (const stream of streams.values()) {
    const changed = stream.tick();
    if (!changed && tickCount % 30 === 0) {
      stream.broadcast();
    }
  }
}, 1000);

type WsData = { streamId: string };

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

    // WebSocket upgrade
    if (path.match(/^\/api\/streams\/([^/]+)\/ws$/)) {
      const id = path.split("/")[3];
      if (!streams.has(id)) return new Response("Stream not found", { status: 404 });
      const ok = server.upgrade(req, { data: { streamId: id } });
      if (ok) return undefined;
      return new Response("WebSocket upgrade failed", { status: 500 });
    }

    // API: list streams
    if (path === "/api/streams") {
      const list = [...streams.values()].map((s) => ({
        id: s.id,
        name: s.name,
        trackCount: s.playlist.length,
      }));
      return Response.json(list);
    }

    // API: stream state
    const streamMatch = path.match(/^\/api\/streams\/([^/]+)$/);
    if (streamMatch) {
      const stream = streams.get(streamMatch[1]);
      if (!stream) return new Response("Not found", { status: 404 });
      return Response.json(stream.getState());
    }

    // API: serve audio file
    const trackMatch = path.match(/^\/api\/tracks\/(.+)$/);
    if (trackMatch) {
      const filename = decodeURIComponent(trackMatch[1]);
      if (filename.includes("..")) return new Response("Forbidden", { status: 403 });
      const filepath = join(MUSIC_DIR, filename);
      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" },
      });
    }

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

  websocket: {
    open(ws: ServerWebSocket<WsData>) {
      const stream = streams.get(ws.data.streamId);
      if (stream) stream.addClient(ws);
    },
    close(ws: ServerWebSocket<WsData>) {
      const stream = streams.get(ws.data.streamId);
      if (stream) stream.removeClient(ws);
    },
    message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
      const stream = streams.get(ws.data.streamId);
      if (!stream) return;
      try {
        const data = JSON.parse(String(message));
        if (data.action === "pause") stream.pause();
        else if (data.action === "unpause") stream.unpause();
      } catch {}
    },
  },
});

console.log("MusicRoom running on http://localhost:3001");
