﻿import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";

export type AppEnv = {
    NODE_ENV: "development" | "test" | "production";
    HOST: string;
    PORT: number;
    TOKEN_PEPPER: string;
    DATA_FILE: string;
    CHAT_LOG_DIR: string;
    CHAT_RETENTION_DAYS: number;
    EXTENSIONS_PATHS: string[];
    EXTENSIONS_MANIFEST_FILE: string;
    EXTENSIONS_AUDIT_FILE: string;
    EXTENSION_CLEANUP_TIMEOUT_MS: number;
    DIAGNOSTICS_ACCESS: "open" | "local" | "admin";
    YOUTUBE_ENABLED: boolean;
    TWITCH_ENABLED: boolean;
    TWITCH_OAUTH_TOKEN: string | null;
    TWITCH_BOT_NICK: string | null;
    TWITCH_CHANNEL: string | null;
    TWITCH_IRC_HOST: string;
    TWITCH_IRC_PORT: number;
    TWITCH_RECONNECT_MS: number;
    SETUP_KEYS: string[];
    YOUTUBE_API_KEY: string | null;
    YOUTUBE_OAUTH_CLIENT_ID: string | null;
    YOUTUBE_OAUTH_CLIENT_SECRET: string | null;
    YOUTUBE_OAUTH_REFRESH_TOKEN: string | null;
    YOUTUBE_OAUTH_TOKEN_URL: string;
    YOUTUBE_POLL_MS: number;
    YOUTUBE_DISCOVERY_POLL_MS: number;
    YOUTUBE_ERROR_LOG_COOLDOWN_MS: number;
    EVENT_HANDLER_TIMEOUT_MS: number;
    EVENT_HANDLER_FAILURE_DEGRADE_THRESHOLD: number;
};

function parseDotEnv(content: string): Record<string, string> {
    const output: Record<string, string> = {};

    for (const rawLine of content.split(/\r?\n/)) {
        const line = rawLine.trim();

        if (!line || line.startsWith("#")) {
            continue;
        }

        const separatorIndex = line.indexOf("=");
        if (separatorIndex === -1) {
            continue;
        }

        const key = line.slice(0, separatorIndex).trim();
        let value = line.slice(separatorIndex + 1).trim();

        if (
            (value.startsWith('"') && value.endsWith('"')) ||
            (value.startsWith("'") && value.endsWith("'"))
        ) {
            value = value.slice(1, -1);
        }

        output[key] = value;
    }

    return output;
}

function loadDotEnvFromProjectRoot(): void {
    const envFilePath = resolve(process.cwd(), ".env");

    if (!existsSync(envFilePath)) {
        return;
    }

    const fileContent = readFileSync(envFilePath, "utf8");
    const pairs = parseDotEnv(fileContent);

    for (const [key, value] of Object.entries(pairs)) {
        if (process.env[key] === undefined) {
            process.env[key] = value;
        }
    }
}

function requireValue(key: string, fallback?: string): string {
    const value = process.env[key] ?? fallback;

    if (!value || !value.trim()) {
        throw new Error(`Missing required environment variable: ${key}`);
    }

    return value;
}

function parsePort(value: string): number {
    const parsed = Number(value);

    if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
        throw new Error(`Invalid PORT value: ${value}`);
    }

    return parsed;
}

function parseBoolean(value: string | undefined, fallback = false): boolean {
    if (value === undefined) {
        return fallback;
    }
    const normalized = value.trim().toLowerCase();
    return normalized === "1" || normalized === "true" || normalized === "yes";
}

function optionalValue(key: string): string | null {
    const value = process.env[key];
    if (!value || !value.trim()) {
        return null;
    }
    return value.trim();
}

function parsePositiveInt(value: string | null, fallback: number): number {
    if (!value) {
        return fallback;
    }
    const parsed = Number(value);
    if (!Number.isInteger(parsed) || parsed <= 0) {
        return fallback;
    }
    return parsed;
}

function parsePortLikeInt(value: string | null, fallback: number): number {
    const parsed = parsePositiveInt(value, fallback);
    if (parsed < 1 || parsed > 65535) {
        return fallback;
    }
    return parsed;
}

function parseCsvList(value: string | null, fallback: string[]): string[] {
    if (!value) {
        return [...fallback];
    }

    const items = value
        .split(",")
        .map((item) => item.trim())
        .filter((item) => item.length > 0);

    if (items.length === 0) {
        return [...fallback];
    }

    return items;
}

function parseDiagnosticsAccess(value: string | null): "open" | "local" | "admin" {
    const normalized = (value ?? "local").trim().toLowerCase();
    if (normalized === "open" || normalized === "local" || normalized === "admin") {
        return normalized;
    }
    return "local";
}

export function loadEnv(): AppEnv {
    loadDotEnvFromProjectRoot();

    const rawNodeEnv = process.env.NODE_ENV ?? "development";
    const nodeEnv =
        rawNodeEnv === "production" || rawNodeEnv === "test"
            ? rawNodeEnv
            : "development";

    return {
        NODE_ENV: nodeEnv,
        HOST: requireValue("HOST", "127.0.0.1"),
        PORT: parsePort(requireValue("PORT", "8787")),
        TOKEN_PEPPER: requireValue("TOKEN_PEPPER", "change-me"),
        DATA_FILE: requireValue("DATA_FILE", "./data/conduit.json"),
        CHAT_LOG_DIR: requireValue("CHAT_LOG_DIR", "./data/chat"),
        CHAT_RETENTION_DAYS: parsePositiveInt(optionalValue("CHAT_RETENTION_DAYS"), 30),
        EXTENSIONS_PATHS: parseCsvList(optionalValue("EXTENSIONS_PATHS"), [
            "./src/extensions/external"
        ]),
        EXTENSIONS_MANIFEST_FILE: requireValue(
            "EXTENSIONS_MANIFEST_FILE",
            "./data/extensions.manifest.json"
        ),
        EXTENSIONS_AUDIT_FILE: requireValue(
            "EXTENSIONS_AUDIT_FILE",
            "./data/extensions.audit.jsonl"
        ),
        EXTENSION_CLEANUP_TIMEOUT_MS: parsePositiveInt(
            optionalValue("EXTENSION_CLEANUP_TIMEOUT_MS"),
            2000
        ),
        DIAGNOSTICS_ACCESS: parseDiagnosticsAccess(optionalValue("DIAGNOSTICS_ACCESS")),
        SETUP_KEYS: parseCsvList(optionalValue("SETUP_KEYS"), []),
        YOUTUBE_ENABLED: parseBoolean(process.env.YOUTUBE_ENABLED, false),
        TWITCH_ENABLED: parseBoolean(process.env.TWITCH_ENABLED, false),
        TWITCH_OAUTH_TOKEN: optionalValue("TWITCH_OAUTH_TOKEN"),
        TWITCH_BOT_NICK: optionalValue("TWITCH_BOT_NICK"),
        TWITCH_CHANNEL: optionalValue("TWITCH_CHANNEL"),
        TWITCH_IRC_HOST: requireValue("TWITCH_IRC_HOST", "irc.chat.twitch.tv"),
        TWITCH_IRC_PORT: parsePortLikeInt(optionalValue("TWITCH_IRC_PORT"), 6697),
        TWITCH_RECONNECT_MS: parsePositiveInt(optionalValue("TWITCH_RECONNECT_MS"), 3000),
        YOUTUBE_API_KEY: optionalValue("YOUTUBE_API_KEY"),
        YOUTUBE_OAUTH_CLIENT_ID: optionalValue("YOUTUBE_OAUTH_CLIENT_ID"),
        YOUTUBE_OAUTH_CLIENT_SECRET: optionalValue("YOUTUBE_OAUTH_CLIENT_SECRET"),
        YOUTUBE_OAUTH_REFRESH_TOKEN: optionalValue("YOUTUBE_OAUTH_REFRESH_TOKEN"),
        YOUTUBE_OAUTH_TOKEN_URL: requireValue(
            "YOUTUBE_OAUTH_TOKEN_URL",
            "https://oauth2.googleapis.com/token"
        ),
        YOUTUBE_POLL_MS: parsePositiveInt(optionalValue("YOUTUBE_POLL_MS"), 5000),
        YOUTUBE_DISCOVERY_POLL_MS: parsePositiveInt(optionalValue("YOUTUBE_DISCOVERY_POLL_MS"), 15000),
        YOUTUBE_ERROR_LOG_COOLDOWN_MS: parsePositiveInt(
            optionalValue("YOUTUBE_ERROR_LOG_COOLDOWN_MS"),
            60000
        ),
        EVENT_HANDLER_TIMEOUT_MS: parsePositiveInt(optionalValue("EVENT_HANDLER_TIMEOUT_MS"), 2000),
        EVENT_HANDLER_FAILURE_DEGRADE_THRESHOLD: parsePositiveInt(
            optionalValue("EVENT_HANDLER_FAILURE_DEGRADE_THRESHOLD"),
            10
        )
    };
}
