'use strict';

const LogSinkPack = require('../pack/log-sink-pack');
const DataOutputX = require('../io/data-outputx');
const DateUtil = require('../util/dateutil');
const conf = require('../conf/configure');
const Logger = require('../logger');
const LlmLogSinkPack = require('./llm-log-sink-pack');
const statCollector = require('./llm-stat-collector');
const AsyncSender = require('../udp/async_sender');
const { getLlmPcode } = require('./llm-pcode');
const IdUtil = require('./llm-id-util');

// macOS kern.ipc.maxsockbuf / net.inet.udp.maxdgram = 9216 바이트 제한 고려.
// Pack 헤더 + 태그 + 필드 + UDP/IP 헤더 오버헤드(~700b) 제외하면 실제 content 한계는 ~8500.
// 20000으로 설정 시 EMSGSIZE로 UDP 송신 자체가 실패 → input_message 등 누락 (긴 프롬프트 케이스).
const _MAX_CONTENT_BYTES = 8000;
const _MAX_QUEUE_SIZE = 4096;

const _queue = [];
let _processing = false;

// LLM call counter for ACTIVE_STATS "LLM:count"
let _llmMeterCount = 0;

function getLlmMeterCountAndReset() {
    const count = _llmMeterCount;
    _llmMeterCount = 0;
    return count;
}

const _TX_SUMMARY_TOKEN_FIELDS = [
    'input_tokens', 'output_tokens', 'total_tokens_count',
    'cached_tokens', 'reasoning_tokens',
    'audio_input_tokens', 'audio_output_tokens',
    'accepted_prediction_tokens', 'rejected_prediction_tokens',
    'embedding_count',
];

/**
 * @param {LlmLogSinkPack} pack
 * @param {object} ctx
 * @param {object} [opts]
 * @param {boolean} [opts.flushNow] - true면 setImmediate 큐 우회, 동일 tick에 _processPack 실행.
 *                                    스트림 abort/error 등 trace race 회피용.
 */
function dispatchLlmPack(pack, ctx, opts) {
    if (!(pack instanceof LlmLogSinkPack)) return;

    pack.setContext(ctx);

    if (!pack.operationType && ctx) {
        pack.operationType = ctx._llm_operation_type || 'unknown';
    }

    if (ctx) {
        const idx = ctx._llm_call_index || 0;
        pack.index = idx;
        ctx._llm_call_index = idx + 1;
    }

    try {
        pack.calculateCost();
    } catch (e) {
        Logger.printError('WHATAP-LLM-001', 'calculateCost failed', e, false);
    }

    _accumulateTxSummary(ctx, pack);

    if (ctx && !pack.success && pack.errorType) {
        ctx._llm_last_error_type = pack.errorType;
        ctx._llm_last_error_model = pack.model || 'unknown';
        ctx._llm_last_error_provider = pack.provider || '';
        ctx._llm_last_error_op_type = pack.operationType || 'unknown';
        ctx._llm_last_error_url = pack.url || '';
    }

    if (_queue.length >= _MAX_QUEUE_SIZE) return;
    // ctx 참조를 pack에 부착 → _processPack에서 collector feed 시 사용
    pack._ctx = ctx;
    _queue.push(pack);

    if (opts && opts.flushNow) {
        // setImmediate 우회: 큐에 있는 모든 pack을 즉시 동기 처리.
        // race 회피용. 호출자가 abort/error 시점에 TX_END보다 먼저 LogSinkPack 송신을 보장하려 할 때.
        try {
            while (_queue.length > 0) {
                const p = _queue.shift();
                try { _processPack(p); } catch (e) {
                    Logger.printError('WHATAP-LLM-007', 'flushNow processPack failed', e, false);
                }
            }
        } finally {
            _processing = false;
        }
        return;
    }

    _scheduleProcess();
}

function _accumulateTxSummary(ctx, pack) {
    if (!ctx) return;
    let summary = ctx._llm_tx_summary;
    if (!summary) {
        summary = {
            call_count: 0, error_count: 0,
            latency: 0, cost: 0, input_cost: 0, output_cost: 0,
            first_step_id: null, last_step_id: null,
            providers: new Set(), models: new Set(), operation_types: new Set(),
        };
        for (const f of _TX_SUMMARY_TOKEN_FIELDS) summary[f] = 0;
        ctx._llm_tx_summary = summary;
    }

    summary.call_count++;
    if (!pack.success) summary.error_count++;

    for (const f of _TX_SUMMARY_TOKEN_FIELDS) {
        if (pack[f]) summary[f] += pack[f];
    }
    if (pack.latency) summary.latency += pack.latency;
    if (pack.cost) summary.cost += pack.cost;
    if (pack.input_cost) summary.input_cost += pack.input_cost;
    if (pack.output_cost) summary.output_cost += pack.output_cost;

    if (pack.stepId) {
        if (!summary.first_step_id) summary.first_step_id = pack.stepId;
        summary.last_step_id = pack.stepId;
    }
    if (pack.provider) summary.providers.add(pack.provider);
    if (pack.model) summary.models.add(pack.model);
    if (pack.operationType && pack.operationType !== 'unknown') {
        summary.operation_types.add(pack.operationType);
    }
}

function sendLlmTxStatus(ctx) {
    if (!ctx) return;
    if (!conf.getProperty('llm_enabled', false)) return;
    const summary = ctx._llm_tx_summary;
    if (!summary || summary.call_count === 0) return;

    // step_status와 동일한 BigInt-precise txid 포맷 사용 (서버에서 트랜잭션과 매칭 필요).
    // String(ctx.id) 은 Number 53bit 경계에서 정밀도 손실 → tx 매칭 실패 원인.
    const tags = {
        'llm_log_type': 'llm_tx_status',
        '@txid': IdUtil.ctxTxidStr(ctx),
        '@first_step_id': summary.first_step_id || '',
        '@last_step_id': summary.last_step_id || '',
    };
    if (summary.providers.size > 0) tags['provider'] = [...summary.providers].sort().join(',');
    if (summary.models.size > 0) tags['model'] = [...summary.models].sort().join(',');
    if (summary.operation_types.size > 0) tags['operation_type'] = [...summary.operation_types].sort().join(',');

    const fields = {
        'call_count.n': summary.call_count,
        'error_count.n': summary.error_count,
    };
    for (const f of _TX_SUMMARY_TOKEN_FIELDS) {
        if (summary[f]) fields[f + '.n'] = summary[f];
    }
    if (summary.latency) fields['latency.n'] = summary.latency;
    if (summary.cost) fields['cost.n'] = +summary.cost.toFixed(6);
    if (summary.input_cost) fields['input_cost.n'] = +summary.input_cost.toFixed(6);
    if (summary.output_cost) fields['output_cost.n'] = +summary.output_cost.toFixed(6);

    try {
        _sendLlmLogSinkPack(tags, fields, '', 0);
    } catch (e) {
        Logger.printError('WHATAP-LLM-002', 'sendLlmTxStatus failed', e, false);
    }
}

// --- internal ---

function _scheduleProcess() {
    if (_processing) return;
    _processing = true;
    setImmediate(_processLoop);
}

function _processLoop() {
    const pack = _queue.shift();
    if (!pack) {
        _processing = false;
        return;
    }
    try {
        _processPack(pack);
    } catch (e) {
        Logger.printError('WHATAP-LLM-003', 'processPack failed', e, false);
    }
    if (_queue.length > 0) {
        setImmediate(_processLoop);
    } else {
        _processing = false;
    }
}

function _processPack(pack) {
    const ctx = pack._ctx;
    // 1. llm_step_status
    const statusTags = _baseTags(pack, 'llm_step_status');
    statusTags['model'] = pack.model || '';
    statusTags['stream'] = String(pack.stream);
    statusTags['success'] = String(pack.success);
    statusTags['operation_type'] = pack.operationType || '';
    // 스펙: features 없으면 값 전달 X
    if (pack.features) statusTags['features'] = pack.features;

    const statusFields = {};
    if (pack.temperature != null) statusFields['temperature.n'] = pack.temperature;
    // 스펙(2026.04.02): finish_reason 필드 추가
    if (pack.finishReason) statusFields['finish_reason'] = pack.finishReason;
    Object.assign(statusFields, pack.getTokenFields());
    if (pack.errorType === 'api_error' || pack.errorType === 'program_error') {
        statusFields['error'] = pack.error;
        statusFields['error_type'] = pack.errorType;
    }
    _sendLlmLogSinkPack(statusTags, statusFields, '', pack.index);

    // 2. system_message
    if (pack.systemTexts && pack.systemTexts.length > 0) {
        for (const text of pack.systemTexts) {
            _sendMessage('system_message', pack, {}, text || '');
        }
    }


    // 3. input_message
    // 긴 프롬프트(10초+ 호출 등)에서도 프롬프트가 반드시 전송되도록 방어.
    // pack.promptText가 undefined/null인 경우에도 빈 문자열로 강제.
    const inputPrompt = (pack.promptText != null) ? String(pack.promptText) : '';
    if (!inputPrompt) {
        Logger.print('WHATAP-LLM-016',
            'input_message promptText empty (model=' + (pack.model || '') + ', op=' + (pack.operationType || '') + ')',
            false);
    }
    _sendMessage('input_message', pack, {}, inputPrompt);

    // 4. output_message
    const outFields = pack.reasoningText ? {reasoning: pack.reasoningText} : {};
    _sendMessage('output_message', pack, outFields, pack.completionText || '');

    // 5. tool
    if (pack.toolCallsText) {
        const toolFields = _parseToolCalls(pack.toolCallsText);
        _sendMessage('tool', pack, toolFields, pack.toolCallsText);
    }

    // 6. tool_result
    if (pack.toolResultsText) {
        _sendMessage('tool_result', pack, {}, pack.toolResultsText);
    }

    // meter
    if (pack.success) {
        _llmMeterCount++;
        statCollector.meterIncrement();
    }

    // ---------- LLM Metric stat collector feed ----------
    try {
        const provider = pack.provider || '';
        const url = pack.url || '';
        const opType = pack.operationType || 'unknown';
        const model = pack.model || 'unknown';

        // active count 감소
        statCollector.onCallEnd(model, opType, provider, url, ctx);

        // perf — TPOT 공식: (latency - ttft) / (output_tokens - 1)
        // 조건: ttft/latency non-null AND output_tokens > 1. stream 여부 무관.
        let tpot = null;
        if (pack.ttft != null && pack.latency != null && (pack.output_tokens || 0) > 1) {
            tpot = (pack.latency - pack.ttft) / (pack.output_tokens - 1);
        }
        statCollector.recordPerf(model, provider, opType, url, pack.latency, pack.ttft, tpot);

        // tokens (with costs and stream/error counts)
        statCollector.recordTokens(model, provider, opType, url, {
            input_tokens: pack.input_tokens || 0,
            output_tokens: pack.output_tokens || 0,
            cached_tokens: pack.cached_tokens || 0,
            reasoning_tokens: pack.reasoning_tokens || 0,
            input_cost: pack.input_cost || 0,
            output_cost: pack.output_cost || 0,
            cached_cost: pack.cached_cost || 0,
            error_count: pack.success ? 0 : 1,
            stream: !!pack.stream,
        });

        // feature
        statCollector.recordFeature(model, provider, opType, url, pack.features || '');

        // errors
        if (!pack.success && pack.errorType) {
            statCollector.recordError(model, provider, opType, url, pack.errorType);
            // last_api_error_count는 ctx 단위로 한 번만 → ctx 전달 시 가능. 여기서는 호출별 누적.
            statCollector.recordLastApiError(model, provider, opType, url, pack.errorType);
        }

        // api status (HTTP statusCode를 별도 저장하지 않으므로 errorType→근사 처리)
        // 실제 status code 캡처를 위해 향후 ctx._llm_http_status 같은 필드 추가 가능
    } catch (e) {
        Logger.printError('WHATAP-LLM-006', 'stat collector feed failed', e, false);
    }
}

function _baseTags(pack, llmLogType) {
    return {
        'llm_log_type': llmLogType,
        '@txid': pack.txid || '',
        '@step_id': pack.stepId || '',
        'provider': pack.provider || '',
    };
}

function _sendMessage(llmLogType, pack, fields, content) {
    const tags = _baseTags(pack, llmLogType);
    if (content) {
        _sendChunked(tags, fields, content, pack.index);
    } else {
        _sendLlmLogSinkPack(tags, fields, '', pack.index);
    }
}

function _parseToolCalls(toolCallsText) {
    const fields = {};
    try {
        const tcList = JSON.parse(toolCallsText);
        if (Array.isArray(tcList)) {
            const names = [];
            const args = [];
            for (const tc of tcList) {
                const fn = tc.function || '';
                if (typeof fn === 'object') {
                    names.push(fn.name || '');
                    // bracket notation: strict mode에서 .arguments를 Function.prototype.arguments로
                    // 오판하는 린터 회피 (실제로는 parsed JSON object의 plain property)
                    args.push(fn['arguments'] || '');
                } else {
                    names.push(fn);
                    args.push(tc['arguments'] || '');
                }
            }
            fields['function'] = names.filter(Boolean).join(',');
            fields['arguments'] = args.filter(Boolean).join(',');
        }
    } catch (e) {
        // ignore parse errors
    }
    return fields;
}

function _sendLlmLogSinkPack(tags, fields, content, index) {
    try {
        // filter null values
        const cleanTags = {};
        for (const [k, v] of Object.entries(tags)) {
            if (v != null) cleanTags[k] = v;
        }
        const cleanFields = {};
        for (const [k, v] of Object.entries(fields)) {
            if (v != null) cleanFields[k] = v;
        }
        cleanFields['index'] = index;

        // truncate content
        if (content) {
            const encoded = Buffer.from(content, 'utf-8');
            if (encoded.length > _MAX_CONTENT_BYTES) {
                content = encoded.subarray(0, _MAX_CONTENT_BYTES).toString('utf-8');
            }
        }

        const now = DateUtil.currentTime();
        const p = LogSinkPack.getLogSinkPack({
            t: now,
            category: LlmLogSinkPack.CATEGORY,
            tags: cleanTags,
            fields: {},
            line: now,
            content: content || '',
        });

        // numeric fields (.n suffix) preserve type, others as string
        for (const [k, v] of Object.entries(cleanFields)) {
            if (typeof v === 'number') {
                if (Number.isInteger(v)) {
                    p.fields.putLong(k, v);
                } else {
                    p.fields.putFloat(k, v);
                }
            } else {
                p.fields.putString(k, String(v));
            }
        }

        p.pcode = getLlmPcode();

        const bout = new DataOutputX();
        bout.writePack(p, null);
        const bytes = bout.toByteArray();
        AsyncSender.send_llm_relaypack(bytes);
    } catch (e) {
        Logger.printError('WHATAP-LLM-004', '_sendLlmLogSinkPack failed', e, false);
    }
}

function _sendChunked(tags, fields, content, index) {
    try {
        if (!content) {
            _sendLlmLogSinkPack(tags, fields, '', index);
            return;
        }

        const contentBytes = Buffer.from(content, 'utf-8');
        const totalBytes = contentBytes.length;

        if (totalBytes <= _MAX_CONTENT_BYTES) {
            const chunkFields = Object.assign({}, fields, {chunk_index: 0, chunk_total: 1});
            _sendLlmLogSinkPack(tags, chunkFields, content, index);
            return;
        }

        // split on utf-8 boundaries
        const chunks = [];
        let i = 0;
        while (i < totalBytes) {
            let end = Math.min(i + _MAX_CONTENT_BYTES, totalBytes);
            if (end < totalBytes) {
                while (end > i && (contentBytes[end] & 0xC0) === 0x80) end--;
            }
            chunks.push(contentBytes.subarray(i, end).toString('utf-8'));
            i = end;
        }

        for (let idx = 0; idx < chunks.length; idx++) {
            const chunkFields = Object.assign({}, fields, {
                chunk_index: idx,
                chunk_total: chunks.length,
            });
            _sendLlmLogSinkPack(tags, chunkFields, chunks[idx], index);
        }
    } catch (e) {
        Logger.printError('WHATAP-LLM-005', '_sendChunked failed', e, false);
    }
}

/**
 * 대기 중인 LLM pack 큐를 동기적으로 즉시 처리.
 * TX_END보다 먼저 input_message/output_message 등이 송신되도록 보장.
 * setImmediate 지연으로 인한 순서 역전 회피.
 * Node.js single-thread: _processLoop과 동시 실행 불가능. 단순 drain 안전.
 */
function flushPendingPacks() {
    while (_queue.length > 0) {
        const p = _queue.shift();
        try {
            _processPack(p);
        } catch (e) {
            Logger.printError('WHATAP-LLM-021', 'flushPendingPacks processPack failed', e, false);
        }
    }
    // 큐가 비었으면 pending setImmediate는 no-op으로 끝날 것. _processing 플래그는 그쪽에서 false로.
}

module.exports = {
    dispatchLlmPack,
    sendLlmTxStatus,
    flushPendingPacks,
    getLlmMeterCountAndReset,
};
