/**
 * Hotspot Detector — identifies frequently-changed and highly-imported files.
 *
 * The combined hotspot score blends three signals:
 *  - churn: git change frequency (how often the file is edited)
 *  - fan-in: number of importers (how much depends on it)
 *  - coverage gap: whether a corresponding test file exists
 *
 * Files at the top of the combined rank are the risky ones: touched often,
 * depended on by many, and not defended by a test.
 */

import { exec } from 'child_process'
import { promisify } from 'util'
import { getCwd } from '../../utils/cwd.js'
import { logForDebugging } from '../../utils/debug.js'
import type { ArchitectureGraph, CombinedHotspot, HotspotMap } from './types.js'

const execAsync = promisify(exec)

// Multiplier applied to untested files so they surface over tested files
// with the same churn and fan-in. Tuned so a file with zero importers but
// high churn still ranks relative to one with high importers.
const UNTESTED_MULTIPLIER = 2

/**
 * Build a hotspot map from git history and architecture graph.
 * Accepts an optional architecture graph — without one, only change
 * frequency data is produced (git log alone).
 */
export async function detectHotspots(architecture: ArchitectureGraph | null): Promise<HotspotMap> {
  const cwd = getCwd()

  const byChangeFrequency = await getChangeFrequency(cwd)
  const byImportFanIn = computeImportFanInFromGraph(architecture)
  const testedFiles = computeTestMappingsFromGraph(architecture)
  const combined = computeCombinedHotspots(architecture, byChangeFrequency)

  logForDebugging(
    `Hotspots: ${byChangeFrequency.length} by churn, ` +
      `${byImportFanIn.length} by fan-in, ` +
      `${testedFiles.length} tested files, ` +
      `${combined.length} combined`,
  )

  return {
    version: 1,
    byChangeFrequency,
    byImportFanIn,
    testedFiles,
    combined,
    computedAt: Date.now(),
  }
}

async function getChangeFrequency(cwd: string): Promise<Array<{ path: string; changes: number }>> {
  try {
    const { stdout } = await execAsync(
      'git log --pretty=format: --name-only --diff-filter=ACMR -n 200 | sort | uniq -c | sort -rn | head -30',
      { cwd, timeout: 5000 },
    )

    return stdout
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.length > 0)
      .map((line) => {
        const match = line.match(/^\s*(\d+)\s+(.+)$/)
        if (!match) return null
        return { path: match[2]!, changes: parseInt(match[1]!, 10) }
      })
      .filter((entry): entry is { path: string; changes: number } => entry !== null)
  } catch {
    return []
  }
}

/**
 * Count importers per file from the dependency graph. Exported for testing.
 */
export function computeImportFanInFromGraph(
  architecture: ArchitectureGraph | null,
): Array<{ path: string; importers: number }> {
  if (!architecture) return []

  const fanIn: Record<string, number> = {}
  for (const deps of Object.values(architecture.dependencies)) {
    const unique = new Set(deps)
    for (const dep of unique) {
      fanIn[dep] = (fanIn[dep] ?? 0) + 1
    }
  }

  return Object.entries(fanIn)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 20)
    .map(([path, importers]) => ({ path, importers }))
}

/**
 * Pair source files with their test-file siblings. A file counts as
 * tested if a `.test.ts`/`.spec.ts` sibling or a matching `__tests__/`
 * entry exists in the graph. Exported for testing.
 */
export function computeTestMappingsFromGraph(
  architecture: ArchitectureGraph | null,
): Array<{ source: string; test: string }> {
  if (!architecture) return []

  const allFiles = new Set(Object.keys(architecture.dependencies))
  const mappings: Array<{ source: string; test: string }> = []

  for (const filePath of allFiles) {
    if (isTestFile(filePath)) continue

    const testCandidates = generateTestCandidates(filePath)
    for (const candidate of testCandidates) {
      if (allFiles.has(candidate)) {
        mappings.push({ source: filePath, test: candidate })
        break
      }
    }
  }

  return mappings
}

/**
 * Rank files by combined churn × fan-in × coverage-gap.
 * Score = changes × (1 + importers) × (hasTest ? 1 : UNTESTED_MULTIPLIER)
 * Only files with recorded change frequency are scored.
 * Exported for testing.
 */
export function computeCombinedHotspots(
  architecture: ArchitectureGraph | null,
  changeFrequency: Array<{ path: string; changes: number }>,
): CombinedHotspot[] {
  if (!architecture || changeFrequency.length === 0) return []

  const fanInMap = new Map<string, number>()
  for (const { path, importers } of computeImportFanInFromGraph(architecture)) {
    fanInMap.set(path, importers)
  }

  const testedSet = new Set(computeTestMappingsFromGraph(architecture).map((m) => m.source))

  const out: CombinedHotspot[] = changeFrequency.map(({ path, changes }) => {
    const importers = fanInMap.get(path) ?? 0
    const hasTest = testedSet.has(path)
    const coverageMultiplier = hasTest ? 1 : UNTESTED_MULTIPLIER
    const score = changes * (1 + importers) * coverageMultiplier
    return { path, score, changes, importers, hasTest }
  })

  return out.sort((a, b) => b.score - a.score).slice(0, 15)
}

function isTestFile(path: string): boolean {
  return (
    /\.(test|spec|e2e)\.(ts|tsx|js|jsx)$/.test(path) ||
    path.includes('__tests__/') ||
    // Python: test_*.py or *_test.py
    /(^|\/)test_.*\.py$/.test(path) ||
    /(^|\/).*_test\.py$/.test(path) ||
    path.includes('/tests/') ||
    path.includes('/test/')
  )
}

function generateTestCandidates(filePath: string): string[] {
  const ext = filePath.match(/\.(\w+)$/)?.[0] ?? '.ts'

  // Python test candidates
  if (ext === '.py') {
    const dir = filePath.match(/^(.+)\/([^/]+)\.py$/)
    const baseName = filePath.match(/\/?([^/]+)\.py$/)?.[1]
    if (!baseName) return []
    const candidates = [
      filePath.replace(/\.py$/, '_test.py'),
      filePath.replace(/\.py$/, '').replace(/\/([^/]+)$/, '/test_$1') + '.py',
    ]
    if (dir) {
      candidates.push(`${dir[1]}/../tests/test_${baseName}.py`)
      candidates.push(`${dir[1]}/../test/test_${baseName}.py`)
    }
    return candidates
  }

  // JS/TS test candidates
  const base = filePath.replace(/\.(ts|tsx|js|jsx)$/, '')
  const candidates = [`${base}.test${ext}`, `${base}.spec${ext}`]
  const dirMatch = filePath.match(/^(.+)\/([^/]+)$/)
  if (dirMatch) {
    candidates.push(`${dirMatch[1]}/__tests__/${dirMatch[2]}`)
  }
  return candidates
}
