// TODO: server
// Peer mssages ✅
// Routing ✅
// Video files being fully sent ❓
// Use Deno static serving for static ✅
// Use Workers, at least for serving static files. Why not nginx? Single binary server.

// import { serveDir } from "jsr:@std/http/file-server"

import { brotli } from "jsr:@deno-library/compress";

const superLog=true;

const memoryCache = true;
const filepathResponseCache: Map<string, Response> = new Map();
// deno-lint-ignore-file prefer-const no-explicit-any
async function serveFile(filename: string) {
    if (!memoryCache) {
        const file = await Deno.readFile("../" + filename);
        const newResponse = new Response(file);
        if (filename.endsWith('.js')) {
            newResponse.headers.set('content-type', 'application/javascript')
        }
        return newResponse;
    }

    const response = filepathResponseCache.get(filename);

    if (response) {
        // console.log('serveFile: cache hit:', filename);
        return response.clone();
    }


    const file = await Deno.readFile("../" + filename);

    const compressed = await brotli.compress(file);

    const newResponse = await new Response(compressed);
    // const newResponse = await new Response(file);

    newResponse.headers.set('Cache-Control', 'no-cache, no-transform');
    newResponse.headers.set('Content-Encoding', 'br');
    

    if (filename.endsWith('.js')) {
        newResponse.headers.set('content-type', 'application/javascript')
    }

    console.log(`Caching: ${filename}`);
    filepathResponseCache.set(filename, newResponse);

    return newResponse.clone();
}

function hashIdToNumber(id: string, range: number) {
    let hash = 0x811c9dc5
    for (const char of id) {
        if (char !== '0' && char !== '-') {
            hash ^= char.charCodeAt(0);
            hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);

        }

    }
    return (hash >>> 0) % range;
}

const colors = [
    160, 196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49,
    51, 45, 44, 43, 42, 41, 40, 39, 33, 27, 21, 57, 93, 129, 165, 201,
];

const resetCode = "\x1b[0m";
function colorID(id: string) {
    if (typeof id !== 'string') {
        console.error(`colorID: expected string but got `, id);
        return "";
    }
    const colorCode = `\x1b[38;5;${colors[hashIdToNumber(id, colors.length)]}m`
    return `${colorCode}${id.substring(0, 5)}${resetCode}`
}


function pingHandler(m: any) {
    const time = Temporal.Now.zonedDateTimeISO();
    // console.log("ping", m);
    console.log(time, `ping handler ${colorID(m.peer_id)}:${m.peer_name} ${colorID(m.user_id)}:${m.user_name}`);
    return '{"type":"pong"}'
}

interface HelloMessage {
    type: string
    user_id: string
    user_name: string
    peer_id: string
    peer_name: string
    known_users: string[]
}

interface Hello2Message {
    type: string
    user_id: string
    user_name: string
    peer_id: string
    peer_name: string
    is_bootstrap_peer: boolean
    // peer_description:RTCSessionDescription
}

// interface PeerState {
//     socket:WebSocket;
//     lastSeen: number;
// }

// const peerStates:Map<string, PeerState> = new Map();
const bootstrapPeers: Set<string> = new Set(); 
const userPeers: Map<string, Set<string>> = new Map();
const peerSockets: Map<string, WebSocket> = new Map();
const socketPeers: Map<WebSocket, string> = new Map();

// function updatePeerState(peerID:string, socket:WebSocket) {

// }

function hello2Handler(m:Hello2Message, socket:WebSocket) {
    // bootstrapPeers.add(m.)
    console.log(`[>hello2]: peer ${colorID(m.peer_id)}:${m.peer_name}, user ${colorID(m.user_id)}:${m.user_name}`);

    if (m.is_bootstrap_peer) {
        bootstrapPeers.add(m.peer_id);
        console.log(`This is a bootstrap peer. bootstrap peers:${[...bootstrapPeers.values()]}`);
    }

    if (!userPeers.has(m.user_id)) {
        userPeers.set(m.user_id, new Set());
    }
    userPeers.get(m.user_id)?.add(m.peer_id);
    peerSockets.set(m.peer_id, socket); // TODO:MAYBEBUG - what happens with multiple windows each with their own websocket?
    socketPeers.set(socket, m.peer_id);

    console.log(userPeers.get(m.user_id));

    if (!m.is_bootstrap_peer) {
        
        return JSON.stringify({ type: 'hello2', bootstrapPeers: [...bootstrapPeers.values()] });
    }

    return JSON.stringify({type:'hello2', message:'hello2 bootstrap peer!'});

}

function helloHandler(m: HelloMessage, socket: WebSocket) {
    console.log(`Received hello from peer ${colorID(m.peer_id)}:${m.peer_name}, user ${colorID(m.user_id)}:${m.user_name}`);


    if (!userPeers.has(m.user_id)) {
        userPeers.set(m.user_id, new Set());
    }
    userPeers.get(m.user_id)?.add(m.peer_id);
    peerSockets.set(m.peer_id, socket); // TODO:MAYBEBUG - what happens with multiple windows each with their own websocket?
    socketPeers.set(socket, m.peer_id);

    if (Symbol.iterator in Object(m.known_users)) {
        for (const knownUserID of m.known_users) {
            console.log(`Adding user ${knownUserID} from peer ${colorID(m.peer_id)}`);
            if (!userPeers.get(knownUserID)) {
                userPeers.set(knownUserID, new Set());
            }

            userPeers.get(knownUserID)?.add(m.peer_id);
        }
    }

    const returnValue: any = {};
    for (const key of userPeers.keys()) {
        const peers = userPeers.get(key);
        if (!peers || peers.size === 0) {
            continue;
        }
        returnValue[key] = [...peers.keys()];
    }


    // console.log(returnValue);

    return JSON.stringify({ type: 'hello', userPeers: returnValue });
}

interface InnerMessage {
    type: string
    user_id: string
}

interface PeerMessage {
    type: string
    from: string
    from_username: string
    from_peername: string
    to: string
    message: InnerMessage
}

function peerMessageHandler(m: PeerMessage, _socket: WebSocket) {
    const time = Temporal.Now.zonedDateTimeISO();

    console.log(`[${time}]pm:${m.message.type} f:${colorID(m.from)}:${m.from_peername}:${m.from_username} t:${colorID(m.to)}`)
    
    const toPeer = peerSockets.get(m.to);
    if (!toPeer) {
        console.log(`Couln't find peer ${m.to}`)
        return null;
    }

    if (toPeer.readyState !== WebSocket.OPEN) {
        console.log("Peer socket is not open:", toPeer);
        deletePeerFromUserPeers(m.to);
        return null;
    }

    const messageToSend = JSON.stringify(m);
    // console.log("ws->", toPeer, messageToSend);

    console.log("peerMessageHandler: before toPeer.send");
    toPeer.send(messageToSend)
    console.log("peerMessageHandler: after toPeer.send");
    return null;
}

const messageDispatch: Map<string, (m: any, socket: WebSocket) => string | null> = new Map();

function deletePeerFromUserPeers(peerIDToDelete: string) {
    for (const peers of userPeers.values()) {
        for (const peerID of peers) {
            if (peerID === peerIDToDelete) {
                peers.delete(peerIDToDelete);
            }
        }
    }
}

function connectWebsocket(request: Request) {
    if (request.headers.get("upgrade") != "websocket") {
        return new Response(null, { status: 501 });
    }

    const { socket, response } = Deno.upgradeWebSocket(request);
    socket.addEventListener("open", () => {
        console.log("New peer websocket connection");
    });
    socket.addEventListener("message", (event) => {
        // console.log(event);
        let message: any;
        try {
            message = JSON.parse(event.data);
        } catch (e) {
            console.error("socket.message: ", e);
            return null;
        }

        superLog && console.log(message);

        const dispatchHandler = messageDispatch.get(message?.type)
        if (!dispatchHandler) {
            console.log("Got message I don't understand: ", event.data);
            return;
        }


        const response = dispatchHandler(message, socket);
        // console.log(response);
        if (response) {
            socket.send(response);
        }

    });

    socket.addEventListener("close", (event: CloseEvent) => {
        const peerID = socketPeers.get(socket);
        if (!peerID) {
            console.log("Websocket close: couldn't find peer 🤔");
            return;
        }
        console.log("Websocket close:", colorID(peerID), `code:${event.code} reason:${event.reason} wasClean: ${event.wasClean}`);
        
        bootstrapPeers.delete(peerID);
        peerSockets.delete(peerID);
        deletePeerFromUserPeers(peerID);
    });

    return response;

}

async function devServerWatchFiles() {
    const parentDir = Deno.cwd().replace(/\/[^/]+$/, '');
    const watcher = Deno.watchFs(["../static/", "../src/"]);
    for await (const event of watcher) {
        if (event.kind === "modify") {
            for (const path of event.paths) {
                const cachedPath = path.replace(parentDir, '');
                filepathResponseCache.delete(cachedPath);
                console.log('Purging updated file:', cachedPath)
                console.log(filepathResponseCache);
            }

        }
    }
}

function handler(request: Request, info: any): Promise<Response> | Response {
    if (request.url === "https://ddln.app/") {
        return serveFile("/static/index.html")
    }

    // console.log(info.remoteAddr.hostname, request.url, request.headers.get('user-agent'));

    const url = new URL(request.url);

    if (url.pathname.endsWith('mp4') || url.pathname.endsWith('webm')) {
        console.log("Not serving video...");
        return new Response("Not serving video", { status: 404 });
    }

    if (url.pathname.includes("/src")) {
         return serveFile(url.pathname);
    }

    if (url.pathname === "/") {
        return serveFile("/static/index.html")
    }

    if (url.pathname === "/ws") {
        return connectWebsocket(request);
    }


    if (url.pathname === "/sw.js") {
        return serveFile("static/sw.js")
    }

    if (url.pathname === "/robots.txt") {
        return serveFile("static/robots.txt")
    }

    if (url.pathname === "/favicon.ico") {
        return serveFile("static/favicon.ico")
    }


    if (url.pathname.includes("/static/")) {
        return serveFile(url.pathname);
    }

    return serveFile("/static/index.html")
}

async function main() {

    messageDispatch.set('ping', pingHandler);
    messageDispatch.set('hello', helloHandler);
    messageDispatch.set('hello2', hello2Handler);
    messageDispatch.set('peer_message', peerMessageHandler);

    const port = parseInt(Deno.env.get("PORT") ?? "443");
    const certFile = Deno.env.get("TLS_CERT") ?? "/etc/letsencrypt/live/ddln.app/fullchain.pem";
    const keyFile  = Deno.env.get("TLS_KEY")  ?? "/etc/letsencrypt/live/ddln.app/privkey.pem";

    Deno.serve({
        hostname: "[::]",
        port,
        cert: Deno.readTextFileSync(certFile),
        key:  Deno.readTextFileSync(keyFile),
    }, handler);

    await devServerWatchFiles();
}

await main();
