'use strict';
const crypto = require('crypto');

class ServerApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl || process.env.TASK_SERVER_URL || 'http://localhost:4010';
        // Clave AES-256-GCM recibida en el register. Persiste para toda la vida del proceso.
        this._sessionKey = null;
    }

    async post(path, data = {}, timeoutMs = 10000) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);
        try {
            const res = await fetch(`${this.baseUrl}${path}`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data),
                signal: controller.signal,
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error(`HTTP ${res.status} ${path}: ${text}`);
            }
            return res.json();
        } finally {
            clearTimeout(timer);
        }
    }

    async get(path) {
        const res = await fetch(`${this.baseUrl}${path}`);
        if (!res.ok) throw new Error(`HTTP ${res.status} ${path}`);
        return res.json();
    }

    async put(path, data = {}, timeoutMs = 10000) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);
        try {
            const res = await fetch(`${this.baseUrl}${path}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data),
                signal: controller.signal,
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error(`HTTP ${res.status} ${path}: ${text}`);
            }
            return res.json();
        } finally {
            clearTimeout(timer);
        }
    }

    /**
     * Descifra un payload AES-256-GCM.
     * Espeja exactamente encryptPayload() del servidor (iv 96-bit, GCM, base64).
     *
     * Si falla (típicamente porque _sessionKey quedó desincronizada de la clave
     * con la que el servidor cifró — ej. un re-register en el medio de un
     * auto-update que generó una clave nueva mientras el proceso viejo todavía
     * tenía una tarea en vuelo), NO hay que devolver el sobre cifrado tal cual:
     * eso deja pasar un objeto `{_enc,iv,tag,data}` sin `.steps` que aguas abajo
     * (scriptTaskHandler) se interpreta como "payload sin steps" — un error real
     * de descifrado se ve entonces como un bug de validación, imposible de
     * diagnosticar sin mirar la DB (visto en producción, 2026-07). Lanzar acá
     * hace que el llamador (pullTask) lo trate como fallo explícito.
     */
    decryptPayload(payload) {
        if (!payload || !payload._enc || !this._sessionKey) return payload;
        try {
            const iv  = Buffer.from(payload.iv,   'base64');
            const tag = Buffer.from(payload.tag,  'base64');
            const enc = Buffer.from(payload.data, 'base64');
            const decipher = crypto.createDecipheriv('aes-256-gcm', this._sessionKey, iv);
            decipher.setAuthTag(tag);
            const plain = Buffer.concat([decipher.update(enc), decipher.final()]);
            return JSON.parse(plain.toString('utf8'));
        } catch (e) {
            const err = new Error(`SESSION_KEY_MISMATCH: no se pudo descifrar el payload (${e.message}) — probable clave de sesión desincronizada, reintentando registro`);
            err.code = 'SESSION_KEY_MISMATCH';
            throw err;
        }
    }

    async registerWorker(workerId, capabilities = ['REQUESTS', 'INBOX', 'FOLLOW'], hwid = null, vmInfo = null, version = null, scriptProtocolVersion = null) {
        const res = await this.post('/workers/register', { workerId, capabilities, hwid, vmInfo, version, scriptProtocolVersion });
        // Guardar la clave de sesión para descifrar payloads subsiguientes
        if (res?.sessionKey) {
            this._sessionKey = Buffer.from(res.sessionKey, 'hex');
        }
        return res;
    }

    async pullTask(workerId, timeoutMs = 45000) {
        const res = await this.post('/tasks/pull', { workerId }, timeoutMs);
        // Descifrar el payload si el servidor lo cifró con AES-256-GCM.
        // Si decryptPayload lanza (clave desincronizada), forzar un re-register
        // acá mismo para refrescar _sessionKey antes del próximo pull — el
        // worker se auto-recupera en vez de repetir el mismo fallo en loop.
        if (res?.task?.payload?._enc) {
            try {
                res.task.payload = this.decryptPayload(res.task.payload);
            } catch (e) {
                this._sessionKey = null; // fuerza re-registro en el próximo ciclo
                throw e;
            }
        }
        return res;
    }

    async heartbeat(taskId, workerId) {
        return this.post(`/tasks/${taskId}/heartbeat`, { workerId });
    }

    async reportResult(taskId, workerId, result) {
        return this.post(`/tasks/${taskId}/result`, { workerId, result });
    }

    async reportFailure(taskId, workerId, error) {
        const msg = error?.message || String(error);
        // Determinar failure_reason desde el mensaje de error del worker
        let failureReason = 'task_crashed';
        if (msg.includes('BROWSER_CLOSED_BY_USER'))  failureReason = 'browser_closed_by_user';
        else if (msg.includes('TASK_CANCELLED'))       failureReason = 'task_cancelled';
        else if (msg.includes('BROWSER_CLOSED'))       failureReason = 'browser_closed';
        else if (msg.includes('CHALLENGE_DETECTED'))   failureReason = 'challenge_detected';
        return this.post(`/tasks/${taskId}/fail`, {
            workerId,
            error: msg,
            failureReason,
        });
    }
}

module.exports = ServerApiClient;
