import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from 'node:fs';
import { join, extname, basename } from 'node:path';
import { homedir } from 'node:os';
import { createHash } from 'node:crypto';
import { RepoMetadata } from './types.js';

const CACHE_DIR = join(homedir(), '.candura', 'cache');
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour

// Language detection by extension
const LANGUAGE_MAP: Record<string, string> = {
  '.ts': 'typescript', '.tsx': 'typescript',
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript',
  '.py': 'python', '.pyx': 'python',
  '.rs': 'rust',
  '.go': 'go',
  '.java': 'java',
  '.kt': 'kotlin',
  '.rb': 'ruby',
  '.php': 'php',
  '.cs': 'csharp',
  '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.h': 'cpp', '.hpp': 'cpp',
  '.c': 'c',
  '.swift': 'swift',
  '.scala': 'scala',
  '.vue': 'vue',
  '.svelte': 'svelte',
  '.sql': 'sql',
  '.sh': 'shell', '.bash': 'shell',
  '.yaml': 'yaml', '.yml': 'yaml',
  '.json': 'json',
  '.md': 'markdown',
  '.html': 'html', '.htm': 'html',
  '.css': 'css', '.scss': 'scss', '.less': 'less',
};

const TEST_PATTERNS = [
  'test/', 'tests/', '__tests__/', 'spec/', 'specs/',
  'test_', '_test.', '.test.', '.spec.',
  'jest.config', 'vitest.config', 'pytest.ini', 'phpunit',
];

const TEST_FRAMEWORKS: Record<string, string> = {
  'jest.config': 'jest',
  'vitest.config': 'vitest',
  'pytest.ini': 'pytest',
  'conftest.py': 'pytest',
  '.mocharc': 'mocha',
  'karma.conf': 'karma',
  'phpunit': 'phpunit',
};

/**
 * Index a repository and return metadata.
 * Uses cache if available and fresh.
 */
export function indexRepo(repoPath: string): RepoMetadata {
  const repoId = createHash('sha256').update(repoPath).digest('hex').slice(0, 16);
  const cachePath = join(CACHE_DIR, `${repoId}.json`);

  // Check cache
  const cached = loadCache(cachePath, repoPath);
  if (cached) return cached;

  // Build fresh index
  const metadata = buildIndex(repoPath, repoId);

  // Save cache
  saveCache(cachePath, metadata);

  return metadata;
}

function loadCache(cachePath: string, repoPath: string): RepoMetadata | null {
  if (!existsSync(cachePath)) return null;

  try {
    const data = JSON.parse(readFileSync(cachePath, 'utf-8')) as RepoMetadata;

    // Check age
    if (Date.now() - data.indexed_at > CACHE_MAX_AGE_MS) return null;

    // Check if git HEAD changed (means repo has new commits)
    const currentHead = getGitHead(repoPath);
    if (currentHead && data.git_head !== currentHead) return null;

    return data;
  } catch {
    return null;
  }
}

function saveCache(cachePath: string, metadata: RepoMetadata): void {
  try {
    mkdirSync(CACHE_DIR, { recursive: true });
    writeFileSync(cachePath, JSON.stringify(metadata, null, 2));
  } catch {
    // Cache write failure is non-fatal
  }
}

function buildIndex(repoPath: string, repoId: string): RepoMetadata {
  // Get tracked files via git
  let files: string[] = [];
  try {
    const output = execSync('git ls-files', {
      cwd: repoPath,
      encoding: 'utf-8',
      maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large repos
      timeout: 10_000,
    });
    files = output.trim().split('\n').filter(Boolean);
  } catch {
    // Not a git repo or git not available — fallback to directory scan
    files = scanDirectory(repoPath);
  }

  // Count languages
  const languages: Record<string, number> = {};
  for (const file of files) {
    const ext = extname(file).toLowerCase();
    const lang = LANGUAGE_MAP[ext];
    if (lang) {
      languages[lang] = (languages[lang] || 0) + 1;
    }
  }

  // Detect tests
  const hasTests = files.some(f =>
    TEST_PATTERNS.some(p => f.toLowerCase().includes(p))
  );

  // Detect test framework
  let testFramework: string | null = null;
  for (const [pattern, framework] of Object.entries(TEST_FRAMEWORKS)) {
    if (files.some(f => f.includes(pattern))) {
      testFramework = framework;
      break;
    }
  }

  // Has CI
  const hasCi = files.some(f =>
    f.includes('.github/workflows') ||
    f.includes('Jenkinsfile') ||
    f.includes('.gitlab-ci') ||
    f.includes('.circleci')
  );

  // Package count (number of package managers detected)
  const packageFiles = ['package.json', 'requirements.txt', 'Cargo.toml', 'go.mod',
    'pom.xml', 'build.gradle', 'Gemfile', 'composer.json'];
  const packageCount = packageFiles.filter(p => files.some(f => basename(f) === p)).length;

  // Has .claudeignore
  const hasClaudeignore = existsSync(join(repoPath, '.claudeignore'));

  // Top-level directories
  const topDirectories = getTopDirectories(repoPath);

  // Average file size (sample 50 files for speed)
  const avgFileTokens = sampleFileSize(repoPath, files);

  // Directory depth
  const maxDepth = files.reduce((max, f) => {
    const depth = f.split('/').length;
    return Math.max(max, depth);
  }, 0);

  // ASSUMPTION: average 80 lines per code file. No measurement for this specific repo.
  // Real averages vary: small utility files ~20 lines, large modules ~500+ lines.
  // This is used only for display/metadata, not for cost calculation, so inaccuracy is tolerable.
  const codeFiles = files.filter(f => {
    const ext = extname(f).toLowerCase();
    return LANGUAGE_MAP[ext] !== undefined;
  });
  const totalLoc = codeFiles.length * 80; // ASSUMPTION: 80 lines/file average (not measured)

  return {
    repo_id: repoId,
    path: repoPath,
    total_files: files.length,
    total_loc: totalLoc,
    languages,
    avg_file_size_tokens: avgFileTokens,
    directory_depth_max: maxDepth,
    has_tests: hasTests,
    test_framework: testFramework,
    has_ci: hasCi,
    package_count: packageCount,
    has_claudeignore: hasClaudeignore,
    top_directories: topDirectories,
    git_head: getGitHead(repoPath) || 'unknown',
    indexed_at: Date.now(),
  };
}

function getGitHead(repoPath: string): string | null {
  try {
    return execSync('git rev-parse HEAD', {
      cwd: repoPath,
      encoding: 'utf-8',
      timeout: 3000,
    }).trim();
  } catch {
    return null;
  }
}

function getTopDirectories(repoPath: string): string[] {
  try {
    const entries = readdirSync(repoPath, { withFileTypes: true });
    return entries
      .filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
      .map(e => e.name)
      .slice(0, 20);
  } catch {
    return [];
  }
}

/**
 * Sample random files to estimate average token size.
 * Tokens ≈ characters / 4 (HEURISTIC for code — rougher than the 3.5 general estimate
 * because code files have more whitespace and repeated patterns that tokenize efficiently).
 * Accuracy: ±30% depending on language and coding style.
 * Used by estimator for per-file cache creation cost calculation.
 */
function sampleFileSize(repoPath: string, files: string[]): number {
  const codeExtensions = new Set(Object.keys(LANGUAGE_MAP));
  const codeFiles = files.filter(f => codeExtensions.has(extname(f).toLowerCase()));

  if (codeFiles.length === 0) return 1000; // Default

  // Sample up to 30 files
  const sampleSize = Math.min(30, codeFiles.length);
  const sampled: string[] = [];
  const step = Math.floor(codeFiles.length / sampleSize);
  for (let i = 0; i < codeFiles.length && sampled.length < sampleSize; i += step) {
    sampled.push(codeFiles[i]);
  }

  let totalChars = 0;
  let counted = 0;

  for (const file of sampled) {
    try {
      const fullPath = join(repoPath, file);
      const stat = statSync(fullPath);
      if (stat.size > 100_000) continue; // Skip very large files
      const content = readFileSync(fullPath, 'utf-8');
      totalChars += content.length;
      counted++;
    } catch {
      // Skip unreadable files
    }
  }

  if (counted === 0) return 1000;
  const avgChars = totalChars / counted;
  // SOURCE: Code tokenizes at ~3.5 chars/token (community consensus).
  // Anthropic says ~4 chars/token for general text; code is denser due to punctuation/operators.
  return Math.round(avgChars / 3.5);
}

function scanDirectory(dirPath: string, maxFiles = 5000): string[] {
  const files: string[] = [];
  const IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.next', 'vendor']);

  function walk(dir: string, prefix: string) {
    if (files.length >= maxFiles) return;
    try {
      const entries = readdirSync(dir, { withFileTypes: true });
      for (const entry of entries) {
        if (files.length >= maxFiles) return;
        if (entry.name.startsWith('.') || IGNORE.has(entry.name)) continue;
        const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
        if (entry.isDirectory()) {
          walk(join(dir, entry.name), rel);
        } else {
          files.push(rel);
        }
      }
    } catch { /* skip unreadable dirs */ }
  }

  walk(dirPath, '');
  return files;
}
