import { format } from "node:util";

// ----------------------------------------------
// Keep the real console functions in a module-level constant
// (this code runs before any Hamster instance is constructed)
const ORIGINAL_CONSOLE_LOG = console.log.bind(console);
const ORIGINAL_CONSOLE_WARN = console.warn.bind(console);
const ORIGINAL_CONSOLE_ERROR = console.error.bind(console);
const ORIGINAL_CONSOLE_INFO = console.info.bind(console);
const ORIGINAL_CONSOLE_DEBUG = console.debug.bind(console);
// ----------------------------------------------

export class Hamster {
    static lastInstance: Hamster | null = null;

    private apiKey: string | undefined = undefined;
    private apiVersion: string | undefined = undefined;
    private defaultTags: string[] = [];
    private captureConsole: boolean = false;
    private copyToLocalConsole: boolean = true;

    private logQueue: {
        message: string;
        tags: string[];
        timestamp: number;
    }[] = [];
    private batchTimeout: NodeJS.Timeout | null = null;
    private lastBatchTime: number = 0;
    private readonly API_ENDPOINT = "https://www.hamsterlog.dev/api/log";
    private readonly BATCH_INTERVAL = 1_000;
    private readonly MAX_REQUESTS_PER_MINUTE = 60;

    // Use the captured natives instead of whatever is currently in `console`
    private originalConsoleLog = ORIGINAL_CONSOLE_LOG;
    private originalConsoleWarn = ORIGINAL_CONSOLE_WARN;
    private originalConsoleError = ORIGINAL_CONSOLE_ERROR;
    private originalConsoleInfo = ORIGINAL_CONSOLE_INFO;
    private originalConsoleDebug = ORIGINAL_CONSOLE_DEBUG;

    constructor(config: {
        /** The API key to use for the Hamster API. */
        apiKey: string | undefined;

        /** These tags will be added to every log. */
        defaultTags: string[];

        /** When using any of the `console` functions: log, warn, error, info, debug, their */
        captureConsole?: boolean;

        /** Also prints logs to the local console. */
        copyToLocalConsole?: boolean;

        /** The API version to use for the Hamster API. */
        apiVersion?: string;
    }) {
        Hamster.lastInstance = this;
        this.apiKey = config.apiKey;
        this.defaultTags = config.defaultTags;
        this.captureConsole = config.captureConsole ?? true;
        this.copyToLocalConsole = config.copyToLocalConsole ?? true;
        this.apiVersion = config.apiVersion;

        /*  Install Hamster’s wrapper only once. */
        if (this.captureConsole && !(console as any).__hamsterPatched) {
            console.log = this.consoleLog.bind(this);
            console.warn = this.consoleWarn.bind(this);
            console.error = this.consoleError.bind(this);
            console.info = this.consoleInfo.bind(this);
            console.debug = this.consoleDebug.bind(this);

            // Mark as patched so future Hamster instances don’t add new layers
            (console as any).__hamsterPatched = true;
        }

        this.lastBatchTime = Date.now();

        if (this.apiKey) {
            this.log("🐹 Hamster initialized.");
        } else if (this.copyToLocalConsole) {
            this.log(
                "🐹 Hamster initialized without API key. Logs will only appear in the local console."
            );
        } else {
            this.log("🐹 Hamster initialized without API key.");
        }
    }

    log(message: string, tags?: string[]) {
        this.sendToServer(message, tags);

        if (this.copyToLocalConsole && this.originalConsoleLog) {
            this.originalConsoleLog(message);
        }
    }

    info(message: string, tags?: string[]) {
        this.sendToServer(message, ["info", ...(tags ?? [])]);

        if (this.copyToLocalConsole && this.originalConsoleInfo) {
            this.originalConsoleInfo(message);
        }
    }

    debug(message: string, tags?: string[]) {
        this.sendToServer(message, ["debug", ...(tags ?? [])]);

        if (this.copyToLocalConsole && this.originalConsoleDebug) {
            this.originalConsoleDebug(message);
        }
    }

    warn(message: string, tags?: string[]) {
        this.sendToServer(message, ["warning", ...(tags ?? [])]);

        if (this.copyToLocalConsole && this.originalConsoleWarn) {
            this.originalConsoleWarn(message);
        }
    }

    error(message: string, tags?: string[]) {
        this.sendToServer(message, ["error", ...(tags ?? [])]);

        if (this.copyToLocalConsole && this.originalConsoleError) {
            this.originalConsoleError(message);
        }
    }

    private consoleLog(message?: any, ...optionalParams: any[]) {
        this.log(format(message, ...optionalParams));
    }

    private consoleWarn(message?: any, ...optionalParams: any[]) {
        this.warn(format(message, ...optionalParams));
    }

    private consoleError(message?: any, ...optionalParams: any[]) {
        this.error(format(message, ...optionalParams));
    }

    private consoleInfo(message?: any, ...optionalParams: any[]) {
        this.info(format(message, ...optionalParams));
    }

    private consoleDebug(message?: any, ...optionalParams: any[]) {
        this.debug(format(message, ...optionalParams));
    }

    private sendToServer(message: string, tags?: string[]) {
        if (!this.apiKey) {
            return;
        }

        tags = [...(this.defaultTags ?? []), ...(tags ?? [])];

        this.logQueue.push({
            message,
            tags,
            timestamp: new Date().getTime(),
        });

        // Schedule batch send if not already scheduled
        if (!this.batchTimeout) {
            this.batchTimeout = setTimeout(
                () => this.sendBatch(),
                this.BATCH_INTERVAL
            );
        }
    }

    private async sendBatch(emergency?: boolean) {
        if (this.logQueue.length === 0) {
            this.batchTimeout = null;
            return;
        }

        const now = Date.now();
        const timeSinceLastBatch = now - this.lastBatchTime;
        const requestsPerMinuteRate = 60_000 / timeSinceLastBatch; // Calculate current rate

        if (
            !emergency &&
            requestsPerMinuteRate > this.MAX_REQUESTS_PER_MINUTE
        ) {
            // If we're exceeding our rate limit, schedule the next batch for later
            this.batchTimeout = setTimeout(
                () => this.sendBatch(),
                this.BATCH_INTERVAL
            );
            return;
        }

        const batchToSend = [...this.logQueue];
        this.logQueue = [];
        this.lastBatchTime = now;
        this.batchTimeout = null;

        try {
            await fetch(this.API_ENDPOINT, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    apiKey: this.apiKey,
                    apiVersion: this.apiVersion,
                    logs: batchToSend,
                }),
            });
        } catch (error) {
            // If the request fails, put the logs back in the queue
            this.originalConsoleError(
                `Failed to send logs to Hamster: ${error}`
            );
            this.logQueue = [...batchToSend, ...this.logQueue];
        }

        // Schedule next batch if there are new logs
        if (this.logQueue.length > 0) {
            this.batchTimeout = setTimeout(
                () => this.sendBatch(),
                this.BATCH_INTERVAL
            );
        }
    }

    static async emergencyFlush(): Promise<void> {
        if (Hamster.lastInstance?.batchTimeout) {
            clearTimeout(Hamster.lastInstance.batchTimeout);
            Hamster.lastInstance.batchTimeout = null;
        }
        await Hamster.lastInstance?.sendBatch(true);
    }
}

// Bun process exit handler
if (typeof process !== "undefined") {
    process.on("beforeExit", async () => {
        await Hamster.emergencyFlush();
    });

    process.on("SIGINT", async () => {
        await Hamster.emergencyFlush();
        process.exit(0);
    });

    process.on("SIGTERM", async () => {
        await Hamster.emergencyFlush();
        process.exit(0);
    });

    process.on("uncaughtException", async (error) => {
        Hamster.lastInstance?.error(
            `Uncaught Exception: ${error.stack || error.message}`
        );
        await Hamster.emergencyFlush();
        process.exit(1);
    });

    process.on("unhandledRejection", async (reason) => {
        Hamster.lastInstance?.error(`Unhandled Promise Rejection: ${reason}`);
        await Hamster.emergencyFlush();
        process.exit(1);
    });
}
