import {
    Client,
    clientSiteResourcesAssociationsCache,
    db,
    ExitNode,
    Org,
    orgs,
    roleClients,
    roles,
    siteResources,
    Transaction,
    userClients,
    userOrgs,
    users
} from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import {
    clients,
    clientSitesAssociationsCache,
    exitNodes,
    Olm,
    olms,
    sites
} from "@server/db";
import { and, eq, inArray, isNotNull, isNull } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger";
import { listExitNodes } from "#dynamic/lib/exitNodes";
import {
    generateAliasConfig,
    getNextAvailableClientSubnet
} from "@server/lib/ip";
import { generateRemoteSubnets } from "@server/lib/ip";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { validateSessionToken } from "@server/auth/sessions/app";
import config from "@server/lib/config";
import {
    addPeer as newtAddPeer,
    deletePeer as newtDeletePeer
} from "@server/routers/newt/peers";

export const handleOlmServerPeerAddMessage: MessageHandler = async (
    context
) => {
    logger.info("Handling register olm message!");
    const { message, client: c, sendToClient } = context;
    const olm = c as Olm;

    const now = Math.floor(Date.now() / 1000);

    if (!olm) {
        logger.warn("Olm not found");
        return;
    }

    const { siteId } = message.data;

    // get the site
    const [site] = await db
        .select()
        .from(sites)
        .where(eq(sites.siteId, siteId))
        .limit(1);

    if (!site) {
        logger.error(
            `handleOlmServerPeerAddMessage: Site with ID ${siteId} not found`
        );
        return;
    }

    if (!site.endpoint) {
        logger.error(
            `handleOlmServerPeerAddMessage: Site with ID ${siteId} has no endpoint`
        );
        return;
    }

    // get the client

    if (!olm.clientId) {
        logger.error(
            `handleOlmServerPeerAddMessage: Olm with ID ${olm.olmId} has no clientId`
        );
        return;
    }

    const [client] = await db
        .select()
        .from(clients)
        .where(and(eq(clients.clientId, olm.clientId)))
        .limit(1);

    if (!client) {
        logger.error(
            `handleOlmServerPeerAddMessage: Client with ID ${olm.clientId} not found`
        );
        return;
    }

    if (!client.pubKey) {
        logger.error(
            `handleOlmServerPeerAddMessage: Client with ID ${client.clientId} has no public key`
        );
        return;
    }

    let endpoint: string | null = null;

    // TODO: should we pick only the one from the site its talking to instead of any good current session?
    const currentSessionSiteAssociationCaches = await db
        .select()
        .from(clientSitesAssociationsCache)
        .where(
            and(
                eq(clientSitesAssociationsCache.clientId, client.clientId),
                isNotNull(clientSitesAssociationsCache.endpoint),
                eq(clientSitesAssociationsCache.publicKey, client.pubKey) // limit it to the current session its connected with otherwise the endpoint could be stale
            )
        );

    // pick an endpoint
    for (const assoc of currentSessionSiteAssociationCaches) {
        if (assoc.endpoint) {
            endpoint = assoc.endpoint;
            break;
        }
    }

    if (!endpoint) {
        logger.error(
            `handleOlmServerPeerAddMessage: No endpoint found for client ${client.clientId}`
        );
        return;
    }

    // NOTE: here we are always starting direct to the peer and will relay later

    await newtAddPeer(siteId, {
        publicKey: client.pubKey,
        allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
        endpoint: endpoint // this is the client's endpoint with reference to the site's exit node
    });

    const allSiteResources = await db // only get the site resources that this client has access to
        .select()
        .from(siteResources)
        .innerJoin(
            clientSiteResourcesAssociationsCache,
            eq(
                siteResources.siteResourceId,
                clientSiteResourcesAssociationsCache.siteResourceId
            )
        )
        .where(
            and(
                eq(siteResources.siteId, site.siteId),
                eq(
                    clientSiteResourcesAssociationsCache.clientId,
                    client.clientId
                )
            )
        );

    // Return connect message with all site configurations
    return {
        message: {
            type: "olm/wg/peer/add",
            data: {
                siteId: site.siteId,
                name: site.name,
                endpoint: site.endpoint,
                publicKey: site.publicKey,
                serverIP: site.address,
                serverPort: site.listenPort,
                remoteSubnets: generateRemoteSubnets(
                    allSiteResources.map(({ siteResources }) => siteResources)
                ),
                aliases: generateAliasConfig(
                    allSiteResources.map(({ siteResources }) => siteResources)
                )
            }
        },
        broadcast: false,
        excludeSender: false
    };
};
