/**
 * Codex Usage Scanner Service
 *
 * Scans Codex CLI session files (JSONL) for token usage data and records it to the database.
 * Codex sessions are stored in: {codexHomeDir}/.codex/sessions/{year}/{month}/{day}/*.jsonl
 *
 * Key difference from Claude sessions:
 * - Codex uses "event_msg" with "token_count" payload instead of "assistant" messages
 * - "last_token_usage" contains per-turn incremental usage (for billing)
 * - "total_token_usage" is cumulative; used to detect when total has increased (skip duplicates)
 * - "cached_input_tokens" maps to cache_read_tokens (90% discount applied via cacheRead pricing)
 * - No cache creation concept (Codex uses different caching model)
 * - reasoning_output_tokens are bundled into output_tokens for billing
 */

import { promises as fs } from 'fs';
import path from 'path';
import { usageDb, userDb } from '../database/db.js';
import { calculateCost, normalizeModelName } from './pricing.js';
import { getUserPaths } from './user-directories.js';

/**
 * Scan all Codex session files for a user
 * @param {string} userUuid - User UUID
 */
export async function scanUserCodexSessions(userUuid) {
  const userPaths = getUserPaths(userUuid);
  const sessionsDir = path.join(userPaths.codexHomeDir, '.codex', 'sessions');
  const scanStatePath = path.join(userPaths.codexHomeDir, '.codex-usage-scan-state.json');

  // Check if codex sessions directory exists
  try {
    await fs.access(sessionsDir);
  } catch {
    // No codex sessions directory yet
    return;
  }

  // Load scan state
  let scanState = { lastScanTime: null, scannedSessions: {} };
  try {
    const stateContent = await fs.readFile(scanStatePath, 'utf8');
    scanState = JSON.parse(stateContent);
  } catch {
    // File doesn't exist or is invalid, start fresh
  }

  let newRecordsCount = 0;

  // Sessions are organized as sessions/{year}/{month}/{day}/*.jsonl
  let yearDirs;
  try {
    yearDirs = await fs.readdir(sessionsDir, { withFileTypes: true });
  } catch {
    return;
  }

  for (const yearDir of yearDirs) {
    if (!yearDir.isDirectory()) continue;
    const yearPath = path.join(sessionsDir, yearDir.name);

    let monthDirs;
    try {
      monthDirs = await fs.readdir(yearPath, { withFileTypes: true });
    } catch {
      continue;
    }

    for (const monthDir of monthDirs) {
      if (!monthDir.isDirectory()) continue;
      const monthPath = path.join(yearPath, monthDir.name);

      let dayDirs;
      try {
        dayDirs = await fs.readdir(monthPath, { withFileTypes: true });
      } catch {
        continue;
      }

      for (const dayDir of dayDirs) {
        if (!dayDir.isDirectory()) continue;
        const dayPath = path.join(monthPath, dayDir.name);

        let sessionFiles;
        try {
          sessionFiles = await fs.readdir(dayPath);
        } catch {
          continue;
        }

        for (const sessionFile of sessionFiles) {
          if (!sessionFile.endsWith('.jsonl')) continue;

          const sessionId = sessionFile.replace('.jsonl', '');
          const sessionPath = path.join(dayPath, sessionFile);

          // Get file stats to check if modified
          let stats;
          try {
            stats = await fs.stat(sessionPath);
          } catch {
            continue;
          }
          const lastModified = stats.mtime.toISOString();

          // Check if we need to scan this session
          const lastScanned = scanState.scannedSessions[sessionId];
          if (lastScanned && lastScanned.lastModified === lastModified) {
            // Already scanned and file hasn't changed
            continue;
          }

          // Scan the session file
          try {
            const scanResult = await scanCodexSessionFile(
              userUuid,
              sessionId,
              sessionPath,
              lastScanned?.lastLine || 0,
              lastScanned?.lastTotalTokens || { input: 0, output: 0 }
            );
            newRecordsCount += scanResult.recordsAdded;

            // Update scan state
            scanState.scannedSessions[sessionId] = {
              lastModified,
              lastLine: scanResult.lastLine,
              lastTotalTokens: scanResult.lastTotalTokens,
              lastScan: new Date().toISOString()
            };
          } catch (error) {
            console.error(`[CodexUsageScanner] Error scanning session ${sessionId}:`, error.message);
          }
        }
      }
    }
  }

  // Save scan state
  scanState.lastScanTime = new Date().toISOString();
  try {
    await fs.writeFile(scanStatePath, JSON.stringify(scanState, null, 2));
  } catch (error) {
    console.error(`[CodexUsageScanner] Error saving scan state for user ${userUuid}:`, error.message);
  }

  if (newRecordsCount > 0) {
    console.log(`[CodexUsageScanner] User ${userUuid}: added ${newRecordsCount} new Codex usage records`);
  }
}

/**
 * Scan a single Codex session JSONL file for usage data
 * @param {string} userUuid - User UUID
 * @param {string} sessionId - Session ID (filename without .jsonl)
 * @param {string} filePath - Full path to JSONL file
 * @param {number} startLine - Line number to start from (for incremental scanning)
 * @param {Object} lastTotalTokens - Last recorded total token counts (to detect increments)
 * @returns {{ recordsAdded: number, lastLine: number, lastTotalTokens: Object }}
 */
async function scanCodexSessionFile(userUuid, sessionId, filePath, startLine, lastTotalTokens) {
  const content = await fs.readFile(filePath, 'utf8');
  const lines = content.trim().split('\n');

  let recordsAdded = 0;
  // currentModel tracks the active model per turn (from turn_context events)
  let currentModel = 'gpt-5.2-codex';
  // prevTotal tracks cumulative total to detect new API calls (skip session-resume snapshots)
  let prevTotal = (lastTotalTokens?.input || 0) + (lastTotalTokens?.output || 0);
  let currentLastTotalTokens = { ...lastTotalTokens } || { input: 0, output: 0 };

  for (let i = startLine; i < lines.length; i++) {
    const line = lines[i];
    if (!line.trim()) continue;

    try {
      const entry = JSON.parse(line);

      // Track current model from turn_context events
      if (entry.type === 'turn_context' && entry.payload?.model) {
        currentModel = entry.payload.model;
        continue;
      }

      // Process token_count events
      if (
        entry.type !== 'event_msg' ||
        entry.payload?.type !== 'token_count' ||
        !entry.payload?.info
      ) {
        continue;
      }

      const { total_token_usage, last_token_usage } = entry.payload.info;

      if (!total_token_usage || !last_token_usage) continue;

      // Calculate current cumulative total
      const currentTotal =
        (total_token_usage.input_tokens || 0) + (total_token_usage.output_tokens || 0);

      // Skip if total hasn't increased (session-resume snapshot or duplicate event)
      if (currentTotal <= prevTotal) continue;

      // Extract per-turn usage from last_token_usage
      const inputTokens = last_token_usage.input_tokens || 0;
      const cachedInputTokens = last_token_usage.cached_input_tokens || 0;
      // Bundle reasoning_output_tokens with output_tokens (same billing rate)
      const outputTokens =
        (last_token_usage.output_tokens || 0) + (last_token_usage.reasoning_output_tokens || 0);

      // Skip if no actual usage
      if (inputTokens === 0 && cachedInputTokens === 0 && outputTokens === 0) {
        prevTotal = currentTotal;
        continue;
      }

      const rawModel = currentModel;
      const model = normalizeModelName(rawModel);

      // Calculate cost:
      // - inputTokens: standard input price
      // - cachedInputTokens: cache read price (90% discount for Codex)
      // - outputTokens: output price
      // - No cache creation tokens for Codex
      const cost = calculateCost({
        model,
        inputTokens,
        outputTokens,
        cacheReadTokens: cachedInputTokens,
        cacheCreationTokens: 0
      });

      const entryTimestamp = entry.timestamp || new Date().toISOString();
      const entryDate = entryTimestamp.split('T')[0];

      // Insert usage record with source='codex'
      usageDb.insertRecord({
        user_uuid: userUuid,
        session_id: sessionId,
        model,
        raw_model: rawModel,
        input_tokens: inputTokens,
        output_tokens: outputTokens,
        cache_read_tokens: cachedInputTokens,
        cache_creation_tokens: 0,
        cost_usd: cost,
        source: 'codex',
        created_at: entryTimestamp
      });

      // Update daily summary
      usageDb.upsertDailySummary({
        user_uuid: userUuid,
        date: entryDate,
        model,
        total_input_tokens: inputTokens,
        total_output_tokens: outputTokens,
        total_cost_usd: cost,
        session_count: 0,
        request_count: 1
      });

      // Update tracking state
      prevTotal = currentTotal;
      currentLastTotalTokens = {
        input: total_token_usage.input_tokens || 0,
        output: total_token_usage.output_tokens || 0
      };
      recordsAdded++;
    } catch (parseError) {
      // Skip lines that can't be parsed
      continue;
    }
  }

  return {
    recordsAdded,
    lastLine: lines.length,
    lastTotalTokens: currentLastTotalTokens
  };
}

/**
 * Manually trigger a Codex scan (for testing or admin use)
 */
export async function triggerCodexScan() {
  console.log('[CodexUsageScanner] Manual Codex scan triggered');
  const users = userDb.getAllUsers();
  for (const user of users) {
    if (!user.uuid) continue;
    try {
      await scanUserCodexSessions(user.uuid);
    } catch (error) {
      console.error(`[CodexUsageScanner] Error scanning user ${user.uuid}:`, error.message);
    }
  }
  return { success: true, message: 'Codex scan completed' };
}
