'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawnSync } = require('child_process');

// ── Output parsing ────────────────────────────────────────────────────────────

function parseRelationships(text) {
  const rel = { callers: 0, callees: 0, imports: 0 };
  const countAfterColon = (str) => {
    const colon = str.indexOf(':');
    if (colon < 0) return 0;
    return str.slice(colon + 1).split(',').filter(s => s.trim().length > 0).length;
  };
  for (const line of text.split('\n')) {
    const t = line.trim();
    if (/^Callers?:/.test(t)) {
      rel.callers += countAfterColon(t);
    } else if (/^Callees?:/.test(t)) {
      rel.callees += countAfterColon(t);
    } else if (/^Imports?\/calls?:/.test(t) || /^Imported\/called by:/.test(t)) {
      rel.imports += countAfterColon(t);
    }
  }
  return rel;
}

function countSurfacedFiles(text) {
  const blockStart = text.indexOf('[Related files from dependency graph]');
  if (blockStart < 0) return 0;
  return (text.slice(blockStart).match(/\[[\d.]+\]/g) || []).length;
}

function parseEnrichmentOutput(raw, subcommand) {
  let text = '';
  let filesSurfaced = 0;
  let structuredRelationships = null;
  let repoLanguage = '';

  try {
    const json = JSON.parse(raw.trim());
    const metrics = json.metrics || json.output?.metrics || json.hookSpecificOutput?.metrics;
    if (metrics && typeof metrics === 'object') {
      filesSurfaced = Number(metrics.files_surfaced ?? metrics.filesSurfaced ?? filesSurfaced) || 0;
      structuredRelationships = {
        callers: Number(metrics.relationship_callers ?? metrics.callers ?? 0) || 0,
        callees: Number(metrics.relationship_callees ?? metrics.callees ?? 0) || 0,
        imports: Number(metrics.relationship_imports ?? metrics.imports ?? 0) || 0,
      };
      repoLanguage = metrics.repo_language || metrics.repoLanguage || '';
    }
    if (subcommand === 'post-grep' && json.output) {
      text = json.output.text || '';
      if (!filesSurfaced) filesSurfaced = (json.output.relatedFiles || []).length;
    } else if (json.hookSpecificOutput) {
      const uto = json.hookSpecificOutput.updatedToolOutput;
      let rawContent = typeof uto === 'string' ? uto : uto?.content;
      if (Array.isArray(rawContent)) {
        rawContent = rawContent.map(c => (typeof c === 'string' ? c : c?.text || '')).join('');
      } else if (typeof rawContent !== 'string') {
        rawContent = '';
      }
      text = rawContent || json.hookSpecificOutput.additionalContext || '';
      if (!filesSurfaced) filesSurfaced = countSurfacedFiles(text);
    } else if (json.systemMessage) {
      text = json.systemMessage;
      if (!filesSurfaced) filesSurfaced = countSurfacedFiles(text);
    }
  } catch {
    text = raw;
    filesSurfaced = countSurfacedFiles(text);
  }

  return {
    filesSurfaced,
    relationships: structuredRelationships || parseRelationships(text),
    repoLanguage,
    tokens: estimateTokens(text),
  };
}

// estimateTokens approximates token count from text length. ~4 chars/token is
// the standard rough heuristic for English/code with BPE tokenizers. This is
// the count of tokens our plugin injected into the model's context — used by
// the portal to estimate tokens saved.
function estimateTokens(text) {
  const s = String(text || '');
  if (!s) return 0;
  return Math.ceil(s.length / 4);
}

function extractRelatedFiles(text) {
  const files = new Set();
  for (const line of String(text || '').split('\n')) {
    const atMatches = line.matchAll(/ @ ([^\[]+?) \[[\d.]+\]/g);
    for (const match of atMatches) {
      files.add(match[1].trim());
    }
    if (/^\s*(Imports\/calls|Imported\/called by):/.test(line)) {
      const values = line.slice(line.indexOf(':') + 1).split(',');
      for (const value of values) {
        const match = value.trim().match(/^(.+?) \[[\d.]+\]$/);
        if (match) files.add(match[1].trim());
      }
    }
  }
  return [...files].filter(Boolean);
}

// ── rg-shim output parsing ────────────────────────────────────────────────────

function parseRgShimOutput(stdout) {
  const blockStart = stdout.indexOf('[Related files from dependency graph]');
  const block = blockStart >= 0 ? stdout.slice(blockStart) : '';
  return {
    filesSurfaced: countSurfacedFiles(stdout),
    relationships: parseRelationships(block),
    enrichmentBlock: block,
    tokens: estimateTokens(block),
  };
}

// ── Arg helpers (shared by hook modules that receive --cwd) ───────────────────

function extractCwd(args) {
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--cwd' && args[i + 1]) return args[i + 1];
    const m = (args[i] || '').match(/^--cwd=(.+)$/);
    if (m) return m[1];
  }
  return null;
}

function stripCwd(args) {
  const out = [];
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--cwd') { i++; continue; }
    if (args[i].startsWith('--cwd=')) continue;
    out.push(args[i]);
  }
  return out;
}

// ── Tool-call classification ──────────────────────────────────────────────────

const SHELL_TOOL_NAMES = new Set(['bash', 'shell']);

// extractGrepPattern pulls the search pattern from a PostToolUse payload — the
// `pattern` arg of a grep tool, or the command string of a bash/shell grep.
// Returns '' when none. This is the search query, not the LLM prompt.
function extractGrepPattern(payload) {
  if (!payload) return '';
  const input = payload.tool_input || payload.toolInput || {};
  if (input.pattern) return String(input.pattern);
  const toolName = (payload.tool_name || payload.toolName || '').toLowerCase();
  if (SHELL_TOOL_NAMES.has(toolName)) {
    return String(input.command || input.cmd || '');
  }
  return '';
}

function isGrepPayload(payload) {
  const toolName = (payload.tool_name || payload.toolName || '').toLowerCase();
  if (toolName.includes('grep')) return true;
  if (SHELL_TOOL_NAMES.has(toolName)) {
    const input = payload.tool_input || payload.toolInput || {};
    const cmd = input.command || input.cmd || '';
    return /\b(grep|rg|ripgrep)\b/.test(cmd);
  }
  return false;
}

// ── Repo-enabled check ────────────────────────────────────────────────────────

function codeGraphCacheDir() {
  return process.env.CIELARA_CACHE_DIR || path.join(os.homedir(), '.cache', 'cielara-code');
}

function codeGraphStatePath() {
  return path.join(codeGraphCacheDir(), 'state.json');
}

function canonicalizePath(filePath) {
  try {
    return fs.realpathSync(filePath);
  } catch {
    return path.resolve(filePath);
  }
}

function normalizeGitPath(value) {
  if (process.platform !== 'win32') return value;
  const match = /^\/([a-zA-Z])\/(.*)$/.exec(value);
  if (!match) return value;
  return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`;
}

function runGit(cwd, args) {
  const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
  if (result.status !== 0) throw new Error(`not inside a git repository: ${cwd}`);
  return normalizeGitPath(result.stdout.trim());
}

function isRepoEnabled(cwd) {
  try {
    const state = JSON.parse(fs.readFileSync(codeGraphStatePath(), 'utf8'));
    if (!Array.isArray(state.enabledRepos) || state.enabledRepos.length === 0) return false;
    const repoRoot = runGit(cwd, ['rev-parse', '--show-toplevel']);
    return state.enabledRepos.includes(canonicalizePath(repoRoot));
  } catch {
    return false;
  }
}

// ── Stdin helper ──────────────────────────────────────────────────────────────

function readStdin() {
  return new Promise((resolve, reject) => {
    const chunks = [];
    process.stdin.on('data', c => chunks.push(c));
    process.stdin.on('end', () => resolve(Buffer.concat(chunks)));
    process.stdin.on('error', reject);
  });
}

module.exports = {
  parseRelationships,
  countSurfacedFiles,
  parseEnrichmentOutput,
  parseRgShimOutput,
  estimateTokens,
  extractRelatedFiles,
  extractCwd,
  stripCwd,
  isGrepPayload,
  extractGrepPattern,
  isRepoEnabled,
  codeGraphCacheDir,
  codeGraphStatePath,
  canonicalizePath,
  runGit,
  readStdin,
};
