// @ts-check
import https from 'https';
import http from 'http';
import { createHash } from 'crypto';
import { needleLog, setTransientLogLineCleaner } from './logging.js';

/** @typedef {import('./local-files-types.js').LocalizationContext} LocalizationContext */

const debug = false;
const DOWNLOAD_TIMEOUT_MS = 30_000;
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let spinnerIndex = 0;
let spinnerActive = false;

setTransientLogLineCleaner(() => clearSpinnerLine());

function clearSpinnerLine() {
    if (!process.stdout.isTTY || !spinnerActive) return;
    process.stdout.write("\r\x1b[2K");
    spinnerActive = false;
}

/** @param {string} url */
function updateMakeLocalProgress(url) {
    if (!process.stdout.isTTY) {
        needleLog("needle:local-files", "Make local: " + url, "log", { dimBody: true, showHeader: false });
        return;
    }
    const frame = SPINNER_FRAMES[spinnerIndex++ % SPINNER_FRAMES.length];
    const maxLength = Math.max(24, (process.stdout.columns || 120) - 4);
    const message = `Make local: ${url}`;
    const text = message.length > maxLength ? `${message.slice(0, Math.max(0, maxLength - 1))}…` : message;
    process.stdout.write(`\r\x1b[2K${frame} ${text}\x1b[0K`);
    spinnerActive = true;
}

export function finishMakeLocalProgress() {
    clearSpinnerLine();
}

/**
 * @param {string} src
 * @param {string} search
 * @param {string} replacement
 * @returns {string}
 */
export function replaceAll(src, search, replacement) {
    return src.split(search).join(replacement);
}

export class Cache {
    /** @type {Map<string, Buffer|string>} */
    __cache = new Map();
    /**
     * @param {string} key
     * @param {Buffer|string} value
     */
    addToCache(key, value) {
        if (debug && this.__cache.has(key)) {
            console.warn("Key " + key + " already exists in cache, overwriting.");
        }
        this.__cache.set(key, value);
    }
    /**
     * @param {string} key
     * @returns {Buffer|string|null}
     */
    getFromCache(key) {
        return this.__cache.get(key) ?? null;
    }
    get map() {
        return this.__cache;
    }
}

/**
 * @param {string} path
 * @param {string} basePath
 * @returns {string}
 */
export function getRelativeToBasePath(path, basePath) {
    const normalizedPath = normalizeWebPath(path);
    const normalizedBasePath = normalizeWebPath(basePath);

    if (!normalizedBasePath) return normalizedPath;

    if (normalizedPath.startsWith(normalizedBasePath)) {
        return "./" + normalizedPath.substring(normalizedBasePath.length);
    }

    const baseSegments = normalizedBasePath.replace(/\/+$/g, "").split("/").filter(Boolean);
    const backtrack = baseSegments.map(() => "..").join("/");
    return (backtrack ? backtrack + "/" : "") + normalizedPath;
}

/**
 * @param {string} path
 * @returns {string}
 */
export function normalizeWebPath(path) {
    if (!path) return "";
    return path.replace(/\\/g, "/");
}

/**
 * @param {string} path
 * @returns {string}
 */
export function ensureTrailingSlash(path) {
    if (!path) return "";
    return path.endsWith("/") ? path : path + "/";
}

/**
 * @param {string} src
 * @returns {string}
 */
export function fixRelativeNewURL(src) {
    src = src.replace(
        /(?<==\s*)(["'])((?:(?:\.{1,2}\/)|\/)?ext\/[^"']*\/)\1/g,
        (/** @type {string} */ _match, /** @type {string} */ quote, /** @type {string} */ path) => {
            const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
            return `new URL(${quote}${runtimePath}${quote}, self.location?.href || ${quote}${quote}).href`;
        }
    );

    src = src.replace(
        /new\s+URL\s*\(\s*(["'`])((?:(?:\.{1,2}\/)|\/)?ext\/[^"'`]+)\1\s*\)/g,
        (/** @type {string} */ _match, /** @type {string} */ quote, /** @type {string} */ path) => {
            const runtimePath = path.startsWith("/ext/") ? path.substring(1) : path;
            return `new URL(${quote}${runtimePath}${quote}, self.location?.href)`;
        }
    );

    return src;
}

/**
 * @param {string} src
 * @returns {string}
 */
export function fixDracoRangeQueryProbe(src) {
    return src;
}

/**
 * @param {string} path
 * @param {string|Buffer} content
 * @returns {string}
 */
export function getValidFilename(path, content) {
    if (path.startsWith("http:") || path.startsWith("https:")) {
        const url = new URL(path);
        const pathParts = url.pathname.split('/');
        const filename = pathParts.pop() || 'file';
        const nameParts = filename.split('.');
        const nameWithoutExt = nameParts.slice(0, -1).join('.');
        path = nameWithoutExt + "-" + createContentMd5(url.host + pathParts.join('/')) + "." + (nameParts.pop() || 'unknown');
    }

    let name = path.replace(/[^a-z0-9_\-\.\+]/gi, '-');

    const maxLength = 200;
    if (path.length > maxLength) {
        const hash = createContentMd5(content);
        let ext = "";
        const extIndex = name.lastIndexOf('.');
        if (extIndex !== -1) {
            ext = name.substring(extIndex + 1);
            name = name.substring(0, extIndex);
        }
        name = name.substring(0, maxLength) + "-" + hash + (ext ? "." + ext : '');
    }
    return name;
}

/**
 * @param {string | Buffer} str
 * @returns {string}
 */
function createContentMd5(str) {
    return createHash('md5')
        .update(str)
        .digest('hex');
}

/**
 * @param {string} url
 * @returns {Promise<string>}
 */
export function downloadText(url) {
    return new Promise((res, rej) => {
        const timer = setTimeout(() => {
            rej(new Error("Download timed out after " + DOWNLOAD_TIMEOUT_MS + "ms: " + url));
        }, DOWNLOAD_TIMEOUT_MS);

        const req = (url.startsWith("http://") ? http.get(url, (response) => {
            if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
                clearTimeout(timer);
                downloadText(response.headers.location).then(res).catch(rej);
                return;
            }

            if (response.statusCode !== 200) {
                clearTimeout(timer);
                clearSpinnerLine();
                rej(new Error("Failed to download (" + response.statusCode + "): " + url));
                return;
            }

            updateMakeLocalProgress(url);

            let data = '';
            response.on('data', (/** @type {Buffer|string} */ chunk) => {
                data += chunk;
            });
            response.on('end', () => {
                clearTimeout(timer);
                res(data);
            });
            response.on('error', (err) => {
                clearTimeout(timer);
                rej(err);
            });
        }) : https.get(url, (response) => {
            if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
                clearTimeout(timer);
                downloadText(response.headers.location).then(res).catch(rej);
                return;
            }

            if (response.statusCode !== 200) {
                clearTimeout(timer);
                clearSpinnerLine();
                rej(new Error("Failed to download (" + response.statusCode + "): " + url));
                return;
            }

            updateMakeLocalProgress(url);

            let data = '';
            response.on('data', (/** @type {Buffer|string} */ chunk) => {
                data += chunk;
            });
            response.on('end', () => {
                clearTimeout(timer);
                res(data);
            });
            response.on('error', (err) => {
                clearTimeout(timer);
                rej(err);
            });
        }));
        req.on('error', (err) => {
            clearTimeout(timer);
            clearSpinnerLine();
            rej(err);
        });
    });
}

/**
 * @param {string} url
 * @returns {Promise<Buffer>}
 */
export function downloadBinary(url) {
    return new Promise((res, rej) => {
        const timer = setTimeout(() => {
            rej(new Error("Download timed out after " + DOWNLOAD_TIMEOUT_MS + "ms: " + url));
        }, DOWNLOAD_TIMEOUT_MS);

        const req = (url.startsWith("http://") ? http.get(url, (response) => {
            if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
                clearTimeout(timer);
                downloadBinary(response.headers.location).then(res).catch(rej);
                return;
            }

            if (response.statusCode !== 200) {
                clearTimeout(timer);
                clearSpinnerLine();
                rej(new Error("Failed to download (" + response.statusCode + "): " + url));
                return;
            }

            updateMakeLocalProgress(url);

            const chunks = /** @type {Buffer[]} */ ([]);
            response.on('data', (/** @type {Buffer} */ chunk) => {
                chunks.push(chunk);
            });
            response.on('end', () => {
                clearTimeout(timer);
                res(Buffer.concat(chunks));
            });
            response.on('error', (err) => {
                clearTimeout(timer);
                rej(err);
            });
        }) : https.get(url, (response) => {
            if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
                clearTimeout(timer);
                downloadBinary(response.headers.location).then(res).catch(rej);
                return;
            }

            if (response.statusCode !== 200) {
                clearTimeout(timer);
                clearSpinnerLine();
                rej(new Error("Failed to download (" + response.statusCode + "): " + url));
                return;
            }

            updateMakeLocalProgress(url);

            const chunks = /** @type {Buffer[]} */ ([]);
            response.on('data', (/** @type {Buffer} */ chunk) => {
                chunks.push(chunk);
            });
            response.on('end', () => {
                clearTimeout(timer);
                res(Buffer.concat(chunks));
            });
            response.on('error', (err) => {
                clearTimeout(timer);
                rej(err);
            });
        }));
        req.on('error', (err) => {
            clearTimeout(timer);
            clearSpinnerLine();
            rej(err);
        });
    });
}

/**
 * @param {LocalizationContext} context
 * @param {string} url
 * @param {unknown} err
 */
export function recordFailedDownload(context, url, err) {
    if (!url || !context?.failedDownloads) return;
    const message = err instanceof Error ? err.message : "";
    if (!context.failedDownloads.has(url)) {
        context.failedDownloads.set(url, message);
    }
}

/**
 * @param {string} url
 * @returns {string}
 */
export function getShortUrlName(url) {
    try {
        const parsed = new URL(url);
        const parts = parsed.pathname.split("/").filter(Boolean);
        return parts[parts.length - 1] || parsed.host;
    }
    catch {
        return url;
    }
}
