'use strict';

const conf = require('../conf/configure');
const IdUtil = require('./llm-id-util');

// USD per 1M tokens: [input, output, cached_input]
const MODEL_PRICING = {
    // OpenAI - GPT-5
    'gpt-5': [1.25, 10.00, 0.125],
    'gpt-5-mini': [0.25, 2.00, 0.025],
    'gpt-5-nano': [0.05, 0.40, 0.005],
    'gpt-5.1': [1.25, 10.00, 0.125],
    'gpt-5.2': [1.75, 14.00, 0.175],
    // OpenAI - GPT-4
    'gpt-4o': [2.50, 10.00, 1.25],
    'gpt-4o-mini': [0.15, 0.60, 0.075],
    'gpt-4o-audio-preview': [2.50, 10.00, null],
    'gpt-4o-mini-audio-preview': [0.15, 0.60, null],
    'gpt-4.1': [2.00, 8.00, 0.50],
    'gpt-4.1-mini': [0.40, 1.60, 0.10],
    'gpt-4.1-nano': [0.10, 0.40, 0.025],
    'gpt-4-turbo': [10.00, 30.00, null],
    'gpt-4': [30.00, 60.00, null],
    'gpt-3.5-turbo': [0.50, 1.50, null],
    // OpenAI - o series
    'o1': [15.00, 60.00, 7.50],
    'o1-pro': [150.00, 600.00, null],
    'o3': [2.00, 8.00, 0.50],
    'o3-mini': [1.10, 4.40, 0.55],
    'o3-pro': [20.00, 80.00, null],
    'o4-mini': [1.10, 4.40, 0.275],
    // OpenAI - Embeddings
    'text-embedding-3-small': [0.02, 0, null],
    'text-embedding-3-large': [0.13, 0, null],
    'text-embedding-ada-002': [0.10, 0, null],
};

// prefix match fallback (longer prefixes first)
const MODEL_PRICING_PREFIX = [
    ['gpt-5.2', [1.75, 14.00, 0.175]],
    ['gpt-5.1', [1.25, 10.00, 0.125]],
    ['gpt-5-nano', [0.05, 0.40, 0.005]],
    ['gpt-5-mini', [0.25, 2.00, 0.025]],
    ['gpt-5', [1.25, 10.00, 0.125]],
    ['gpt-4o-mini', [0.15, 0.60, 0.075]],
    ['gpt-4o', [2.50, 10.00, 1.25]],
    ['gpt-4.1-nano', [0.10, 0.40, 0.025]],
    ['gpt-4.1-mini', [0.40, 1.60, 0.10]],
    ['gpt-4.1', [2.00, 8.00, 0.50]],
    ['gpt-4-turbo', [10.00, 30.00, null]],
    ['gpt-4', [30.00, 60.00, null]],
    ['gpt-3.5-turbo', [0.50, 1.50, null]],
    ['o4-mini', [1.10, 4.40, 0.275]],
    ['o3-pro', [20.00, 80.00, null]],
    ['o3-mini', [1.10, 4.40, 0.55]],
    ['o3', [2.00, 8.00, 0.50]],
    ['o1-pro', [150.00, 600.00, null]],
    ['o1', [15.00, 60.00, 7.50]],
    ['text-embedding-3-small', [0.02, 0, null]],
    ['text-embedding-3-large', [0.13, 0, null]],
    ['text-embedding-ada', [0.10, 0, null]],
];

const URL_OPERATION_MAP = [
    ['/v1/chat/completions', 'chat'],
    ['/v1/responses', 'response'],
    ['/v1/completions', 'completion'],
    ['/v1/embeddings', 'embedding'],
    ['/v1/images/generations', 'image_generation'],
    ['/v1/images/edits', 'image_edit'],
    ['/v1/audio/transcriptions', 'audio_transcription'],
    ['/v1/audio/translations', 'audio_translation'],
    ['/v1/audio/speech', 'audio_speech'],
    ['/v1/moderations', 'moderation'],
    ['/v1/fine_tuning', 'fine_tuning'],
    ['/v1/files', 'file'],
    ['/v1/assistants', 'assistant'],
    ['/v1/threads', 'thread'],
    ['/v1/vector_stores', 'vector_store'],
    ['/v1/batches', 'batch'],
    ['/v1/messages', 'chat'],
];

const OPENAI_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, dimensions: STEP_STATUS spec은 suffix 없음 (`.n` 미부여)

function classifyOperation(url) {
    if (!url) return 'unknown';
    for (const [pattern, opType] of URL_OPERATION_MAP) {
        if (url.indexOf(pattern) >= 0) return opType;
    }
    return 'unknown';
}

function _parseCustomPricing() {
    const raw = conf.getProperty('llm_model_pricing', '');
    if (!raw) return null;
    const pricing = {};
    for (const entry of raw.split(',')) {
        const parts = entry.trim().split('|');
        if (parts.length < 3) continue;
        const model = parts[0].trim();
        const inp = parseFloat(parts[1]);
        const out = parseFloat(parts[2]);
        const cached = parts.length > 3 && parts[3].trim() ? parseFloat(parts[3]) : null;
        if (!isNaN(inp) && !isNaN(out)) {
            pricing[model] = [inp, out, cached];
        }
    }
    return Object.keys(pricing).length > 0 ? pricing : null;
}

function _getPricing(model) {
    if (!model) return null;
    const custom = _parseCustomPricing();
    if (custom && custom[model]) return custom[model];
    if (MODEL_PRICING[model]) return MODEL_PRICING[model];
    for (const [prefix, p] of MODEL_PRICING_PREFIX) {
        if (model.startsWith(prefix)) return p;
    }
    return null;
}

function LlmLogSinkPack() {
    // base
    this.txid = null;
    this.stepId = null;
    this.index = 0;
    this.provider = '';
    this.url = '';
    this.operationType = 'unknown';

    // status
    this.model = null;
    this.stream = false;
    this.success = false;
    this.features = '';
    this.temperature = null;
    this.finishReason = null;
    this.error = null;
    this.errorType = null;

    // tokens
    this.input_tokens = null;
    this.output_tokens = null;
    this.total_tokens_count = null;
    this.cached_tokens = null;
    this.reasoning_tokens = null;
    this.audio_input_tokens = null;
    this.audio_output_tokens = null;
    this.accepted_prediction_tokens = null;
    this.rejected_prediction_tokens = null;
    this.embedding_count = null;
    this.dimensions = null;

    // cost (USD)
    this.cost = null;
    this.input_cost = null;
    this.output_cost = null;
    this.cached_cost = null;

    // latency (ms)
    this._startTime = 0;
    this._endTime = 0;
    this.latency = null;
    this.ttft = null;

    // messages
    this.systemTexts = [];
    this.promptText = '';
    this.completionText = '';
    this.reasoningText = '';

    // tool
    this.toolCallsText = '';
    this.toolResultsText = '';
}

LlmLogSinkPack.CATEGORY = '#LlmCallLog';

LlmLogSinkPack.classifyOperation = classifyOperation;

LlmLogSinkPack.prototype.setContext = function (ctx) {
    if (!ctx) return;
    if (!this.txid) {
        // writeLong이 쓰는 바이트값(BigInt(num))과 일치하는 문자열 사용. ctx 라이프사이클 동안 캐싱.
        this.txid = IdUtil.ctxTxidStr(ctx);
    }
    if (!this.stepId) {
        var sid = ctx._llm_step_id || ctx.step_id || 0;
        // step_id는 endHttpc에서 이미 ctx._stepIdStr로 캐싱되어 있을 수 있음
        this.stepId = ctx._stepIdStr || IdUtil.stepIdStr(sid);
    }
    if (!this.provider) {
        const httpcUrl = ctx._llm_httpc_url || '';
        if (httpcUrl) {
            let url = httpcUrl;
            if (url.indexOf('://') >= 0) {
                url = url.split('://')[1];
            }
            this.provider = url.split('/')[0];
            const slashIdx = url.indexOf('/');
            if (slashIdx >= 0) {
                this.url = url.substring(slashIdx);
            }
        }
    }
};

LlmLogSinkPack.prototype.setError = function (error, errorType) {
    this.error = String(error);
    this.errorType = errorType;
};

LlmLogSinkPack.prototype.setTokens = function (tokenDict) {
    for (const [key, val] of Object.entries(tokenDict)) {
        if (val != null && this.hasOwnProperty(key)) {
            this[key] = val;
        }
    }
};

LlmLogSinkPack.prototype.calculateCost = function () {
    const pricing = _getPricing(this.model);
    if (!pricing) return;
    const [inputPrice, outputPrice, cachedPrice] = pricing;

    const inputCount = this.input_tokens || 0;
    const outputCount = this.output_tokens || 0;
    const cached = (this.cached_tokens || 0);

    let inputCost, cachedCost;
    if (cachedPrice != null && cached > 0) {
        const nonCached = Math.max(inputCount - cached, 0);
        cachedCost = (cached * cachedPrice) / 1_000_000;
        inputCost = (nonCached * inputPrice) / 1_000_000 + cachedCost;
    } else {
        inputCost = (inputCount * inputPrice) / 1_000_000;
        cachedCost = 0;
    }

    const outputCost = (outputCount * outputPrice) / 1_000_000;

    this.input_cost = +inputCost.toFixed(6);
    this.output_cost = +outputCost.toFixed(6);
    this.cached_cost = +cachedCost.toFixed(6);
    this.cost = +(inputCost + outputCost).toFixed(6);
};

LlmLogSinkPack.prototype.getTokenFields = function () {
    const fields = {};
    for (const key of OPENAI_TOKEN_FIELDS) {
        const val = this[key];
        if (val != null) fields[key + '.n'] = val;
    }
    if (this.cost != null) fields['cost.n'] = this.cost;
    if (this.input_cost) fields['input_cost.n'] = this.input_cost;
    if (this.output_cost) fields['output_cost.n'] = this.output_cost;
    if (this.cached_cost) fields['cached_cost.n'] = this.cached_cost;
    if (this.latency != null) fields['latency.n'] = this.latency;
    if (this.ttft != null) fields['ttft.n'] = this.ttft;
    // 스펙: STEP_STATUS는 dimensions/embedding_count에 .n suffix 없음
    if (this.dimensions != null) fields['dimensions'] = this.dimensions;
    if (this.embedding_count != null) fields['embedding_count'] = this.embedding_count;
    return fields;
};

module.exports = LlmLogSinkPack;
