"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigStorage = void 0;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const profile_manager_1 = require("./profile-manager");
const nip_base_domain_util_1 = require("./nip-base-domain.util");
class ConfigStorage {
    constructor(profile) {
        this.configDir = profile_manager_1.ProfileManager.getProfileDir(profile);
        this.configFile = (0, node_path_1.join)(this.configDir, 'config.json');
        this.encryptionKeyFile = (0, node_path_1.join)(this.configDir, '.key');
        this.ensureConfigDir();
        this.encryptionKey = this.getOrCreateEncryptionKey();
    }
    /**
     * Ensure profile directory exists
     */
    ensureConfigDir() {
        if (!(0, node_fs_1.existsSync)(this.configDir)) {
            (0, node_fs_1.mkdirSync)(this.configDir, { recursive: true, mode: 0o700 });
        }
    }
    /**
     * Get or create encryption key for AES-256-GCM
     */
    getOrCreateEncryptionKey() {
        if ((0, node_fs_1.existsSync)(this.encryptionKeyFile)) {
            return (0, node_fs_1.readFileSync)(this.encryptionKeyFile);
        }
        // Generate new 32-byte key for AES-256
        const key = (0, node_crypto_1.randomBytes)(32);
        (0, node_fs_1.writeFileSync)(this.encryptionKeyFile, key, { mode: 0o600 });
        return key;
    }
    /**
     * Encrypt string using AES-256-GCM
     */
    encrypt(plaintext) {
        const iv = (0, node_crypto_1.randomBytes)(16);
        const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', this.encryptionKey, iv);
        let encrypted = cipher.update(plaintext, 'utf8', 'hex');
        encrypted += cipher.final('hex');
        const authTag = cipher.getAuthTag();
        // Format: iv:authTag:ciphertext
        return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
    }
    /**
     * Decrypt string using AES-256-GCM
     */
    decrypt(ciphertext) {
        const parts = ciphertext.split(':');
        if (parts.length !== 3) {
            throw new Error('Invalid ciphertext format');
        }
        const [ivHex, authTagHex, encryptedHex] = parts;
        const iv = Buffer.from(ivHex, 'hex');
        const authTag = Buffer.from(authTagHex, 'hex');
        const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', this.encryptionKey, iv);
        decipher.setAuthTag(authTag);
        let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        return decrypted;
    }
    /**
     * Read config file (create if doesn't exist)
     */
    readConfig() {
        if (!(0, node_fs_1.existsSync)(this.configFile)) {
            return {
                tokens: {},
                credentials: {},
                metadata: {
                    version: '1.0.0',
                    createdAt: new Date().toISOString(),
                    updatedAt: new Date().toISOString(),
                },
            };
        }
        try {
            const content = (0, node_fs_1.readFileSync)(this.configFile, 'utf8');
            return JSON.parse(content);
        }
        catch (error) {
            throw new Error(`Failed to read config file: ${error.message}`);
        }
    }
    /**
     * Write config file
     */
    writeConfig(config) {
        config.metadata.updatedAt = new Date().toISOString();
        (0, node_fs_1.writeFileSync)(this.configFile, JSON.stringify(config, null, 2), {
            mode: 0o600,
        });
    }
    /**
     * Save encrypted API token for provider
     */
    saveToken(provider, token) {
        const config = this.readConfig();
        const now = new Date().toISOString();
        const existing = config.tokens[provider];
        config.tokens[provider] = {
            encrypted: this.encrypt(token),
            createdAt: existing?.createdAt || now,
            updatedAt: now,
        };
        this.writeConfig(config);
    }
    /**
     * Get decrypted API token for provider
     */
    getToken(provider) {
        const config = this.readConfig();
        const tokenData = config.tokens[provider];
        if (!tokenData) {
            return null;
        }
        try {
            return this.decrypt(tokenData.encrypted);
        }
        catch (error) {
            throw new Error(`Failed to decrypt token for ${provider}: ${error.message}`);
        }
    }
    /**
     * Get token metadata (without decrypting)
     */
    getTokenMetadata(provider) {
        const config = this.readConfig();
        const tokenData = config.tokens[provider];
        if (!tokenData) {
            return null;
        }
        return {
            createdAt: tokenData.createdAt,
            updatedAt: tokenData.updatedAt,
        };
    }
    /**
     * Remove API token for provider
     */
    removeToken(provider) {
        const config = this.readConfig();
        delete config.tokens[provider];
        this.writeConfig(config);
    }
    /**
     * Check if token exists for provider
     */
    hasToken(provider) {
        const config = this.readConfig();
        return provider in config.tokens;
    }
    /**
     * Save encrypted credentials for provider
     */
    saveCredentials(provider, credentials) {
        const config = this.readConfig();
        config.credentials[provider] = this.encrypt(JSON.stringify(credentials));
        this.writeConfig(config);
    }
    /**
     * Get decrypted credentials for provider
     */
    getCredentials(provider) {
        const config = this.readConfig();
        const encryptedCredentials = config.credentials[provider];
        if (!encryptedCredentials) {
            return null;
        }
        try {
            const decrypted = this.decrypt(encryptedCredentials);
            return JSON.parse(decrypted);
        }
        catch (error) {
            throw new Error(`Failed to decrypt credentials for ${provider}: ${error.message}`);
        }
    }
    hasCredentials(provider) {
        const config = this.readConfig();
        return provider in config.credentials;
    }
    removeCredentials(provider) {
        const config = this.readConfig();
        delete config.credentials[provider];
        this.writeConfig(config);
    }
    /**
     * List all configured providers
     */
    listProviders() {
        const config = this.readConfig();
        const tokenProviders = Object.keys(config.tokens);
        const credentialProviders = Object.keys(config.credentials);
        return [...new Set([...tokenProviders, ...credentialProviders])];
    }
    /**
     * Get config file path (for debugging)
     */
    getConfigPath() {
        return this.configFile;
    }
    /**
     * Save API URL
     */
    saveApiUrl(url) {
        const config = this.readConfig();
        config.apiUrl = url;
        config.metadata.apiUrlUpdatedAt = new Date().toISOString();
        this.writeConfig(config);
    }
    /**
     * Returns the ISO timestamp of the last apiUrl update, or null if never tracked.
     */
    getApiUrlUpdatedAt() {
        const config = this.readConfig();
        return config.metadata.apiUrlUpdatedAt ?? null;
    }
    /**
     * Get API URL.
     * Resolution order:
     *   1. Saved value in this profile's config.json
     *   2. FLUI_API_URL env var
     *   3. Derived from clusters.json (master IP + nip token)
     * Returns null when no source provides a value — callers must decide whether
     * to error out (e.g. command needs API access) or just skip.
     */
    getApiUrl() {
        const config = this.readConfig();
        if (config.apiUrl) {
            return config.apiUrl;
        }
        if (process.env.FLUI_API_URL) {
            return process.env.FLUI_API_URL;
        }
        try {
            const profileDir = profile_manager_1.ProfileManager.getProfileDir();
            const clustersPath = (0, node_path_1.join)(profileDir, 'clusters.json');
            if ((0, node_fs_1.existsSync)(clustersPath)) {
                const clusters = JSON.parse((0, node_fs_1.readFileSync)(clustersPath, 'utf-8'));
                const cluster = Array.isArray(clusters) ? clusters[0] : null;
                if (cluster?.masterIpAddress) {
                    const base = (0, nip_base_domain_util_1.buildNipBaseDomain)(cluster.masterIpAddress, cluster.nipHostnameToken);
                    return `https://api.${base}/api/v1`;
                }
            }
        }
        catch {
            // ignore read errors
        }
        return null;
    }
    /**
     * Same as getApiUrl but throws a CLI-friendly error when no URL is configured.
     * Use this from commands that cannot work without an API endpoint.
     */
    getApiUrlOrThrow() {
        const url = this.getApiUrl();
        if (!url) {
            throw new Error('API URL is not configured for this context.\n' +
                '  • Run `flui env create` to provision a cluster (URL is auto-derived).\n' +
                '  • Or set it manually: `flui config set api-url https://api.example.com/api/v1`.');
        }
        return url;
    }
    /**
     * Remove API URL from config
     */
    removeApiUrl() {
        const config = this.readConfig();
        delete config.apiUrl;
        this.writeConfig(config);
    }
    /**
     * Save encrypted API key for CLI M2M access
     */
    setApiKey(key) {
        const config = this.readConfig();
        config.apiKey = this.encrypt(key);
        this.writeConfig(config);
    }
    /**
     * Get decrypted API key for CLI M2M access
     * Falls back to FLUI_API_KEY environment variable
     */
    getApiKey() {
        if (process.env.FLUI_API_KEY) {
            return process.env.FLUI_API_KEY;
        }
        const config = this.readConfig();
        if (!config.apiKey)
            return null;
        try {
            return this.decrypt(config.apiKey);
        }
        catch {
            return null;
        }
    }
    /**
     * Read a non-secret preference from the active profile. Returns null when unset.
     * Preferences live in a dedicated namespace so they never collide with tokens/apiKey/metadata.
     */
    getPreference(key) {
        const config = this.readConfig();
        const value = config.preferences?.[key];
        return value === undefined ? null : value;
    }
    /**
     * Persist a non-secret preference under the active profile.
     * Use null/undefined to clear via `removePreference` instead.
     */
    setPreference(key, value) {
        const config = this.readConfig();
        if (!config.preferences)
            config.preferences = {};
        config.preferences[key] = value;
        this.writeConfig(config);
    }
    removePreference(key) {
        const config = this.readConfig();
        if (!config.preferences)
            return;
        delete config.preferences[key];
        this.writeConfig(config);
    }
    getAllPreferences() {
        return { ...this.readConfig().preferences };
    }
    // ACME issuance tracking (Let's Encrypt rate-limit awareness)
    // Window matches LE: 5 certs per identical domain set per 168h.
    getAcmeIssuances() {
        const all = (this.getPreference('acmeIssuances') ?? []).filter((i) => Date.now() - new Date(i.issuedAt).getTime() < 168 * 3600 * 1000);
        this.setPreference('acmeIssuances', all);
        return all;
    }
    recordAcmeIssuance(entry) {
        const all = this.getAcmeIssuances();
        all.push({ ...entry, issuedAt: new Date().toISOString() });
        this.setPreference('acmeIssuances', all);
    }
    countAcmeIssuances(domains, server) {
        return this.getAcmeIssuances().filter((i) => i.domains === domains && i.server === server).length;
    }
}
exports.ConfigStorage = ConfigStorage;
