const fs = require("node:fs/promises");
const path = require("node:path");
const os = require("node:os");

const PROVIDERS = ["Claude", "Codex", "Gemini"];

async function scanUsage(options = {}) {
  const homeDir = options.homeDir || os.homedir();
  const timeZone = options.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai";
  const now = options.now || new Date();

  const records = [];
  const diagnostics = [];

  await collectProvider({
    root: path.join(homeDir, ".claude", "projects"),
    matcher: (file) => file.endsWith(".jsonl"),
    parseLine: parseClaudeLine,
    records,
    diagnostics
  });

  await collectProvider({
    root: path.join(homeDir, ".codex", "sessions"),
    matcher: (file) => file.endsWith(".jsonl"),
    parseLine: parseCodexLine,
    records,
    diagnostics
  });

  await collectProvider({
    root: path.join(homeDir, ".gemini"),
    matcher: (file) => file.endsWith(".jsonl") && file.includes(`${path.sep}chats${path.sep}`),
    parseLine: parseGeminiLine,
    records,
    diagnostics
  });

  const summary = summarizeRecords(records, { now, timeZone });
  return {
    ...summary,
    diagnostics
  };
}

async function collectProvider({ root, matcher, parseLine, records, diagnostics }) {
  const files = await listFiles(root).catch((error) => {
    diagnostics.push({
      level: "warn",
      source: root,
      message: error.code === "ENOENT" ? "目录不存在" : error.message
    });
    return [];
  });

  for (const file of files) {
    if (!matcher(file)) continue;
    const text = await fs.readFile(file, "utf8").catch((error) => {
      diagnostics.push({ level: "warn", source: file, message: error.message });
      return "";
    });
    for (const line of text.split(/\r?\n/)) {
      if (!line.trim()) continue;
      const record = parseLine(line, file);
      if (record) records.push(record);
    }
  }
}

async function listFiles(root) {
  const out = [];
  async function walk(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        await walk(fullPath);
      } else if (entry.isFile()) {
        out.push(fullPath);
      }
    }
  }
  await walk(root);
  return out;
}

function parseClaudeLine(line, sourceFile) {
  const obj = parseJson(line);
  const usage = obj?.message?.usage || obj?.usage;
  if (!usage) return null;

  const inputTokens = intValue(usage.input_tokens);
  const outputTokens = intValue(usage.output_tokens);
  const cachedTokens =
    intValue(usage.cache_read_input_tokens) +
    intValue(usage.cache_creation_input_tokens);

  return normalizeRecord({
    provider: "Claude",
    source: sourceLabel(sourceFile, "Claude"),
    sourceFile,
    timestamp: obj.timestamp || obj.createdAt || obj.date,
    sessionId: obj.sessionId,
    cwd: obj.cwd,
    model: obj.message?.model || obj.model || "Claude",
    inputTokens,
    outputTokens,
    cachedTokens,
    reasoningTokens: 0,
    toolTokens: 0
  });
}

function parseCodexLine(line, sourceFile) {
  const obj = parseJson(line);
  const usage = obj?.payload?.info?.last_token_usage;
  if (!usage) return null;

  return normalizeRecord({
    provider: "Codex",
    source: sourceLabel(sourceFile, "Codex"),
    sourceFile,
    timestamp: obj.timestamp,
    sessionId: obj.payload?.id || obj.payload?.thread_id,
    cwd: obj.payload?.cwd,
    model: obj.payload?.model || "Codex",
    inputTokens: intValue(usage.input_tokens),
    outputTokens: intValue(usage.output_tokens),
    cachedTokens: intValue(usage.cached_input_tokens),
    reasoningTokens: intValue(usage.reasoning_output_tokens),
    toolTokens: 0
  });
}

function parseGeminiLine(line, sourceFile) {
  const obj = parseJson(line);
  const tokens = obj?.tokens;
  if (!tokens) return null;

  return normalizeRecord({
    provider: "Gemini",
    source: sourceLabel(sourceFile, "Gemini"),
    sourceFile,
    timestamp: obj.timestamp,
    sessionId: obj.id,
    cwd: obj.cwd,
    model: obj.model || "Gemini",
    inputTokens: intValue(tokens.input),
    outputTokens: intValue(tokens.output),
    cachedTokens: intValue(tokens.cached),
    reasoningTokens: intValue(tokens.thoughts),
    toolTokens: intValue(tokens.tool),
    explicitTotal: intValue(tokens.total)
  });
}

function normalizeRecord(input) {
  const timestamp = input.timestamp ? new Date(input.timestamp) : null;
  if (!timestamp || Number.isNaN(timestamp.getTime())) return null;

  const computedTotal =
    input.inputTokens +
    input.outputTokens +
    input.cachedTokens +
    input.reasoningTokens +
    input.toolTokens;

  return {
    provider: input.provider,
    source: input.source,
    sourceFile: input.sourceFile,
    timestamp: timestamp.toISOString(),
    sessionId: input.sessionId || "",
    cwd: input.cwd || "",
    model: input.model || input.provider,
    inputTokens: input.inputTokens,
    outputTokens: input.outputTokens,
    cachedTokens: input.cachedTokens,
    reasoningTokens: input.reasoningTokens,
    toolTokens: input.toolTokens,
    totalTokens: input.explicitTotal || computedTotal
  };
}

function summarizeRecords(records, options = {}) {
  const now = options.now || new Date();
  const timeZone = options.timeZone || "Asia/Shanghai";
  const todayKey = dateKey(now, timeZone);
  const monthKey = todayKey.slice(0, 7);

  const providerTotals = Object.fromEntries(PROVIDERS.map((provider) => [provider, emptyBucket(provider)]));
  const sourceBuckets = new Map();
  const dailyBuckets = new Map();
  let todayTokens = 0;
  let monthTokens = 0;
  let totalTokens = 0;
  let cachedTokens = 0;
  let inputTokens = 0;
  let outputTokens = 0;
  let reasoningTokens = 0;

  for (const record of records) {
    const key = dateKey(new Date(record.timestamp), timeZone);
    const recordMonth = key.slice(0, 7);
    const total = record.totalTokens;

    totalTokens += total;
    cachedTokens += record.cachedTokens;
    inputTokens += record.inputTokens;
    outputTokens += record.outputTokens;
    reasoningTokens += record.reasoningTokens;
    if (key === todayKey) todayTokens += total;
    if (recordMonth === monthKey) monthTokens += total;

    addToBucket(providerTotals[record.provider] || (providerTotals[record.provider] = emptyBucket(record.provider)), record);

    const sourceKey = `${record.provider}|${record.source}`;
    if (!sourceBuckets.has(sourceKey)) {
      sourceBuckets.set(sourceKey, {
        provider: record.provider,
        source: record.source,
        model: record.model,
        sourceFile: record.sourceFile,
        ...emptyTokenFields()
      });
    }
    addToBucket(sourceBuckets.get(sourceKey), record);

    if (!dailyBuckets.has(key)) {
      dailyBuckets.set(key, Object.fromEntries(PROVIDERS.map((provider) => [provider, 0])));
    }
    dailyBuckets.get(key)[record.provider] = (dailyBuckets.get(key)[record.provider] || 0) + total;
  }

  const dailyTrend = Array.from(dailyBuckets.entries())
    .sort(([a], [b]) => a.localeCompare(b))
    .slice(-30)
    .map(([date, values]) => ({ date, ...values }));

  const sourceRows = Array.from(sourceBuckets.values())
    .sort((a, b) => b.totalTokens - a.totalTokens)
    .slice(0, 8);

  const cacheHitRate = totalTokens > 0 ? Math.round((cachedTokens / totalTokens) * 100) : 0;

  return {
    generatedAt: now.toISOString(),
    timeZone,
    records,
    cards: {
      todayTokens,
      monthTokens,
      totalTokens,
      cacheHitRate,
      inputTokens,
      outputTokens,
      cachedTokens,
      reasoningTokens,
      sessionCount: new Set(records.map((record) => record.sessionId || record.sourceFile)).size
    },
    providerTotals,
    dailyTrend,
    sourceRows,
    insights: buildInsights({ todayTokens, monthTokens, totalTokens, cacheHitRate, providerTotals, records })
  };
}

function buildInsights({ todayTokens, cacheHitRate, providerTotals, records }) {
  const topProvider = Object.values(providerTotals)
    .filter((bucket) => bucket.totalTokens > 0)
    .sort((a, b) => b.totalTokens - a.totalTokens)[0];

  const insights = [];
  if (topProvider) {
    insights.push({
      tone: "warn",
      title: `${topProvider.provider} 是当前主要用量来源`,
      copy: `已从本地日志统计 ${formatTokens(topProvider.totalTokens)} Tokens。`
    });
  }
  if (cacheHitRate > 0) {
    insights.push({
      tone: "good",
      title: "检测到缓存 Tokens",
      copy: `当前 ${cacheHitRate}% 的统计用量来自缓存上下文。`
    });
  }
  insights.push({
    tone: "spike",
      title: "今日用量快照已更新",
      copy: `当前本地日期已统计 ${formatTokens(todayTokens)} Tokens。`
  });
  insights.push({
    tone: "info",
    title: "仅执行本地扫描",
    copy: `已解析 ${records.length} 条用量记录，未上传对话内容。`
  });
  return insights.slice(0, 4);
}

function addToBucket(bucket, record) {
  bucket.inputTokens += record.inputTokens;
  bucket.outputTokens += record.outputTokens;
  bucket.cachedTokens += record.cachedTokens;
  bucket.reasoningTokens += record.reasoningTokens;
  bucket.toolTokens += record.toolTokens;
  bucket.totalTokens += record.totalTokens;
  bucket.count += 1;
}

function emptyBucket(provider) {
  return {
    provider,
    ...emptyTokenFields()
  };
}

function emptyTokenFields() {
  return {
    inputTokens: 0,
    outputTokens: 0,
    cachedTokens: 0,
    reasoningTokens: 0,
    toolTokens: 0,
    totalTokens: 0,
    count: 0
  };
}

function dateKey(date, timeZone) {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit"
  }).formatToParts(date);
  const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
  return `${values.year}-${values.month}-${values.day}`;
}

function sourceLabel(sourceFile, provider) {
  if (provider === "Claude" && sourceFile.includes(`${path.sep}.claude${path.sep}projects${path.sep}`)) {
    return "projects/*.jsonl";
  }
  if (provider === "Codex" && sourceFile.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`)) {
    return "sessions/*.jsonl";
  }
  if (provider === "Gemini" && sourceFile.includes(`${path.sep}chats${path.sep}`)) {
    return "tmp/chats/*.jsonl";
  }
  return path.basename(sourceFile);
}

function parseJson(line) {
  try {
    return JSON.parse(line);
  } catch {
    return null;
  }
}

function intValue(value) {
  return Number.isFinite(Number(value)) ? Number(value) : 0;
}

function formatTokens(value) {
  if (value >= 100_000_000) return trimNumber(value / 100_000_000, 2) + "亿";
  if (value >= 10_000) return trimNumber(value / 10_000, 1) + "万";
  return String(value);
}

function trimNumber(value, digits) {
  return value.toFixed(digits).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
}

module.exports = {
  parseClaudeLine,
  parseCodexLine,
  parseGeminiLine,
  summarizeRecords,
  scanUsage,
  formatTokens
};
