import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import {
  buildGitDiffRevisionArgs,
  getChangedBranchCoverage,
  getChangedStatementCoverage,
  getIgnoredChangedLinesFromFile,
  normalizeDiffRangeMode,
  parseChangedLineMap,
  resolveGitDiffContext,
} from './check-components-diff-coverage-lib.mjs'
import { COMPONENT_COVERAGE_EXCLUDE_LABEL } from './component-coverage-filters.mjs'
import {
  APP_COMPONENTS_PREFIX,
  createComponentCoverageContext,
  getModuleName,
  isAnyComponentSourceFile,
  isExcludedComponentSourceFile,
  isTrackedComponentSourceFile,
  loadTrackedCoverageEntries,
} from './components-coverage-common.mjs'
import { EXCLUDED_COMPONENT_MODULES } from './components-coverage-thresholds.mjs'

const REQUESTED_DIFF_RANGE_MODE = normalizeDiffRangeMode(process.env.DIFF_RANGE_MODE)
const EXCLUDED_MODULES_LABEL = [...EXCLUDED_COMPONENT_MODULES].sort().join(', ')

const repoRoot = repoRootFromCwd()
const context = createComponentCoverageContext(repoRoot)
const baseSha = process.env.BASE_SHA?.trim()
const headSha = process.env.HEAD_SHA?.trim() || 'HEAD'
const coverageFinalPath = path.join(context.webRoot, 'coverage', 'coverage-final.json')

if (!baseSha || /^0+$/.test(baseSha)) {
  appendSummary([
    '### app/components Pure Diff Coverage',
    '',
    'Skipped pure diff coverage check because `BASE_SHA` was not available.',
  ])
  process.exit(0)
}

if (!fs.existsSync(coverageFinalPath)) {
  console.error(`Coverage report not found at ${coverageFinalPath}`)
  process.exit(1)
}

const diffContext = resolveGitDiffContext({
  base: baseSha,
  head: headSha,
  mode: REQUESTED_DIFF_RANGE_MODE,
  execGit,
})
const coverage = JSON.parse(fs.readFileSync(coverageFinalPath, 'utf8'))
const changedFiles = getChangedFiles(diffContext)
const changedComponentSourceFiles = changedFiles.filter(isAnyComponentSourceFile)
const changedSourceFiles = changedComponentSourceFiles.filter(filePath => isTrackedComponentSourceFile(filePath, context.excludedComponentCoverageFiles))
const changedExcludedSourceFiles = changedComponentSourceFiles.filter(filePath => isExcludedComponentSourceFile(filePath, context.excludedComponentCoverageFiles))

if (changedSourceFiles.length === 0) {
  appendSummary(buildSkipSummary(changedExcludedSourceFiles))
  process.exit(0)
}

const coverageEntries = loadTrackedCoverageEntries(coverage, context)
const diffChanges = getChangedLineMap(diffContext)
const diffRows = []
const ignoredDiffLines = []
const invalidIgnorePragmas = []

for (const [file, changedLines] of diffChanges.entries()) {
  if (!isTrackedComponentSourceFile(file, context.excludedComponentCoverageFiles))
    continue

  const entry = coverageEntries.get(file)
  const ignoreInfo = getIgnoredChangedLinesFromFile(path.join(repoRoot, file), changedLines)

  for (const [line, reason] of ignoreInfo.ignoredLines.entries()) {
    ignoredDiffLines.push({
      file,
      line,
      reason,
    })
  }

  for (const invalidPragma of ignoreInfo.invalidPragmas) {
    invalidIgnorePragmas.push({
      file,
      ...invalidPragma,
    })
  }

  const statements = getChangedStatementCoverage(entry, ignoreInfo.effectiveChangedLines)
  const branches = getChangedBranchCoverage(entry, ignoreInfo.effectiveChangedLines)
  diffRows.push({
    branches,
    file,
    ignoredLineCount: ignoreInfo.ignoredLines.size,
    moduleName: getModuleName(file),
    statements,
  })
}

const diffTotals = diffRows.reduce((acc, row) => {
  acc.statements.total += row.statements.total
  acc.statements.covered += row.statements.covered
  acc.branches.total += row.branches.total
  acc.branches.covered += row.branches.covered
  return acc
}, {
  branches: { total: 0, covered: 0 },
  statements: { total: 0, covered: 0 },
})

const diffStatementFailures = diffRows.filter(row => row.statements.uncoveredLines.length > 0)
const diffBranchFailures = diffRows.filter(row => row.branches.uncoveredBranches.length > 0)

appendSummary(buildSummary({
  changedSourceFiles,
  diffContext,
  diffBranchFailures,
  diffRows,
  diffStatementFailures,
  diffTotals,
  ignoredDiffLines,
  invalidIgnorePragmas,
}))

if (process.env.CI) {
  for (const failure of diffStatementFailures.slice(0, 20)) {
    const firstLine = failure.statements.uncoveredLines[0] ?? 1
    console.log(`::error file=${failure.file},line=${firstLine}::Uncovered changed statements: ${formatLineRanges(failure.statements.uncoveredLines)}`)
  }

  for (const failure of diffBranchFailures.slice(0, 20)) {
    const firstBranch = failure.branches.uncoveredBranches[0]
    const line = firstBranch?.line ?? 1
    console.log(`::error file=${failure.file},line=${line}::Uncovered changed branches: ${formatBranchRefs(failure.branches.uncoveredBranches)}`)
  }

  for (const invalidPragma of invalidIgnorePragmas.slice(0, 20)) {
    console.log(`::error file=${invalidPragma.file},line=${invalidPragma.line}::Invalid diff coverage ignore pragma: ${invalidPragma.reason}`)
  }
}

if (
  diffStatementFailures.length > 0
  || diffBranchFailures.length > 0
  || invalidIgnorePragmas.length > 0
) {
  process.exit(1)
}

function buildSummary({
  changedSourceFiles,
  diffContext,
  diffBranchFailures,
  diffRows,
  diffStatementFailures,
  diffTotals,
  ignoredDiffLines,
  invalidIgnorePragmas,
}) {
  const lines = [
    '### app/components Pure Diff Coverage',
    '',
    ...buildDiffContextSummary(diffContext),
    '',
    `Excluded modules: \`${EXCLUDED_MODULES_LABEL}\``,
    `Excluded file kinds: \`${COMPONENT_COVERAGE_EXCLUDE_LABEL}\``,
    '',
    '| Check | Result | Details |',
    '|---|---:|---|',
    `| Changed statements | ${formatDiffPercent(diffTotals.statements)} | ${diffTotals.statements.covered}/${diffTotals.statements.total} |`,
    `| Changed branches | ${formatDiffPercent(diffTotals.branches)} | ${diffTotals.branches.covered}/${diffTotals.branches.total} |`,
    '',
  ]

  const changedRows = diffRows
    .filter(row => row.statements.total > 0 || row.branches.total > 0)
    .sort((a, b) => {
      const aScore = percentage(a.statements.covered + a.branches.covered, a.statements.total + a.branches.total)
      const bScore = percentage(b.statements.covered + b.branches.covered, b.statements.total + b.branches.total)
      return aScore - bScore || a.file.localeCompare(b.file)
    })

  lines.push('<details><summary>Changed file coverage</summary>')
  lines.push('')
  lines.push('| File | Module | Changed statements | Statement coverage | Uncovered statements | Changed branches | Branch coverage | Uncovered branches | Ignored lines |')
  lines.push('|---|---|---:|---:|---|---:|---:|---|---:|')
  for (const row of changedRows) {
    lines.push(`| ${row.file.replace('web/', '')} | ${row.moduleName} | ${row.statements.total} | ${formatDiffPercent(row.statements)} | ${formatLineRanges(row.statements.uncoveredLines)} | ${row.branches.total} | ${formatDiffPercent(row.branches)} | ${formatBranchRefs(row.branches.uncoveredBranches)} | ${row.ignoredLineCount} |`)
  }
  lines.push('</details>')
  lines.push('')

  if (diffStatementFailures.length > 0) {
    lines.push('Uncovered changed statements:')
    for (const row of diffStatementFailures)
      lines.push(`- ${row.file.replace('web/', '')}: ${formatLineRanges(row.statements.uncoveredLines)}`)
    lines.push('')
  }

  if (diffBranchFailures.length > 0) {
    lines.push('Uncovered changed branches:')
    for (const row of diffBranchFailures)
      lines.push(`- ${row.file.replace('web/', '')}: ${formatBranchRefs(row.branches.uncoveredBranches)}`)
    lines.push('')
  }

  if (ignoredDiffLines.length > 0) {
    lines.push('Ignored changed lines via pragma:')
    for (const ignoredLine of ignoredDiffLines)
      lines.push(`- ${ignoredLine.file.replace('web/', '')}:${ignoredLine.line} - ${ignoredLine.reason}`)
    lines.push('')
  }

  if (invalidIgnorePragmas.length > 0) {
    lines.push('Invalid diff coverage ignore pragmas:')
    for (const invalidPragma of invalidIgnorePragmas)
      lines.push(`- ${invalidPragma.file.replace('web/', '')}:${invalidPragma.line} - ${invalidPragma.reason}`)
    lines.push('')
  }

  lines.push(`Changed source files checked: ${changedSourceFiles.length}`)
  lines.push('Blocking rules: uncovered changed statements, uncovered changed branches, invalid ignore pragmas.')

  return lines
}

function buildSkipSummary(changedExcludedSourceFiles) {
  const lines = [
    '### app/components Pure Diff Coverage',
    '',
    ...buildDiffContextSummary(diffContext),
    '',
    `Excluded modules: \`${EXCLUDED_MODULES_LABEL}\``,
    `Excluded file kinds: \`${COMPONENT_COVERAGE_EXCLUDE_LABEL}\``,
    '',
  ]

  if (changedExcludedSourceFiles.length > 0) {
    lines.push('Only excluded component modules or type-only files changed, so pure diff coverage was skipped.')
    lines.push(`Skipped files: ${changedExcludedSourceFiles.length}`)
  }
  else {
    lines.push('No tracked source changes under `web/app/components/`. Pure diff coverage skipped.')
  }

  return lines
}

function buildDiffContextSummary(diffContext) {
  const lines = [
    `Compared \`${diffContext.base.slice(0, 12)}\` -> \`${diffContext.head.slice(0, 12)}\``,
  ]

  if (diffContext.useCombinedMergeDiff) {
    lines.push(`Requested diff range mode: \`${diffContext.requestedMode}\``)
    lines.push(`Effective diff strategy: \`combined-merge\` (${diffContext.reason})`)
  }
  else if (diffContext.reason) {
    lines.push(`Requested diff range mode: \`${diffContext.requestedMode}\``)
    lines.push(`Effective diff range mode: \`${diffContext.mode}\` (${diffContext.reason})`)
  }
  else {
    lines.push(`Diff range mode: \`${diffContext.mode}\``)
  }

  return lines
}

function getChangedFiles(diffContext) {
  if (diffContext.useCombinedMergeDiff) {
    const output = execGit(['diff-tree', '--cc', '--no-commit-id', '--name-only', '-r', diffContext.head, '--', APP_COMPONENTS_PREFIX])
    return output
      .split('\n')
      .map(line => line.trim())
      .filter(Boolean)
  }

  const output = execGit(['diff', '--name-only', '--diff-filter=ACMR', ...buildGitDiffRevisionArgs(diffContext.base, diffContext.head, diffContext.mode), '--', APP_COMPONENTS_PREFIX])
  return output
    .split('\n')
    .map(line => line.trim())
    .filter(Boolean)
}

function getChangedLineMap(diffContext) {
  if (diffContext.useCombinedMergeDiff) {
    const diff = execGit(['diff-tree', '--cc', '--no-commit-id', '-r', '--unified=0', diffContext.head, '--', APP_COMPONENTS_PREFIX])
    return parseChangedLineMap(diff, filePath => isTrackedComponentSourceFile(filePath, context.excludedComponentCoverageFiles))
  }

  const diff = execGit(['diff', '--unified=0', '--no-color', '--diff-filter=ACMR', ...buildGitDiffRevisionArgs(diffContext.base, diffContext.head, diffContext.mode), '--', APP_COMPONENTS_PREFIX])
  return parseChangedLineMap(diff, filePath => isTrackedComponentSourceFile(filePath, context.excludedComponentCoverageFiles))
}

function formatLineRanges(lines) {
  if (!lines || lines.length === 0)
    return ''

  const ranges = []
  let start = lines[0]
  let end = lines[0]

  for (let index = 1; index < lines.length; index += 1) {
    const current = lines[index]
    if (current === end + 1) {
      end = current
      continue
    }

    ranges.push(start === end ? `${start}` : `${start}-${end}`)
    start = current
    end = current
  }

  ranges.push(start === end ? `${start}` : `${start}-${end}`)
  return ranges.join(', ')
}

function formatBranchRefs(branches) {
  if (!branches || branches.length === 0)
    return ''

  return branches.map(branch => `${branch.line}[${branch.armIndex}]`).join(', ')
}

function percentage(covered, total) {
  if (total === 0)
    return 100
  return (covered / total) * 100
}

function formatDiffPercent(metric) {
  if (metric.total === 0)
    return 'n/a'

  return `${percentage(metric.covered, metric.total).toFixed(2)}%`
}

function appendSummary(lines) {
  const content = `${lines.join('\n')}\n`
  if (process.env.GITHUB_STEP_SUMMARY)
    fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, content)
  console.log(content)
}

function execGit(args) {
  return execFileSync('git', args, {
    cwd: repoRoot,
    encoding: 'utf8',
  })
}

function repoRootFromCwd() {
  return execFileSync('git', ['rev-parse', '--show-toplevel'], {
    cwd: process.cwd(),
    encoding: 'utf8',
  }).trim()
}
