'use strict';

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

const DEFAULT_MAX_CONTEXT_BYTES = 36000;
const DEFAULT_MAX_DIFF_BYTES = 24000;
const DEFAULT_MAX_UNTRACKED_BYTES = 6000;

function isCodeReviewRequest(message) {
  const text = String(message || '').toLowerCase();
  return /\bcode\s+review\b/.test(text)
    || /\breview\s+(?:the\s+)?(?:code|diff|patch|changes?|changed files|pull request|pr)\b/.test(text)
    || /\breview\s+(?:my\s+|the\s+)?(?:local\s+|uncommitted\s+|unstaged\s+|staged\s+|working\s+tree\s+|workspace\s+)?changes?\b/.test(text)
    || /\b(?:review|inspect|check|audit)\s+(?:my\s+|the\s+)?(?:local\s+|working\s+tree\s+|workspace\s+|uncommitted\s+|unstaged\s+|staged\s+)?(?:diff|patch|changes?)\b/.test(text)
    || /\b(?:what|show|tell\s+me)\s+(?:changed|is\s+changed|local\s+changes|unstaged\s+changes|uncommitted\s+changes)\b/.test(text)
    || /\b(?:check|show|run)\s+(?:git\s+)?status\b/.test(text)
    || /\b(?:do|run|perform|take)\s+(?:a\s+)?(?:close\s+|thorough\s+)?code\s+review\b/.test(text)
    || /\b(?:review|audit)\s+my\s+(?:changes?|diff|patch|code)\b/.test(text);
}

function looksLikePrematureCodeReviewReply(text) {
  const s = String(text || '').trim();
  if (!s) return false;
  const lower = s.toLowerCase();
  const promisesInspection = /\b(?:let me|i'll|i will|now let me|going to)\b[\s\S]{0,160}\b(?:look|inspect|check|get|read|open|review)\b[\s\S]{0,120}\b(?:diffs?|status|changes?|files?|codebase|repo|repository)\b/.test(lower);
  const hasFindingsShape = /\b(?:findings?|issues?|bug|regression|risk|blocker|severity|no issues?|looks good|line\s+\d+|:\d+)\b/.test(lower);
  return promisesInspection && !hasFindingsShape;
}

function snapshotIndicatesDirty(codeReviewContextBlock) {
  const block = String(codeReviewContextBlock || '');
  if (!/## Code Review Workspace Snapshot/.test(block)) return false;
  const statusMatch = block.match(/### Git status\s*\n([\s\S]*?)(?:\n### |\n\n### |$)/);
  if (!statusMatch) return false;
  const status = statusMatch[1].trim();
  return !!status && status !== '(clean)';
}

function looksLikeContradictoryCleanReviewReply(text, codeReviewContextBlock) {
  if (!snapshotIndicatesDirty(codeReviewContextBlock)) return false;
  const lower = String(text || '').toLowerCase();
  if (!lower.trim()) return false;
  return /\bworking tree (?:is )?clean\b/.test(lower)
    || /\bworktree (?:is )?clean\b/.test(lower)
    || /\bno (?:local |uncommitted |unstaged |staged )?changes\b/.test(lower)
    || /\bnothing (?:is )?(?:staged|unstaged|changed|to review)\b/.test(lower)
    || /\bno staged,?\s+unstaged,?\s+(?:or\s+)?untracked\b/.test(lower)
    || /\bindex (?:is )?clean\b/.test(lower);
}

function runGit(cwd, args, opts = {}) {
  try {
    return execFileSync('git', args, {
      cwd,
      encoding: 'utf8',
      timeout: opts.timeoutMs || 10000,
      maxBuffer: opts.maxBuffer || 1024 * 1024,
      stdio: ['ignore', 'pipe', 'pipe'],
    }).trim();
  } catch (err) {
    if (opts.optional) return '';
    throw err;
  }
}

function truncateBytes(text, maxBytes) {
  const s = String(text || '');
  const buf = Buffer.from(s, 'utf8');
  if (buf.length <= maxBytes) return s;
  const head = buf.subarray(0, Math.floor(maxBytes * 0.7)).toString('utf8');
  const tail = buf.subarray(buf.length - Math.floor(maxBytes * 0.25)).toString('utf8');
  return `${head}\n\n[truncated ${buf.length - Buffer.byteLength(head) - Buffer.byteLength(tail)} bytes]\n\n${tail}`;
}

function gitRootFor(cwd) {
  if (!cwd) return null;
  const resolved = path.resolve(cwd);
  if (!fs.existsSync(resolved)) return null;
  const root = runGit(resolved, ['rev-parse', '--show-toplevel'], { optional: true });
  return root || null;
}

function listUntrackedFiles(root, statusShort) {
  const fromStatus = String(statusShort || '')
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.startsWith('?? '))
    .map((line) => line.slice(3).trim())
    .filter(Boolean);
  if (fromStatus.length > 0) return fromStatus;
  const raw = runGit(root, ['ls-files', '--others', '--exclude-standard'], { optional: true });
  return raw ? raw.split('\n').map((s) => s.trim()).filter(Boolean) : [];
}

function isSensitivePreviewPath(relPath) {
  const parts = String(relPath || '').split(/[\\/]+/).map((part) => part.toLowerCase());
  const base = parts[parts.length - 1] || '';
  if (!base) return true;
  if (base === '.env' || base.startsWith('.env.')) return true;
  if (['.npmrc', '.netrc', '.pypirc', '.dockercfg', 'credentials', 'credentials.json'].includes(base)) return true;
  if (/\.(pem|key|p12|pfx|crt|cer|kdb|jks)$/i.test(base)) return true;
  if (/(^|[-_.])(secret|secrets|credential|credentials|token|tokens|apikey|api-key|private-key)([-_.]|$)/i.test(base)) return true;
  return parts.includes('.ssh') || parts.includes('.aws') || parts.includes('.gcloud') || parts.includes('.azure');
}

function readUntrackedPreviews(root, files, maxBytes) {
  const chunks = [];
  let remaining = maxBytes;
  for (const rel of files.slice(0, 8)) {
    if (remaining <= 0) break;
    if (isSensitivePreviewPath(rel)) {
      chunks.push(`--- ${rel} (untracked preview skipped: sensitive filename) ---`);
      continue;
    }
    const abs = path.resolve(root, rel);
    if (!abs.startsWith(root + path.sep) && abs !== root) continue;
    let stat;
    try { stat = fs.statSync(abs); } catch { continue; }
    if (!stat.isFile() || stat.size > 200000) continue;
    const ext = path.extname(abs).toLowerCase();
    if (/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|gz|tar|mp4|mov|mp3|wasm|sqlite|db)$/i.test(ext)) continue;
    let raw;
    try { raw = fs.readFileSync(abs, 'utf8'); } catch { continue; }
    if (raw.includes('\u0000')) continue;
    const preview = truncateBytes(raw, Math.min(remaining, 3000));
    const block = `--- ${rel} (untracked preview) ---\n${preview}`;
    chunks.push(block);
    remaining -= Buffer.byteLength(block, 'utf8');
  }
  return chunks.join('\n\n');
}

function firstExistingRef(root, refs) {
  for (const ref of refs) {
    const exists = runGit(root, ['rev-parse', '--verify', '--quiet', ref], { optional: true });
    if (exists) return ref;
  }
  return null;
}

function buildCodeReviewContextBlock({ message, cwd, maxContextBytes = DEFAULT_MAX_CONTEXT_BYTES } = {}) {
  if (!isCodeReviewRequest(message)) return '';
  const root = gitRootFor(cwd);
  if (!root) {
    return [
      '## Code Review Workspace Snapshot',
      `Requested cwd: ${cwd || '(none)'}`,
      'Git context: unavailable. The cwd is not an existing git worktree.',
      '',
      'Instruction: explain that no git diff could be inspected and ask the user for a repository path or patch.',
    ].join('\n');
  }

  const branch = runGit(root, ['branch', '--show-current'], { optional: true }) || '(detached)';
  const head = runGit(root, ['rev-parse', '--short', 'HEAD'], { optional: true }) || '(unknown)';
  const status = runGit(root, ['status', '--short'], { optional: true }) || '(clean)';
  const unstagedStat = runGit(root, ['diff', '--stat'], { optional: true });
  const stagedStat = runGit(root, ['diff', '--cached', '--stat'], { optional: true });
  const unstagedDiff = runGit(root, ['diff', '--no-ext-diff', '--unified=80'], { optional: true, maxBuffer: 2 * 1024 * 1024 });
  const stagedDiff = runGit(root, ['diff', '--cached', '--no-ext-diff', '--unified=80'], { optional: true, maxBuffer: 2 * 1024 * 1024 });
  const untrackedFiles = listUntrackedFiles(root, status);
  const untrackedPreview = readUntrackedPreviews(root, untrackedFiles, DEFAULT_MAX_UNTRACKED_BYTES);

  let committedStat = '';
  let committedDiff = '';
  if (!unstagedDiff && !stagedDiff && untrackedFiles.length === 0) {
    const baseRef = firstExistingRef(root, ['main', 'origin/main', 'master', 'origin/master']);
    if (baseRef) {
      const mergeBase = runGit(root, ['merge-base', 'HEAD', baseRef], { optional: true });
      if (mergeBase) {
        committedStat = runGit(root, ['diff', '--stat', `${mergeBase}..HEAD`], { optional: true });
        committedDiff = runGit(root, ['diff', '--no-ext-diff', '--unified=80', `${mergeBase}..HEAD`], { optional: true, maxBuffer: 2 * 1024 * 1024 });
      }
    }
  }

  const diffSections = [];
  if (stagedStat) diffSections.push(`### Staged diff stat\n${stagedStat}`);
  if (unstagedStat) diffSections.push(`### Unstaged diff stat\n${unstagedStat}`);
  if (committedStat) diffSections.push(`### Branch diff stat\n${committedStat}`);
  if (stagedDiff) diffSections.push(`### Staged diff\n${truncateBytes(stagedDiff, DEFAULT_MAX_DIFF_BYTES)}`);
  if (unstagedDiff) diffSections.push(`### Unstaged diff\n${truncateBytes(unstagedDiff, DEFAULT_MAX_DIFF_BYTES)}`);
  if (committedDiff) diffSections.push(`### Branch diff\n${truncateBytes(committedDiff, DEFAULT_MAX_DIFF_BYTES)}`);
  if (untrackedFiles.length > 0) {
    diffSections.push(`### Untracked files\n${untrackedFiles.slice(0, 30).join('\n')}`);
    if (untrackedPreview) diffSections.push(`### Untracked file previews\n${untrackedPreview}`);
  }
  if (diffSections.length === 0) diffSections.push('### Diff\nNo staged, unstaged, untracked, or branch diff was found.');

  const block = [
    '## Code Review Workspace Snapshot',
    `Requested cwd: ${path.resolve(cwd)}`,
    `Project: ${root}`,
    `Branch: ${branch}`,
    `HEAD: ${head}`,
    '',
    '### Git status',
    status,
    '',
    ...diffSections,
    '',
    '### Code Review Instructions',
    'You already have the git status and diff above. Do not say you will inspect, read, or fetch diffs.',
    `Start with: Checked ${root} (requested cwd ${path.resolve(cwd)}, branch ${branch}, status ${status === '(clean)' ? 'clean' : 'dirty'}).`,
    'Produce the review now. Lead with findings ordered by severity. Cite file paths and line references when the diff gives enough context.',
    'If there are no issues, say that clearly and include remaining test gaps or residual risk.',
  ].join('\n');

  return truncateBytes(block, maxContextBytes);
}

module.exports = {
  isCodeReviewRequest,
  looksLikePrematureCodeReviewReply,
  snapshotIndicatesDirty,
  looksLikeContradictoryCleanReviewReply,
  buildCodeReviewContextBlock,
  gitRootFor,
};
