import crypto from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
dotenv.config();



const CACHE_DIR = path.join(process.cwd(), 'cache');

class CacheCredenciales {
    /**
     * @private {Buffer} encryptionKey
     * @private {string} filePath
     */
    encryptionKey;
    filePath;

    /**
     * Constructor para CacheCredenciales.
     * @param {string} cacheName - Nombre único para el archivo de caché.
     * @param {string} secretKey - Clave secreta para la encriptación del caché.
     */
    constructor(cacheName, secretKey) {
        this.encryptionKey = crypto.createHash('sha256').update(secretKey).digest();
        const fileName = crypto.createHash('md5').update(cacheName).digest('hex') + '.enc';
        this.filePath = path.join(CACHE_DIR, fileName);
    }

    /**
     * Guarda datos encriptados en el archivo de caché.
     * Si el archivo ya existe, lo elimina antes de guardar, replicando el comportamiento PHP.
     * @param {object} data - Los datos a guardar.
     * @returns {Promise<boolean>} - True si se guardó correctamente, false en caso contrario.
     */
    async guardar(data) {
        try {
            await fs.mkdir(CACHE_DIR, { recursive: true });

            // Replicar el comportamiento de 'unlink' de PHP: eliminar si existe antes de guardar
            if (await this.#fileExists(this.filePath)) {
                await fs.unlink(this.filePath);
                console.log(`Caché existente eliminado antes de guardar: ${this.filePath}`);
            }

            const iv = crypto.randomBytes(16); // Vector de inicialización de 16 bytes
            const serializedData = JSON.stringify(data); // Serializar a JSON
            const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
            let encryptedData = cipher.update(serializedData, 'utf8', 'base64');
            encryptedData += cipher.final('base64');

            // Combinar IV y datos encriptados, luego codificar en Base64
            const combined = Buffer.concat([iv, Buffer.from(encryptedData, 'base64')]).toString('base64');

            await fs.writeFile(this.filePath, combined);
            return true;
        } catch (error) {
            console.error('Error al guardar en caché:', error);
            return false;
        }
    }

    /**
     * Obtiene y desencripta datos del archivo de caché.
     * @returns {Promise<object | null>} - Los datos desencriptados o null si no se encuentran o hay un error.
     */
    async obtener() {
        try {
            if (!(await this.#fileExists(this.filePath))) {
                return null;
            }

            const content = await fs.readFile(this.filePath, 'utf8');
            const combined = Buffer.from(content, 'base64');

            const iv = combined.subarray(0, 16); // Extraer el IV
            const encryptedDataBuffer = combined.subarray(16); // Extraer los datos encriptados

            const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
            let decryptedData = decipher.update(encryptedDataBuffer.toString('base64'), 'base64', 'utf8');
            decryptedData += decipher.final('utf8');

            return JSON.parse(decryptedData); // Deserializar de JSON
        } catch (error) {
            console.error('Error al obtener del caché o desencriptar (posible corrupción):', error);
            // Si hay un error de desencriptación o parseo, el caché está corrupto; lo eliminamos.
            await this.eliminar().catch(() => { }); // Intentar eliminar, pero sin fallar si no se puede.
            return null;
        }
    }

    /**
     * Elimina el archivo de caché.
     * @returns {Promise<boolean>} - True si se eliminó correctamente, false en caso contrario.
     */
    async eliminar() {
        try {
            if (await this.#fileExists(this.filePath)) {
                await fs.unlink(this.filePath);
                console.log(`Caché eliminado: ${this.filePath}`);
                return true;
            }
            return false;
        } catch (error) {
            console.error('Error al eliminar el archivo de caché:', error);
            return false;
        }
    }

    /**
     * @private
     * Verifica si un archivo existe de forma asíncrona.
     * @param {string} filePath - La ruta del archivo.
     * @returns {Promise<boolean>} - True si el archivo existe, false en caso contrario.
     */
    async #fileExists(filePath) {
        try {
            await fs.access(filePath);
            return true;
        } catch {
            return false;
        }
    }
}

/**
 * Obtiene un token de autenticación desde la API.
 * Utiliza variables de entorno para las credenciales y la URL.
 * @returns {Promise<string>} - El token de autenticación.
 * @throws {Error} - Si falla la solicitud o no se encuentra el token.
 */
async function obtenerTokenAutenticacion() {
    // Asegurarse de que las variables de entorno están cargadas
    const passwordCredenciales = process.env.PASSWORD_CREDENCIALES;
    const userCredenciales = process.env.USER_CREDENCIALES;
    const urlApic = process.env.URL_APIC;
    if (!passwordCredenciales || !userCredenciales || !urlApic) {
        throw new Error("Variables de entorno para autenticación (PASSWORD_CREDENCIALES, USER_CREDENCIALES, URL_APIC) no configuradas.");
    }

    const url = `${urlApic}/Autentificacion/`;
    const postData = new URLSearchParams({
        clave: passwordCredenciales,
        user: userCredenciales
    });

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Accept': 'application/json'
            },
            body: postData.toString()
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Error HTTP ${response.status} al obtener token: ${errorText}`);
        }

        const auth = await response.json();
        if (!auth.token) {
            throw new Error("Token no encontrado en la respuesta de autenticación.");
        }
        return auth.token;
    } catch (error) {
        throw new Error(`Error en la solicitud de token: ${error.message}`);
    }
}

/**
 * Desencripta un campo individual en formato IV:Datos (hexadecimal).
 * Reemplaza la función PHP decryptAES256Field.
 * @param {string} encryptedField - Campo encriptado en formato 'IVHex:DataHex'.
 * @param {Buffer} key - Clave de 32 bytes (Buffer) para AES-256-CBC.
 * @returns {string} - El valor desencriptado.
 * @throws {Error} - Si el formato es inválido o la desencriptación falla.
 */
function decryptAES256Field(encryptedField, key) {
    const parts = encryptedField.split(':');
    if (parts.length !== 2) {
        throw new Error("Formato de campo encriptado inválido: no contiene 'IVHex:DataHex'.");
    }

    const ivHex = parts[0];
    const dataHex = parts[1];

    const iv = Buffer.from(ivHex, 'hex'); // hex2bin en PHP
    const encryptedData = Buffer.from(dataHex, 'hex'); // hex2bin en PHP

    if (iv.length !== 16) {
        throw new Error("IV inválido o de tamaño incorrecto (debe ser 16 bytes).");
    }

    try {
        const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
        let decrypted = decipher.update(encryptedData);
        decrypted = Buffer.concat([decrypted, decipher.final()]);
        return decrypted.toString('utf8');
    } catch (err) {
        throw new Error(`Error al desencriptar campo: ${err.message}`);
    }
}

/**
 * Desencripta campos 'user' y 'password' dentro de la estructura de credenciales de la API.
 * Reemplaza la función PHP decryptApiCredentials.
 * @param {object} data - Estructura de datos del API.
 * @param {string} encryptionKeyHex - Clave hexadecimal para AES-256-CBC (ej. 'a1b2c3d4...').
 * @returns {object} - Estructura con campos desencriptados.
 * @throws {Error} - Si hay errores en el proceso.
 */
function decryptApiCredentials(data, encryptionKeyHex) {
    const encryptionKey = Buffer.from(encryptionKeyHex, 'hex'); // hex2bin en PHP

    if (encryptionKey.length !== 32) {
        throw new Error("La clave de desencriptación de la API debe ser de exactamente 32 bytes (64 caracteres hexadecimales).");
    }

    if (!data.resultado || !Array.isArray(data.resultado)) {
        throw new Error("Estructura de datos inválida de la API: falta 'resultado' o no es un array.");
    }

    // Usar map para crear un nuevo array sin modificar el original directamente (mejor práctica)
    const processedResult = data.resultado.map(item => {
        if (!item.credenciales) {
            return item; // Retorna el ítem como está si no hay credenciales
        }

        const creds = { ...item.credenciales }; // Copia para no modificar el original

        // Desencriptar usuario si existe y tiene el formato IV:Datos
        if (typeof creds.user === 'string' && creds.user.includes(':')) {
            try {
                creds.user = decryptAES256Field(creds.user, encryptionKey);
            } catch (err) {
                console.warn(`Advertencia: Fallo al desencriptar usuario para dbmane ${item.dbmane}: ${err.message}`);
                // Podrías decidir qué hacer aquí: mantener el valor encriptado, establecerlo a null, etc.
                // Por ahora, lo dejaremos como está si falla para no romper el flujo.
            }
        }

        // Desencriptar contraseña si existe y tiene el formato IV:Datos
        if (typeof creds.password === 'string' && creds.password.includes(':')) {
            try {
                creds.password = decryptAES256Field(creds.password, encryptionKey);
            } catch (err) {
                console.warn(`Advertencia: Fallo al desencriptar contraseña para dbmane ${item.dbmane}: ${err.message}`);
            }
        }

        return { ...item, credenciales: creds }; // Retorna el ítem con las credenciales modificadas
    });

    return { ...data, resultado: processedResult }; // Retorna el objeto de datos completo con el resultado procesado
}

/**
 * Obtiene los datos de credenciales desde la API.
 * Aplica la desencriptación de campos sensibles si es necesario.
 * @param {string} token - El token de autenticación.
 * @param {object} solicitud - El cuerpo de la solicitud para las credenciales.
 * @returns {Promise<object>} - Los datos de credenciales desencriptados y procesados de la API.
 * @throws {Error} - Si falla la solicitud o el formato de respuesta es inválido.
 */
async function obtenerDatosCredenciales(token, solicitud) {
    const urlApic = process.env.URL_APIC;
    const encryptionKeyApi = process.env.ENCRYPTION_KEY; // Clave de desencriptación de la API

    if (!urlApic || !encryptionKeyApi) {
        throw new Error("Variables de entorno para credenciales (URL_APIC, ENCRYPTION_KEY) no configuradas.");
    }

    const url = `${urlApic}/credenciales/`;
    const body = JSON.stringify(solicitud);

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': token,
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: body
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Error HTTP ${response.status} al obtener credenciales: ${errorText}`);
        }

        let data = await response.json();
        if (!data.resultado || !Array.isArray(data.resultado)) {
            throw new Error("Formato de respuesta de credenciales inválido de la API.");
        }

        // Llamada a la función de desencriptación de la API
        data = decryptApiCredentials(data, encryptionKeyApi);
        return data;
    } catch (error) {
        throw new Error(`Error en la solicitud de credenciales o desencriptación: ${error.message}`);
    }
}

/**
 * Procesa las credenciales obtenidas de la API para un formato estandarizado.
 * @param {object} credencialesRaw - Las credenciales raw (ya desencriptadas por decryptApiCredentials) obtenidas de la API.
 * @returns {object} - Las credenciales procesadas.
 * @throws {Error} - Si no se encuentran credenciales válidas.
 */
function procesarCredenciales(credencialesRaw) {
    const config_db = {};

    if (!credencialesRaw || !Array.isArray(credencialesRaw.resultado)) {
        throw new Error("Estructura de respuesta inválida: falta 'resultado' o no es un array.");
    }

    for (const item of credencialesRaw.resultado) {
        if (!item || typeof item !== 'object') { // Validar que item sea un objeto
            console.warn("Advertencia: Elemento de resultado inválido, omitiendo.", item);
            continue;
        }

        const dbname = item.dbmane ?? null;
        const cred = item.credenciales ?? null;

        if (dbname === null || cred === null || typeof cred !== 'object') {
            console.warn("Advertencia: Elemento de credenciales incompleto o malformado, omitiendo.", item);
            continue;
        }

        // Validar campos mínimos requeridos (igual que PHP)
        const required = ['user', 'password', 'port', 'ip'];
        const missing = required.filter(field => cred[field] === undefined || cred[field] === null);

        if (missing.length > 0) {
            console.warn(`Advertencia: Credenciales incompletas para ${dbname}. Faltan: ${missing.join(', ')}`);
            continue;
        }

        config_db[dbname] = {
            name: cred.connection_name ?? 'default', // PHP default
            username: cred.user,
            password: cred.password,
            puerto: cred.port,
            ip: cred.ip,
            dns: cred.dns ?? '', // PHP default
            db_type: cred.db_type ?? 'mysql' // PHP default
        };
    }

    if (Object.keys(config_db).length === 0) {
        console.error("No se encontraron credenciales válidas después del procesamiento. Datos recibidos:", JSON.stringify(credencialesRaw));
        throw new Error("No se encontraron credenciales válidas después del procesamiento.");
    }

    return config_db;
}

/**
 * Función principal para obtener credenciales, utilizando caché o la API.
 * @param {object} solicitud - Los datos de solicitud para las credenciales.
 * @param {string} [cacheSecretKey='your_strong_cache_secret_key'] - Clave secreta para el cifrado del caché. **¡USAR VARIABLE DE ENTORNO EN PRODUCCIÓN!**
 * @returns {Promise<object>} - Las credenciales procesadas y listas para usar.
 * @throws {Error} - Si no se pueden obtener las credenciales.
 */
export async function ObtenerCredenciales(solicitud, cacheSecretKey = process.env.SECRET_CACHE_KEY || 'a_secret_key_for_caching') {
    const cacheName = crypto.createHash('md5').update(JSON.stringify(solicitud)).digest('hex');
    const cache = new CacheCredenciales(cacheName, cacheSecretKey);

    // Intentar obtener del caché (esta versión de PHP no tiene TTL en la función obtener)
    const credencialesCache = await cache.obtener();
    if (credencialesCache !== null) {
        console.log("Credenciales obtenidas del caché.");
        return credencialesCache;
    }

    console.log("Credenciales no encontradas en caché, obteniendo de la API...");

    // Si no hay caché válido, obtener del API
    const token = await obtenerTokenAutenticacion();
    // La solicitud es un objeto JS, obtenerDatosCredenciales lo serializa a JSON
    const credencialesApi = await obtenerDatosCredenciales(token, solicitud);

    if (!credencialesApi || Object.keys(credencialesApi).length === 0) {
        throw new Error("No se pudieron obtener credenciales del API o están vacías.");
    }

    // Las credenciales de la API ya vienen desencriptadas por 'obtenerDatosCredenciales'
    const credencialesProcesadas = procesarCredenciales(credencialesApi);

    // Guardar en caché para próximas ejecuciones (sin esperar el resultado)
    cache.guardar(credencialesProcesadas)
        .then(success => {
            if (!success) {
                console.warn("Advertencia: No se pudo guardar en caché las credenciales.");
            } else {
                console.log("Credenciales guardadas en caché.");
            }
        })
        .catch(err => {
            console.error("Error al guardar credenciales en caché:", err);
        });

    return credencialesProcesadas;
}

export { CacheCredenciales };
