/**
 * @fileoverview API server factory module.
 * Exports a function to create and start the API server.
 * This allows the server to be started in-process from the main application.
 * 
 * Routes are organized into modular files in the ./routes directory.
 * Each route module handles its own validation, business logic, and responses.
 */

import { serve, file } from "bun";
import { logger } from "@shared/lib/logger";
import { handleRequest } from "./routes";
import { getFullDashboardStats } from "./routes/stats.helper";
import { join } from "path";
import { handleGameMessage, roomManager } from "./games/ws-handler";
import { getSession } from "./routes/auth.routes";

export interface WebServerConfig {
    port?: number;
    hostname?: string;
}

export interface WebServerInstance {
    server: ReturnType<typeof serve>;
    stop: () => Promise<void>;
    url: string;
}

/**
 * Creates and starts the API server.
 * 
 * @param config - Server configuration options
 * @param config.port - Port to listen on (default: 3000)
 * @param config.hostname - Hostname to bind to (default: "localhost")
 * @returns Promise resolving to server instance with stop() method
 * 
 * @example
 * const server = await createWebServer({ port: 3000, hostname: "0.0.0.0" });
 * console.log(`Server running at ${server.url}`);
 * 
 * // To stop the server:
 * await server.stop();
 */
const MIME_TYPES: Record<string, string> = {
    ".html": "text/html",
    ".js": "application/javascript",
    ".css": "text/css",
    ".json": "application/json",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".svg": "image/svg+xml",
    ".ico": "image/x-icon",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
};

/**
 * Serve static files from the panel dist directory.
 * Falls back to index.html for SPA routing.
 */
async function servePanelStatic(pathname: string, distDir: string): Promise<Response | null> {
    // Don't serve panel for API/auth/ws/assets routes
    if (pathname.startsWith("/api/") || pathname.startsWith("/auth/") || pathname === "/ws" || pathname.startsWith("/assets/")) {
        return null;
    }

    // Try to serve the exact file
    const filePath = join(distDir, pathname);
    const bunFile = file(filePath);
    if (await bunFile.exists()) {
        const ext = pathname.substring(pathname.lastIndexOf("."));
        const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
        return new Response(bunFile, {
            headers: {
                "Content-Type": contentType,
                "Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=31536000, immutable",
            },
        });
    }

    // SPA fallback: serve index.html for all non-file routes
    const indexFile = file(join(distDir, "index.html"));
    if (await indexFile.exists()) {
        return new Response(indexFile, {
            headers: { "Content-Type": "text/html", "Cache-Control": "no-cache" },
        });
    }

    return null;
}

export async function createWebServer(config: WebServerConfig = {}): Promise<WebServerInstance> {
    const { port = 3000, hostname = "localhost" } = config;

    // Configuration constants
    const MAX_CONNECTIONS = 200;
    const MAX_PAYLOAD_BYTES = 16384; // 16KB
    const IDLE_TIMEOUT_SECONDS = 60;

    // Interval for broadcasting stats to all connected WS clients
    let statsBroadcastInterval: Timer | undefined;

    const server = serve({
        port,
        hostname,
        async fetch(req, server) {
            const url = new URL(req.url);

            // WebSocket upgrade handling
            if (url.pathname === "/ws") {
                const currentConnections = server.pendingWebSockets;
                if (currentConnections >= MAX_CONNECTIONS) {
                    logger.warn("web", `Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`);
                    return new Response("Connection limit reached", { status: 429 });
                }

                const session = getSession(req);
                const success = server.upgrade(req, { data: { session } });
                if (success) return undefined;
                return new Response("WebSocket upgrade failed", { status: 400 });
            }

            // Delegate to modular route handlers
            const response = await handleRequest(req, url);
            if (response) return response;

            // Serve panel static files (production)
            const panelDistDir = join(import.meta.dir, "../../panel/dist");
            const staticResponse = await servePanelStatic(url.pathname, panelDistDir);
            if (staticResponse) return staticResponse;

            // No matching route found
            return new Response("Not Found", { status: 404 });
        },

        websocket: {
            /**
             * Called when a WebSocket client connects.
             * Subscribes the client to the dashboard channel and sends initial stats.
             */
            open(ws) {
                ws.subscribe("dashboard");
                ws.subscribe("lobby");
                logger.debug("web", `Client connected. Total: ${server.pendingWebSockets}`);

                // Send initial stats
                getFullDashboardStats().then(stats => {
                    ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats }));
                });

                // Send initial room list
                ws.send(JSON.stringify({ type: "ROOM_LIST_UPDATE", rooms: roomManager.listRooms() }));

                // Start broadcast interval if this is the first client
                if (!statsBroadcastInterval) {
                    statsBroadcastInterval = setInterval(async () => {
                        try {
                            const stats = await getFullDashboardStats();
                            server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats }));
                        } catch (error) {
                            logger.error("web", "Error in stats broadcast", error);
                        }
                    }, 5000);
                }
            },

            /**
             * Called when a WebSocket message is received.
             * Handles PING/PONG heartbeat messages.
             */
            async message(ws, message) {
                try {
                    const messageStr = message.toString();

                    // Defense-in-depth: redundant length check before parsing
                    if (messageStr.length > MAX_PAYLOAD_BYTES) {
                        logger.error("web", "Payload exceeded maximum limit");
                        return;
                    }

                    const rawData = JSON.parse(messageStr);
                    const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types");
                    const parsed = WsMessageSchema.safeParse(rawData);

                    if (!parsed.success) {
                        logger.error("web", "Invalid message format", parsed.error.issues);
                        return;
                    }

                    if (parsed.data.type === "PING") {
                        ws.send(JSON.stringify({ type: "PONG" }));
                    }

                    // Route game messages
                    const gameTypes = ["CREATE_ROOM", "JOIN_ROOM", "LEAVE_ROOM", "GAME_ACTION"];
                    if (gameTypes.includes(parsed.data.type)) {
                        const sessionData = (ws.data as any)?.session;
                        if (!sessionData) {
                            ws.send(JSON.stringify({ type: "ERROR", message: "Not authenticated" }));
                            return;
                        }
                        handleGameMessage(parsed.data as any, {
                            playerId: sessionData.discordId,
                            username: sessionData.username,
                            send: (data) => ws.send(data),
                            subscribe: (channel) => ws.subscribe(channel),
                            unsubscribe: (channel) => ws.unsubscribe(channel),
                            publish: (channel, data) => server.publish(channel, data),
                        });
                    }
                } catch (e) {
                    logger.error("web", "Failed to handle message", e);
                }
            },

            /**
             * Called when a WebSocket client disconnects.
             * Stops the broadcast interval if no clients remain.
             */
            close(ws) {
                ws.unsubscribe("dashboard");
                ws.unsubscribe("lobby");
                logger.debug("web", `Client disconnected. Total remaining: ${server.pendingWebSockets}`);

                // Stop broadcast interval if no clients left
                if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
                    clearInterval(statsBroadcastInterval);
                    statsBroadcastInterval = undefined;
                }
            },
            maxPayloadLength: MAX_PAYLOAD_BYTES,
            idleTimeout: IDLE_TIMEOUT_SECONDS,
        },
    });

    // Listen for real-time events from the system bus
    const { systemEvents, EVENTS } = await import("@shared/lib/events");
    systemEvents.on(EVENTS.DASHBOARD.NEW_EVENT, (event) => {
        server.publish("dashboard", JSON.stringify({ type: "NEW_EVENT", data: event }));
    });

    const url = `http://${hostname}:${port}`;

    return {
        server,
        url,
        stop: async () => {
            if (statsBroadcastInterval) {
                clearInterval(statsBroadcastInterval);
            }
            server.stop(true);
        },
    };
}

/**
 * Starts the web server from the main application root.
 * Kept for backward compatibility.
 * 
 * @param webProjectPath - Deprecated, no longer used
 * @param config - Server configuration options
 * @returns Promise resolving to server instance
 */
export async function startWebServerFromRoot(
    webProjectPath: string,
    config: WebServerConfig = {}
): Promise<WebServerInstance> {
    return createWebServer(config);
}
