import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
import { getOwnMachineId } from "sliftutils/misc/https/certs";
import { devDebugbreak, getDomain, isLocal, isPublic, noSyncing } from "../config";
import child_process from "child_process";
import { getAllNodeIds, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
import { getArchivesBackblazePublic } from "../-a-archives/archivesBackBlaze";
import { nestArchives } from "../-a-archives/archives";
import { SocketFunction } from "socket-function/SocketFunction";
import { delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
import { compare, compareArray, isNodeTrue, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
import { cacheLimited, lazy } from "socket-function/src/caching";
import { canHaveChildren } from "socket-function/src/types";
import { hostArchives } from "../-b-authorities/cdnAuthority";
import { getModuleFromConfig } from "../3-path-functions/pathFunctionLoader";
import path from "path";
import debugbreak from "debugbreak";
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
import { getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
import { blue, magenta } from "socket-function/src/formatting/logColors";
import { errorToUndefined } from "../errors";
import { deploySchema } from "./deploySchema";
import { atomic, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
import { Querysub } from "../4-querysub/Querysub";
import { onEdgeNodesChanged } from "./edgeBootstrap";
import { startEdgeNotifier } from "./edgeClientWatcher";
import { getGitRefLive, getGitURLLive } from "./git";
import { DeployProgress } from "./deployFunctions";
import { PromiseObj } from "../promise";
import type { FunctionRunnerIndex } from "../4-querysub/FunctionRunnerTracking";

const UPDATE_POLL_INTERVAL = timeInMinute * 15;
const DEAD_NODE_COUNT_THRESHOLD = 15;

const edgeNodeStorage = isNodeTrue() && nestArchives("edgenodes/", getArchivesBackblazePublic(getDomain()));
const edgeNodeIndexFile = "edge-nodes-index.json";

const getEdgeNodeConfig = cacheLimited(10000, async (fileName: string): Promise<EdgeNodeConfig | undefined> => {
    let edgeNodeConfig = await edgeNodeStorage.get(fileName);
    if (!edgeNodeConfig) return undefined;
    let obj = JSON.parse(edgeNodeConfig.toString());
    obj.fileName = fileName;
    return obj;
});
let nextNodeNum = 1;
let registeredNodePaths = new Set<string>();
function getNextNodePath() {
    let path = "node" + (nextNodeNum++) + "_" + getOwnNodeId();
    registeredNodePaths.add(path);
    return path;
}
function getNodeIdFromPath(path: string) {
    return path.split("_").slice(1).join("_");
}

let edgeShutdown = false;

export type EdgeNodesIndex = {
    edgeNodes: EdgeNodeConfig[];
    liveHash: string;
};


// IMPORTANT! The EdgeNodeConfig MUST be immutable, otherwise every node has to read every EdgeNodeConfig
//  every poll, which is too much reading.
export type EdgeNodeConfig = {
    // NOTE: Only information needed for routing should be added here. The rest can be obtained by talking
    //  to the node itself (after picking the edgeNode node).
    nodeId: string;
    // (Redundant from nodeId)
    machineId: string;
    // EX: 127-0-0-1.example.com:3334
    host: string;
    gitHash: string;
    bootTime: number;
    entryPaths: string[];
    public: boolean;
    // Set once this node has been told it will shut down (scheduled switchover). Clients deprioritize these nodes when picking an edge node.
    scheduledShutdownTime?: number;
    // The function runner index at registration time, so clients have it immediately at startup (they refresh it by polling QuerysubController.getFunctionRunnerIndex).
    functionRunnerIndex?: FunctionRunnerIndex;
};

export type EdgeNodeStat = {
    host: string;
    nodeId: string;
    public: boolean;
    live: boolean;
    // Only set once the probe finishes
    latency?: number;
    alive?: boolean;
    finished: boolean;
};
export type EdgeNodeStats = {
    nodes: EdgeNodeStat[];
    /** The node we actually booted from */
    pickedHost: string;
    /** What the picking algorithm chose (or would have chosen, when overridden) on its own. Empty until its probe decides. */
    autoPickedHost: string;
    /** Set when a url override requested a specific host, even if we couldn't connect to it (compare with pickedHost to detect that) */
    requestedHost?: string;
};
declare global {
    // Written by the edge bootstrapper as it probes the edge nodes, so the edge node dropdown can show what there was to pick from (and their latencies)
    var EDGE_NODE_STATS: EdgeNodeStats | undefined;
}
let registeredEdgeNode: { host: string; entryPaths: string[] } | boolean | undefined;
export async function registerEdgeNode(config: {
    host: string;
    entryPaths: string[];
}) {
    if (!SocketFunction.isMounted()) {
        throw new Error("registerEdgeNode must be called after SocketFunction.mount");
    }
    console.log(magenta(`registerEdgeNode ${JSON.stringify(config.host)}`));

    if (registeredEdgeNode) {
        throw new Error(`registerEdgeNode already called. Should only host 1 edgeNode per node. Previously registered with ${JSON.stringify(registeredEdgeNode)}, new call used ${JSON.stringify(config)}`);
    }
    registeredEdgeNode = true;
    let host = config.host;
    await delay(0);

    const { registerShutdownHandler } = await import("../diagnostics/periodic");
    registerShutdownHandler(async () => {
        edgeShutdown = true;
        console.log(magenta(`Removing node from edge node list due to shutdown`));
        await Promise.all(Array.from(registeredNodePaths).map(async nodePath => {
            await edgeNodeStorage.del(nodePath);
        }));
        await updateEdgeNodesFile();
    });
    const { watchScheduledShutdown } = await import("../-g-core-values/scheduledShutdown");
    watchScheduledShutdown((time) => {
        void (async () => {
            // Immediately publish our shutdown time, so clients can deprioritize us when picking an edge node.
            await publishOwnScheduledShutdown(time);
            let duration = time - Date.now();
            // Wait a bit to fully remove ourself from the edge node list
            await delay(Math.max(0, duration * 0.2));
            console.log(magenta(`Removing node from edge node list due to scheduled shutdown at ${new Date(time).toLocaleString()}`));
            await Promise.all(Array.from(registeredNodePaths).map(async nodePath => {
                await edgeNodeStorage.del(nodePath);
            }));
            await updateEdgeNodesFile();
        })();
    });

    await waitForFirstTimeSync();

    if (isPublic() && !noSyncing()) {
        let loadedHash = "";
        let firstLoad = new PromiseObj<void>();
        Querysub.createWatcher(() => {
            let hash = deploySchema()[getDomain()].deploy.live.hash;
            if (hash === loadedHash || !Querysub.isAllSynced()) return;

            loadedHash = hash;
            void Promise.resolve().then(async () => {
                try {
                    await loadEntryPointsByHash({
                        host,
                        entryPaths: config.entryPaths,
                        hash,
                    });
                } finally {
                    firstLoad.resolve();
                }
            });
        });
        console.log(`Waiting for entry point to finish loading for live hash before registering edge node`);
        await firstLoad.promise;
        console.log(`Finished waiting for entry point to finish loading for live hash before registering edge node`);
    } else {
        let gitHash = await getGitRefLive();

        let nodeId = getOwnNodeId();
        let machineId = getOwnMachineId(getDomain());

        let edgeNodeConfig: EdgeNodeConfig = {
            nodeId,
            machineId,
            host,
            gitHash,
            bootTime: Date.now(),
            entryPaths: config.entryPaths,
            public: isPublic(),
            functionRunnerIndex: await getInitialFunctionRunnerIndex(),
        };

        await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
    }
    registeredEdgeNode = {
        host,
        entryPaths: config.entryPaths,
    };

    await startUpdateLoop();
}

const loadEntryPointsByHash = runInSerial(async function loadEntryPointsByHash(config: {
    host: string;
    entryPaths: string[];
    hash: string;
}) {
    let gitHash = config.hash;
    let entryPaths = config.entryPaths;

    let gitURL = await getGitURLLive();
    let filePath = entryPaths[0];
    console.log(magenta(`Loading entry point from ${gitHash}, path: ${filePath}`));

    let module = await getModuleFromConfig({
        gitURL,
        gitRef: gitHash,
        FilePath: filePath,
        FunctionId: "entryPoint",
    });
    console.log(magenta(`Loaded entry point from ${gitHash}, path: ${filePath}, resolved to ${module.filename}`));

    let depth = entryPaths[0].replaceAll("\\", "/").split("/").filter(x => x && x !== ".").length;

    let firstPath = path.resolve(module.filename);
    let rootPath = firstPath.replaceAll("\\", "/").split("/").slice(0, -depth).join("/") + "/";

    entryPaths = entryPaths.map(x => path.resolve(rootPath + x).replaceAll("\\", "/"));

    let nodeId = getOwnNodeId();
    let machineId = getOwnMachineId(getDomain());

    let edgeNodeConfig: EdgeNodeConfig = {
        nodeId,
        machineId,
        host: config.host,
        gitHash,
        bootTime: Date.now(),
        entryPaths,
        public: isPublic(),
        functionRunnerIndex: await getInitialFunctionRunnerIndex(),
    };

    await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
    console.log(magenta(`Registered edge node`), edgeNodeConfig);

    await updateEdgeNodesFile();

    onEdgeNodesChanged();
});

async function getInitialFunctionRunnerIndex(): Promise<FunctionRunnerIndex | undefined> {
    const { startFunctionRunnerTracking, getFunctionRunnerIndex } = await import("../4-querysub/FunctionRunnerTracking");
    await startFunctionRunnerTracking();
    return getFunctionRunnerIndex();
}

async function publishOwnScheduledShutdown(time: number) {
    for (let nodePath of Array.from(registeredNodePaths)) {
        let buffer = await edgeNodeStorage.get(nodePath);
        if (!buffer) continue;
        let config: EdgeNodeConfig = JSON.parse(buffer.toString());
        if (config.scheduledShutdownTime === time) continue;
        config.scheduledShutdownTime = time;
        await edgeNodeStorage.set(nodePath, Buffer.from(JSON.stringify(config)));
        getEdgeNodeConfig.clearKey(nodePath);
    }
    await updateEdgeNodesFile();
}

export const getEdgeNodeConfigURL = lazy(async () => {
    let { getURL } = await hostArchives({
        archives: edgeNodeStorage,
        subdomain: `edge`,
        domain: getDomain(),
    });
    return await getURL(edgeNodeIndexFile);
});

const startUpdateLoop = lazy(async () => {
    startEdgeNotifier();
    SocketFunction.expose(EdgeNodeController);
    await getEdgeNodeConfigURL();
    await runInfinitePollCallAtStart(UPDATE_POLL_INTERVAL * (1 + Math.random() * 0.1), updateLoop);
});

async function updateLoop() {
    if (edgeShutdown) return;
    await runEdgeNodesAliveCheck();
    await updateEdgeNodesFile();
}

let deadCounts = new Map<string, number>();
async function runEdgeNodesAliveCheck() {
    let edgeNodeNodeIds = await edgeNodeStorage.find("node", { type: "files" });
    let nodeIds = new Set(await getAllNodeIds());
    for (let fileName of edgeNodeNodeIds) {
        let nodeId = getNodeIdFromPath(fileName);
        if (!nodeIds.has(nodeId)) {
            deadCounts.set(fileName, (deadCounts.get(fileName) ?? 0) + 1);
        }
    }

    for (let [fileName, count] of Array.from(deadCounts)) {
        if (count < DEAD_NODE_COUNT_THRESHOLD) continue;
        console.log(`Node is dead. Removing from edgeNodes.`, { fileName, count });
        deadCounts.delete(fileName);
        await edgeNodeStorage.del(fileName);
    }
}
async function updateEdgeNodesFile() {
    let prevEdgeNodeFile = await edgeNodeStorage.get(edgeNodeIndexFile);
    let prevEdgeNodeIndex: EdgeNodesIndex = {
        edgeNodes: [],
        liveHash: "",
    };
    try {
        if (prevEdgeNodeFile) {
            prevEdgeNodeIndex = JSON.parse(prevEdgeNodeFile.toString());
        }
    } catch (e) {
        console.error(`Failed to parse ${edgeNodeIndexFile}`, { error: e, prevEdgeNodeFile });
    }

    let edgeNodeFiles = await edgeNodeStorage.find("node", { type: "files" });
    let edgeNodeNodeIds = edgeNodeFiles.map(getNodeIdFromPath);

    let liveHash = "";
    if (!noSyncing()) {
        liveHash = await Querysub.commitAsync(() => {
            return atomic(deploySchema()[getDomain()].deploy.live.hash);
        });
        if (!liveHash) {
            require("debugbreak")(2);
            debugger;
            throw new Error(`No live hash set?`);
        }
    }

    let diff = !!compareArray(edgeNodeNodeIds.sort(), prevEdgeNodeIndex.edgeNodes.map(x => x.nodeId).sort());
    if (diff) {
        console.log(`Detected edgeNodes changed. Updating ${edgeNodeIndexFile}`, { edgeNodeNodeIds, liveHash });
    }
    if (liveHash !== prevEdgeNodeIndex.liveHash) {
        diff = true;
        console.log(`Detected live hash changed. Updating ${edgeNodeIndexFile}`, { liveHash, prevHash: prevEdgeNodeIndex.liveHash });
    }
    if (!diff) {
        // Node configs are otherwise immutable, but the scheduled shutdown time is written into them after the fact, and clients need it to avoid picking dying edge nodes.
        let prevShutdownTimes = new Map(prevEdgeNodeIndex.edgeNodes.map(x => [x.nodeId, x.scheduledShutdownTime]));
        for (let nodeFile of edgeNodeFiles) {
            let config = await getEdgeNodeConfig(nodeFile);
            if (!config) continue;
            if (prevShutdownTimes.get(config.nodeId) !== config.scheduledShutdownTime) {
                diff = true;
                console.log(`Detected scheduled shutdown time changed for ${config.nodeId}. Updating ${edgeNodeIndexFile}`);
                break;
            }
        }
    }
    if (!diff) return;



    let newEdgeNodeIndex: EdgeNodesIndex = {
        edgeNodes: [],
        liveHash,
    };
    for (let nodeFile of edgeNodeFiles) {
        let edgeNodeConfig = await getEdgeNodeConfig(nodeFile);
        if (!edgeNodeConfig) continue;
        newEdgeNodeIndex.edgeNodes.push(edgeNodeConfig);
    }

    console.log(magenta(`Updating ${edgeNodeIndexFile} with ${JSON.stringify(newEdgeNodeIndex)}`));
    await edgeNodeStorage.set(edgeNodeIndexFile, Buffer.from(JSON.stringify(newEdgeNodeIndex)));
    await delay(Math.random() * 1000);
    async function verify() {
        let buffer = await edgeNodeStorage.get(edgeNodeIndexFile);
        let index = JSON.parse(buffer!.toString()) as EdgeNodesIndex;
        let ownNodeId = getOwnNodeId();
        return index.edgeNodes.some(x => x.nodeId === ownNodeId);
    }
    if (registeredEdgeNode) {
        try {
            if (!await verify()) {
                console.error(`Found own our node wasn't in the edge node index file, trying to update again in a bit`);
                await delay(Math.random() * 1000 + timeInSecond * 10);
                await updateEdgeNodesFile();
            }
        } catch (e) {
            console.error(`Failed to verify update to edge node index file due to an error, trying to update again in a bit`, { error: e });
            await delay(Math.random() * 1000 + timeInSecond * 10);
            await updateEdgeNodesFile();
        }
    }
}

export async function preloadUI(hash: string, progress?: DeployProgress) {
    progress?.({ section: "Finding EdgeNodeControllers", progress: 0 });
    let nodeIds = await getControllerNodeIdList(EdgeNodeController);
    progress?.({ section: "Finding EdgeNodeControllers", progress: 1 });
    await Promise.allSettled(nodeIds.map(async nodeId => {
        let controller = EdgeNodeController.nodes[nodeId.nodeId];
        let section = `${nodeId.entryPoint}:${nodeId.nodeId}|Preloading UI`;
        progress?.({ section, progress: 0 });
        console.log(blue(`Preloading UI on ${nodeId.nodeId}, hash: ${hash}`));
        await errorToUndefined(controller.preloadUI({ hash }));
        progress?.({ section, progress: 1 });
        console.log(blue(`Finished preloading UI on ${nodeId.nodeId}`));
    }));
}

class EdgeNodeControllerBase {
    public async preloadUI(config: {
        hash: string;
    }) {
        if (!isPublic()) return;
        const { hash } = config;
        if (!registeredEdgeNode || !canHaveChildren(registeredEdgeNode)) {
            throw new Error(`Somehow we have no registered edge node. How did we even expose this controller?`);
        }
        await loadEntryPointsByHash({
            host: registeredEdgeNode.host,
            entryPaths: registeredEdgeNode.entryPaths,
            hash,
        });
    }
}

const EdgeNodeController = SocketFunction.register(
    "EdgeNodeController-e0110af9-f8c4-42f3-ad11-633f8e43abdd",
    new EdgeNodeControllerBase(),
    () => ({
        preloadUI: {},
    }),
    () => ({
        hooks: [requiresNetworkTrustHook],
    }),
    {
        noAutoExpose: true,
    }
);

