'use strict';

/**
 * LLM Metric stat collector (singleton).
 * Observers (openai-observer, llm-sender) push events here;
 * counter tasks read & reset every interval (default 5s).
 *
 * Key shape: "model||provider||operation_type||url"
 */

const _FEATURES = ['vision', 'tool_use', 'reasoning', 'structured_output',
    'websearch', 'computer_use', 'document'];

function _key(model, provider, opType, url) {
    return [
        model || 'unknown',
        provider || '',
        opType || 'unknown',
        url || '',
    ].join('||');
}

function _parseKey(k) {
    const parts = k.split('||');
    return { model: parts[0], provider: parts[1], op_type: parts[2], url: parts[3] };
}

// --- state ---
let _active = new Map();        // key -> current running count (gauge)
let _activePeak = new Map();    // key -> peak during current window (찰나 호출도 가시화)

// model -> { provider, url, ts(ms) } : active 호출에 host/url 태깅용 보조 캐시.
// TTL/cap으로 leak 방지 (동적 model 이름, fine-tune ID 등 누적 케이스 대비).
const _modelInfo = new Map();
const _MODEL_CACHE_TTL_MS_DEFAULT = 10 * 60 * 1000; // 10 min
const _MODEL_CACHE_MAX_SIZE_DEFAULT = 500;
let _modelCacheConf = null;     // { ttlMs, maxSize } lazy-loaded

let _apiStatus4xx = new Map();  // key -> count
let _apiStatus5xx = new Map();

let _errApi = new Map();
let _errProgram = new Map();
let _errLastApi = new Map();

let _featCalls = new Map();     // key -> count
let _featCounts = new Map();    // key -> { feat: count }

let _perfCallCount = new Map();
let _perfLatencySum = new Map();
let _perfTtftSum = new Map();
let _perfTtftCount = new Map();
let _perfTpotSum = new Map();
let _perfTpotCount = new Map();

let _tokCalls = new Map();
let _tokError = new Map();
let _tokStream = new Map();
let _tokCounts = new Map();     // key -> { input_tokens, output_tokens, ... }
let _tokCosts = new Map();      // key -> { input_cost, output_cost, cached_cost }

let _meterCount = 0;

function _inc(map, k, by) {
    map.set(k, (map.get(k) || 0) + (by || 1));
}
function _incNested(map, k, sub, by) {
    let inner = map.get(k);
    if (!inner) { inner = {}; map.set(k, inner); }
    inner[sub] = (inner[sub] || 0) + (by || 1);
}

// =============================================================
// Update API (called from observer / llm-sender)
// =============================================================

function onCallStart(model, opType, ctx) {
    const k = _key(model, '', opType, '');
    const cur = (_active.get(k) || 0) + 1;
    _active.set(k, cur);
    // peak는 현재 윈도우에서의 최고값 기록 (짧은 호출도 최소 1로 가시화)
    const peak = _activePeak.get(k) || 0;
    if (cur > peak) _activePeak.set(k, cur);
    // ctx-level inflight tracking: tx 종료 시 누락된 onCallEnd를 강제 정리.
    // (예외 throw, finalize 미호출 등 edge case에서 active count leak 방지)
    if (ctx) {
        if (!ctx._llm_inflight_keys) ctx._llm_inflight_keys = [];
        ctx._llm_inflight_keys.push(k);
    }
}

function _getModelCacheConf() {
    if (_modelCacheConf) return _modelCacheConf;
    let ttl = _MODEL_CACHE_TTL_MS_DEFAULT;
    let cap = _MODEL_CACHE_MAX_SIZE_DEFAULT;
    try {
        const conf = require('../conf/configure');
        const ttlSec = parseInt(conf.getProperty('llm_model_cache_ttl_sec', 600));
        if (ttlSec > 0) ttl = ttlSec * 1000;
        const maxSize = parseInt(conf.getProperty('llm_model_cache_max_size', 500));
        if (maxSize > 0) cap = maxSize;
    } catch (e) {}
    _modelCacheConf = { ttlMs: ttl, maxSize: cap };
    return _modelCacheConf;
}

function setHostUrl(model, provider, url) {
    if (!model || (!provider && !url)) return;
    let info = _modelInfo.get(model);
    if (info) {
        // re-insert (LRU 효과: Map insertion order로 oldest 추적)
        _modelInfo.delete(model);
    } else {
        info = { provider: '', url: '', ts: 0 };
    }
    if (provider) info.provider = provider;
    if (url) info.url = url;
    info.ts = Date.now();
    _modelInfo.set(model, info);

    // hard cap 초과 시 oldest 제거 (Map iteration order = insertion order)
    const cap = _getModelCacheConf().maxSize;
    if (_modelInfo.size > cap) {
        const oldestKey = _modelInfo.keys().next().value;
        if (oldestKey != null) _modelInfo.delete(oldestKey);
    }
}

/**
 * TTL 만료된 entry 제거. counter task에서 주기적으로 호출.
 * 5초 tick에 1회만 도는 작업이라 비용 무시.
 */
function pruneModelCache() {
    const ttl = _getModelCacheConf().ttlMs;
    const cutoff = Date.now() - ttl;
    for (const [k, v] of _modelInfo) {
        if (v.ts < cutoff) _modelInfo.delete(k);
    }
}

function onCallEnd(model, opType, provider, url, ctx) {
    const k = _key(model, '', opType, '');
    const cur = (_active.get(k) || 0) - 1;
    if (cur <= 0) _active.delete(k); else _active.set(k, cur);
    setHostUrl(model, provider, url);
    // 정상 onCallEnd가 처리한 key는 inflight에서 제거 (cleanupInflight가 중복 decrement하지 않도록)
    if (ctx && ctx._llm_inflight_keys) {
        const idx = ctx._llm_inflight_keys.indexOf(k);
        if (idx >= 0) ctx._llm_inflight_keys.splice(idx, 1);
    }
}

/**
 * tx 종료 시 호출. ctx에 남아있는 inflight key를 모두 강제 decrement.
 * 정상 케이스에선 비어있고, 예외 경로에서 leak된 카운트만 정리됨.
 */
function cleanupInflight(ctx) {
    if (!ctx || !ctx._llm_inflight_keys || ctx._llm_inflight_keys.length === 0) return;
    for (const k of ctx._llm_inflight_keys) {
        const cur = (_active.get(k) || 0) - 1;
        if (cur <= 0) _active.delete(k); else _active.set(k, cur);
    }
    ctx._llm_inflight_keys = null;
}

function recordApiStatus(model, provider, opType, url, statusCode) {
    if (statusCode == null) return;
    const k = _key(model, provider, opType, url);
    if (statusCode >= 400 && statusCode < 500) _inc(_apiStatus4xx, k);
    else if (statusCode >= 500 && statusCode < 600) _inc(_apiStatus5xx, k);
}

function recordError(model, provider, opType, url, errorType) {
    const k = _key(model, provider, opType, url);
    if (errorType === 'api_error') _inc(_errApi, k);
    else if (errorType === 'program_error') _inc(_errProgram, k);
}

function recordLastApiError(model, provider, opType, url, errorType) {
    if (errorType !== 'api_error') return;
    const k = _key(model, provider, opType, url);
    _inc(_errLastApi, k);
}

function recordFeature(model, provider, opType, url, features) {
    const k = _key(model, provider, opType, url);
    _inc(_featCalls, k);
    if (features) {
        const list = String(features).split(',');
        for (const raw of list) {
            const f = raw.trim();
            if (_FEATURES.indexOf(f) >= 0) {
                _incNested(_featCounts, k, f);
            }
        }
    }
}

function recordPerf(model, provider, opType, url, latencyMs, ttftMs, tpotMs) {
    const k = _key(model, provider, opType, url);
    _inc(_perfCallCount, k);
    if (latencyMs != null && latencyMs >= 0) {
        _perfLatencySum.set(k, (_perfLatencySum.get(k) || 0) + latencyMs);
    }
    if (ttftMs != null && ttftMs >= 0) {
        _perfTtftSum.set(k, (_perfTtftSum.get(k) || 0) + ttftMs);
        _inc(_perfTtftCount, k);
    }
    if (tpotMs != null && tpotMs >= 0) {
        _perfTpotSum.set(k, (_perfTpotSum.get(k) || 0) + tpotMs);
        _inc(_perfTpotCount, k);
    }
}

function recordTokens(model, provider, opType, url, opts) {
    opts = opts || {};
    const k = _key(model, provider, opType, url);
    _inc(_tokCalls, k);
    if (opts.error_count) _inc(_tokError, k, opts.error_count);
    if (opts.stream) _inc(_tokStream, k);

    const tokenKeys = ['input_tokens', 'output_tokens', 'cached_tokens', 'reasoning_tokens',
        'cache_creation_input_tokens', 'cache_read_input_tokens'];
    for (const tk of tokenKeys) {
        if (opts[tk]) _incNested(_tokCounts, k, tk, opts[tk]);
    }
    const costKeys = ['input_cost', 'output_cost', 'cached_cost'];
    for (const ck of costKeys) {
        if (opts[ck]) {
            let inner = _tokCosts.get(k);
            if (!inner) { inner = {}; _tokCosts.set(k, inner); }
            inner[ck] = (inner[ck] || 0) + opts[ck];
        }
    }
}

function meterIncrement() {
    _meterCount++;
}

// =============================================================
// Snapshot & reset (called by counter tasks)
// =============================================================

function snapshotAndResetActive() {
    // peak(윈도우 내 최고 동시 실행수)를 emit. 짧은 호출도 >= 1로 가시화.
    // _active(현재 running)는 gauge이므로 reset하지 않음.
    // peak는 swap 패턴으로 GC 압박 최소화: 기존 Map을 그대로 반환하고 새 Map 할당.
    const peak = _activePeak;
    _activePeak = new Map();
    // 다음 윈도우의 peak 시작점은 현재 running count (지속 호출 가시화)
    _active.forEach((v, k) => {
        if (v > 0) _activePeak.set(k, v);
    });
    // _modelInfo는 read-only로 참조만 전달 (task 처리 중 race 없음 — Node single-thread).
    return { active: peak, modelInfo: _modelInfo };
}

function snapshotAndResetApiStatus() {
    const out = { x4: _apiStatus4xx, x5: _apiStatus5xx };
    _apiStatus4xx = new Map();
    _apiStatus5xx = new Map();
    return out;
}

function snapshotAndResetErrors() {
    const out = { api: _errApi, program: _errProgram, lastApi: _errLastApi };
    _errApi = new Map();
    _errProgram = new Map();
    _errLastApi = new Map();
    return out;
}

function snapshotAndResetFeatures() {
    const out = { calls: _featCalls, counts: _featCounts };
    _featCalls = new Map();
    _featCounts = new Map();
    return out;
}

function snapshotAndResetPerf() {
    const out = {
        callCount: _perfCallCount,
        latencySum: _perfLatencySum,
        ttftSum: _perfTtftSum,
        ttftCount: _perfTtftCount,
        tpotSum: _perfTpotSum,
        tpotCount: _perfTpotCount,
    };
    _perfCallCount = new Map();
    _perfLatencySum = new Map();
    _perfTtftSum = new Map();
    _perfTtftCount = new Map();
    _perfTpotSum = new Map();
    _perfTpotCount = new Map();
    return out;
}

function snapshotAndResetTokens() {
    const out = {
        calls: _tokCalls,
        error: _tokError,
        stream: _tokStream,
        counts: _tokCounts,
        costs: _tokCosts,
    };
    _tokCalls = new Map();
    _tokError = new Map();
    _tokStream = new Map();
    _tokCounts = new Map();
    _tokCosts = new Map();
    return out;
}

function getAndResetMeterCount() {
    const c = _meterCount;
    _meterCount = 0;
    return c;
}

module.exports = {
    FEATURES: _FEATURES,
    parseKey: _parseKey,
    onCallStart, onCallEnd, setHostUrl, pruneModelCache, cleanupInflight,
    recordApiStatus, recordError, recordLastApiError,
    recordFeature, recordPerf, recordTokens,
    meterIncrement,
    snapshotAndResetActive, snapshotAndResetApiStatus, snapshotAndResetErrors,
    snapshotAndResetFeatures, snapshotAndResetPerf, snapshotAndResetTokens,
    getAndResetMeterCount,
};
