// @ts-check
import { spawn } from "child_process";
import { NEEDLE_CLOUD_CLI_NAME } from "./cloud.js";
import { existsSync } from "fs";
import http from "http";
import https from "https";
import { needleLog } from "../vite/logging.js";

const port = 8424;
const localServerUrl = `http://localhost:${port}`;
const licenseServerUrl = `http://localhost:${port}/api/license`;
const projectIdentifierUrl = `http://localhost:${port}/api/public_key`;
const needleCloudApiEndpoint = "https://cloud.needle.tools/api";

/** @param {string} message @param {"error" | "log" | "warn"} [level] */
function logLicense(message, level = "log") {
    needleLog("needle-license", message, level);
}

/** @param {string} message @param {"error" | "log" | "warn"} [level] */
function logIdentifier(message, level = "log") {
    needleLog("needle-identifier", message, level);
}

/**
 * Ensure response streams are fully released to avoid lingering sockets.
 * @param {Response | null | undefined} response
 */
async function releaseResponse(response) {
    if (!response?.body) return;
    try {
        await response.arrayBuffer();
    }
    catch {
        try {
            await response.body.cancel();
        }
        catch {
            // ignore
        }
    }
}

/**
 * @param {string} path
 * @param {{ method:"GET"|"POST", accessToken:string, body?: Record<string, any>, timeoutMs:number }} options
 * @returns {Promise<NeedleCloudHttpResponse>}
 */
async function requestNeedleCloud(path, options) {
    const url = new URL(`${needleCloudApiEndpoint}${path}`);
    const requestBody = options.body ? JSON.stringify(options.body) : undefined;
    return new Promise((resolve, reject) => {
        const req = https.request(url, {
            method: options.method,
            agent: false,
            timeout: options.timeoutMs,
            headers: {
                Authorization: `Bearer ${options.accessToken}`,
                "x-needle": "cli",
                "Connection": "close",
                ...(requestBody ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(requestBody) } : {}),
            }
        }, (res) => {
            let text = "";
            res.setEncoding("utf8");
            res.on("data", (/** @type {Buffer|string} */ chunk) => text += chunk);
            res.on("end", () => {
                resolve({
                    ok: (res.statusCode || 500) >= 200 && (res.statusCode || 500) < 300,
                    status: res.statusCode || 500,
                    statusText: res.statusMessage || "",
                    text,
                });
            });
        });
        req.on("timeout", () => req.destroy(new Error("Request timed out")));
        req.on("error", reject);
        if (requestBody) req.write(requestBody);
        req.end();
    });
}

/**
 * @typedef {{loglevel?:"verbose"}} DefaultOptions
 */

/** @typedef {{ ok: boolean, status: number, statusText: string, text: string }} NeedleCloudHttpResponse */

/**
 * Replace license string - used for webpack
 * @param {string} code
 * @param {DefaultOptions & {accessToken?:string, team:string|undefined}} opts
 */
export async function replaceLicense(code, opts) {
    const index = code?.indexOf("NEEDLE_ENGINE_LICENSE_TYPE");
    if (index >= 0) {
        const licenseType = await resolveLicense(opts);
        if (!licenseType) {
            return code;
        }
        const end = code.indexOf(";", index);
        if (end >= 0) {
            const line = code.substring(index, end);
            const replaced = "NEEDLE_ENGINE_LICENSE_TYPE = \"" + licenseType + "\"";
            code = code.replace(line, replaced);
            return code;
        }
    }
    return code;
}

/**
 * Resolve the license using the needle engine licensing server
 * @param {DefaultOptions & {accessToken?:string, team?:string} | null} args
 * @returns {Promise<string | null>}
 */
export async function resolveLicense(args = null) {
    let accessToken = args?.accessToken;

    // If a process.env.NEEDLE_CLOUD_TOKEN is set we want to use this (e.g. via nextjs)
    if (!accessToken) {
        if (process.env.NEEDLE_CLOUD_TOKEN) {
            logLicense("INFO: Using Needle Cloud access token from  NEEDLE_CLOUD_TOKEN environment variable");
            accessToken = process.env.NEEDLE_CLOUD_TOKEN;
        }
        else if (process.env.CI) {
            logLicense("WARN: Missing NEEDLE_CLOUD_TOKEN for CI environment", "warn");
        }
    }

    if (accessToken) {
        const res = await requestNeedleCloud("/v1/account/get/licenses", {
            method: "GET",
            accessToken,
            timeoutMs: 10_000,
        }).catch((/** @type {{ message?: string }} */ err) => {
            return { ok: false, error: err.message };
        });
        if ("error" in res) {
            logLicense(`Could not fetch license from Needle Cloud API (${res.error})`, "error");
            return null;
        }
        if (res.ok) {
            return tryParseLicense(res.text);
        }
        else {
            logLicense(`Could not fetch license from Needle Cloud API (${res.status})`, "error");
            if (process.env.CI) {
                return null;
            }
        }
    }
    else if (process.env.CI) {
        const isGithubCI = process.env.GITHUB_ACTIONS;
        let message = "WARN: Missing NEEDLE_CLOUD_TOKEN for CI environment run.";
        if (isGithubCI) {
            const repositoryUrl = process.env.GITHUB_REPOSITORY;
            const url = `${repositoryUrl}/settings/secrets/actions`;
            message += `\nPlease add the token to your GitHub repository secrets: ${url}`;
        }
        logLicense(message, "warn");
        return null;
    }


    if (!canRunCLI(args)) {
        logLicense("License server CLI is not available. Please use an access token for authorization.", "error");
        return null;
    }

    // Fallback to use CLI
    // Wait for the server to start
    if (!await waitForLicenseServer(args || undefined)) {
        logLicense("ERR: Failed to start license server...", "error");
        return null;
    }

    const url = new URL(licenseServerUrl);
    if (args?.team)
        url.searchParams.append("org", args.team);
    if (accessToken) {
        url.searchParams.append("token", accessToken);
    }

    const includeFetchLineInSuccessLog = true;
    const timeout = AbortSignal.timeout(10_000);
    const licenseResponse = await fetch(url.toString(), {
        method: "GET",
        signal: timeout,
        headers: {
            "Connection": "close"
        }
    }).catch((/** @type {{ message?: string, cause?: { code?: string } }} */ err) => {
        if (args?.loglevel === "verbose") console.error("Error fetching license", err.message);

        if (err.cause?.code === "ECONNREFUSED") {
            return { error: "[needle-license] ERR: Failed to connect to license server (ECONNREFUSED)" };
        }
        else {
            return { error: "[needle-license] ERR: Failed to fetch license." };
        }
    });

    if (!licenseResponse) {
        logLicense("WARN: Failed to fetch license", "warn");
        return null;
    }
    else if ("error" in licenseResponse) {
        console.error(licenseResponse.error);
        return null;
    }
    else if (!licenseResponse.ok) {
        if (licenseResponse.status === 500)
            logLicense(`ERROR: Failed to fetch license (${licenseResponse.status})`, "error");
        else
            logLicense(`No license found (${licenseResponse.status})`);
        await releaseResponse(licenseResponse);
        return null;
    }

    const text = await licenseResponse.text();
    return tryParseLicense(text, { includeFetchLine: includeFetchLineInSuccessLog });
}


/**
 * @param {string} str License string
 * @param {{includeFetchLine?: boolean}} [options]
 */
function tryParseLicense(str, options = undefined) {
    try {
        /** @type {{needle_engine_license:string}} */
        const licenseJson = JSON.parse(str);
        if (licenseJson.needle_engine_license) {
            const success = `INFO: Successfully received \"${licenseJson.needle_engine_license?.toUpperCase()}\" license`;
            if (options?.includeFetchLine) {
                logLicense(`INFO: Fetching license...\n${success}`);
            }
            else {
                logLicense(success);
            }
            return licenseJson.needle_engine_license;
        }
        if ("error" in licenseJson) {
            logLicense(`ERROR in license check: \"${licenseJson.error}\"`, "error");
        }
        else if (licenseJson.needle_engine_license == null) {
            return null;
        }
        else {
            logLicense("WARN: Received invalid license. " + JSON.stringify(licenseJson), "warn");
        }
        return null;
    }
    catch (/** @type {unknown} */ err) {
        logLicense("ERROR: Failed to parse license response", "error");
        return null;
    }
}


/**
 * @param {string | undefined} project_id
 * @param {DefaultOptions | undefined} opts
 */
export async function getPublicIdentifier(project_id, opts = undefined) {

    let accessToken = /** @type {string | undefined} */ (undefined);
    if (!accessToken) {

        if (opts?.loglevel === "verbose" && process.env.CI) logIdentifier("INFO: Running in CI environment");

        if (process.env.NEEDLE_CLOUD_TOKEN) {
            logIdentifier("INFO: Using Needle Cloud access token from environment variable");
            accessToken = process.env.NEEDLE_CLOUD_TOKEN;
        }

        if (accessToken) {
            const body = {
                project_id: project_id || process.env.GITHUB_REPOSITORY || undefined,
                machine_id: process.env.GITHUB_REPOSITORY_ID || "unknown",
            }
            const res = await requestNeedleCloud("/v1/account/public_key", {
                method: "POST",
                accessToken,
                timeoutMs: 10_000,
                body,
            }).catch((/** @type {{ message?: string }} */ err) => {
                if (opts?.loglevel === "verbose") {
                    console.error(err);
                }
                return { ok: false, error: err.message };
            });
            if ("error" in res) {
                logIdentifier(`Could not fetch project identifier from Needle Cloud API (${res.error})`, "error");
                return null;
            }
            if (res.ok) {
                try {
                    /** @type {{public_key:string}} */
                    const json = JSON.parse(res.text);
                    logIdentifier(`INFO: Successfully received public project identifier`);
                    return json.public_key;
                }
                catch (/** @type {unknown} */ err) {
                    logIdentifier("ERROR: Failed to parse project identifier response", "error");
                    return null;
                }
            }
            else {
                logIdentifier(`Could not fetch project identifier from Needle Cloud API (${res.status}, ${res.statusText}, ${res.text})`, "error");
                return null;
            }
        }
    }


    if (!canRunCLI(opts)) {
        logLicense("License server CLI is not available. Please use an access token for authorization.", "error");
        return null;
    }



    // Wait for the server to start
    if (!await waitForLicenseServer(opts)) {
        logIdentifier("ERR: Failed to start license server...", "error");
        return null;
    }

    logIdentifier(`INFO: Fetching project identifier...`);
    const url = new URL(projectIdentifierUrl);
    if (project_id) url.searchParams.append("project_id", project_id);
    const timeout = AbortSignal.timeout(5_000);
    const res = await fetch(projectIdentifierUrl, {
        method: "GET",
        signal: timeout,
        headers: {
            "Connection": "close"
        }
    }).catch((/** @type {{ message?: string, cause?: { code?: string } }} */ err) => {

        if (opts?.loglevel === "verbose") {
            console.error(err);
        }

        if (err.cause?.code === "ECONNREFUSED") {
            return { error: "[needle-identifier] Could not connect to the license server: The connection was actively refused" };
        }
        else {
            return { error: "[needle-identifier] ERR: Failed to fetch project identifier." };
        }
    })

    if (!res) {
        logIdentifier("WARN: Failed to fetch project identifier", "warn");
        return null;
    }
    else if ("error" in res) {
        console.error(res.error);
        return null;
    }
    else if (!res.ok) {
        logIdentifier("ERROR: Failed to fetch project identifier", "error");
        await releaseResponse(res);
        return null;
    }
    const text = await res.text();

    try {
        /** @type {{public_key:string}} */
        const json = JSON.parse(text);
        return json.public_key;
    }
    catch (/** @type {unknown} */ err) {
        // TODO: report error to backend
        if (opts?.loglevel === "verbose") console.error(err);
        return null;
    }
};


/** @type {boolean | undefined} */
let licenseServerStarted = undefined;

/**
 * If we run the build command without an editor and the license server is just being started we need to to wait for the root URL to return a response
 * @param {DefaultOptions | undefined} opts
 * @returns {Promise<boolean>}
 */
async function waitForLicenseServer(opts) {
    if (opts?.loglevel === "verbose") console.log("[needle] INFO: Waiting for license server to start...");

    // Make sure the licensing server is running - but only call this once
    if (licenseServerStarted === undefined) {
        licenseServerStarted = true;
        const startcmd = runCommand("npx", ["--yes", NEEDLE_CLOUD_CLI_NAME, "start-server"], opts);
        startcmd.then(() => licenseServerStarted = false);
    }

    let attempts = 0;
    const maxAttempts = 3;
    do {
        const timeout = AbortSignal.timeout(5_000);

        // When using `fetch` then webcontainers do sometimes throw and exit the process silently - so we use http.get instead
        const response = await fetch_WaitForServer(localServerUrl, {
            method: "GET",
            signal: timeout
        }).catch((/** @type {{ message?: string, stack?: string, cause?: { code?: string } }} */ err) => {
            if (opts?.loglevel === "verbose") {
                console.error("ERROR connecting to local license server url at " + localServerUrl, err.message);
            }
            if (typeof err?.stack === "string" && err.stack?.includes("staticblitz.com")) {
                if (opts?.loglevel === "verbose") console.log("[needle] INFO: Running in Stackblitz environment, skipping license server check.");
                return { abort: true, error: err }; // Stackblitz does not support the license server
            }
            if (err.cause?.code === "ECONNREFUSED" || err?.message?.includes("ECONNREFUSED")) {
                if (!licenseServerStarted) {
                    console.error("[needle] ERR: Failed to connect to license server (ECONNREFUSED)");
                }
                else {
                    // Waiting for server to start
                    // EConnectRefuse happen if starting the license server for the first time
                }
            }
            else {
                if (attempts === maxAttempts) {
                    console.error("[needle] ERR: Failed to start license server...", err.message);
                }
            }            return undefined;        });
        if (typeof response === "object") {
            if ("abort" in response) {
                return false; // Abort signal was triggered, e.g. in Stackblitz
            }
        }
        if (response) {
            if (opts?.loglevel === "verbose") console.log(`[needle] INFO: License server is running and reachable at ${localServerUrl}`);
            return true;
        }
        if (attempts === 0) {
            console.log("[needle] INFO: Waiting for license server to start...");
        }
        attempts++;
        if (attempts <= maxAttempts) {
            await new Promise(res => setTimeout(res, 1000));
        }
    } while (attempts < maxAttempts);

    return false;
}


/**
 * @param {DefaultOptions | undefined | null} opts
 */
function canRunCLI(opts) {
    if (process.env.CI) {
        if (opts?.loglevel === "verbose") logLicense("INFO: Running in CI environment");
        return false; // We cannot run the CLI in CI environments
    }
    // Note: the .stackblitz directory doesnt always exist it seems
    if (existsSync("/home/.stackblitz")) {
        if (opts?.loglevel === "verbose") logLicense("INFO: Running in Stackblitz environment");
        return false;
    }
    // Default to true:
    return true;
}

/**
 * @param {string} processName
 * @param {string[]} args
 * @param {DefaultOptions | undefined} opts
 * @returns {Promise<boolean>}
 */
async function runCommand(processName, args, opts) {
    if (opts?.loglevel === "verbose") logLicense(`INFO: Running command: ${processName} ${args.join(" ")}`);
    const process = spawn(processName, [...args], {
        shell: true,
        timeout: 30_000,
        stdio: opts?.loglevel === "verbose" ? "inherit" : "ignore",
        // detached: true,
    });
    return new Promise((resolve, _reject) => {
        process.on('close', (code) => {
            if (opts?.loglevel === "verbose")
                logLicense(`INFO: \"${processName}\" process exited with code ${code}`);
            if (code === 0 || code === null || code === undefined) {
                resolve(true);
            } else {
                logLicense(`WARN: \"${processName}\" process exited with code ${code}\nProcess Arguments: ${args.join(" ")}`, "warn");
                resolve(false);
            }
        });
        process.on('error', (err) => {
            if (opts?.loglevel === "verbose") console.error("Error running " + processName, err);
            resolve(false);
        });
    });
}



/**
 * Fetches the content from the given URL and returns it as a string.
 * @param {string} url - The URL to fetch
 * @param {http.RequestOptions | https.RequestOptions} [options] - Optional request options
 * @return {Promise<string>} - The content of the URL
 */
function fetch_WaitForServer(url, options) {
    const module = url.startsWith("https") ? https : http;
    return new Promise((resolve, reject) => {
        if (!options) { options = {}; }
        module.get(url, options, (res) => {
            let data = '';
            res.on('data', (/** @type {Buffer|string} */ chunk) => {
                data += chunk;
            });
            res.on('end', () => {
                resolve(data);
            });
        }).on("error", (err) => {
            reject(err);
        });
    });
}