import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { promisify } from 'node:util'
import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate'

const execFileAsync = promisify(execFile)
const DIAGNOSTIC_PATTERN = /^(.+?\.(?:ts|tsx))\((\d+),(\d+)\): error TS(\d+): (.+)$/
const DEFAULT_BATCH_SIZE = 100
const DEFAULT_BATCH_ITERATIONS = 5
const DEFAULT_MAX_ROUNDS = 20
const TYPECHECK_CACHE_DIR = path.join(os.tmpdir(), 'migrate-no-unchecked-indexed-access')

type CliOptions = {
  batchIterations: number
  batchSize: number
  maxRounds: number
  project: string
  verbose: boolean
}

type DiagnosticEntry = {
  code: number
  fileName: string
  line: number
  message: string
}

function parseArgs(argv: string[]): CliOptions {
  const options: CliOptions = {
    batchIterations: DEFAULT_BATCH_ITERATIONS,
    batchSize: DEFAULT_BATCH_SIZE,
    maxRounds: DEFAULT_MAX_ROUNDS,
    project: 'tsconfig.json',
    verbose: false,
  }

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i]

    if (arg === '--') continue

    if (arg === '--verbose') {
      options.verbose = true
      continue
    }

    if (arg === '--project') {
      const value = argv[i + 1]
      if (!value) throw new Error('Missing value for --project')

      options.project = value
      i += 1
      continue
    }

    if (arg === '--batch-size') {
      const value = Number(argv[i + 1])
      if (!Number.isInteger(value) || value <= 0) throw new Error('Invalid value for --batch-size')

      options.batchSize = value
      i += 1
      continue
    }

    if (arg === '--batch-iterations') {
      const value = Number(argv[i + 1])
      if (!Number.isInteger(value) || value <= 0)
        throw new Error('Invalid value for --batch-iterations')

      options.batchIterations = value
      i += 1
      continue
    }

    if (arg === '--max-rounds') {
      const value = Number(argv[i + 1])
      if (!Number.isInteger(value) || value <= 0) throw new Error('Invalid value for --max-rounds')

      options.maxRounds = value
      i += 1
      continue
    }

    throw new Error(`Unknown option: ${arg}`)
  }

  return options
}

function getTypeCheckBuildInfoPath(projectPath: string): string {
  const hash = createHash('sha1').update(projectPath).digest('hex').slice(0, 16)

  return path.join(TYPECHECK_CACHE_DIR, `${hash}.tsbuildinfo`)
}

async function runTypeCheck(
  project: string,
  options?: {
    incremental?: boolean
  },
): Promise<{ diagnostics: DiagnosticEntry[]; exitCode: number; rawOutput: string }> {
  const projectPath = path.resolve(process.cwd(), project)
  const projectDirectory = path.dirname(projectPath)
  const buildInfoPath = getTypeCheckBuildInfoPath(projectPath)
  const incremental = options?.incremental ?? true

  await fs.mkdir(TYPECHECK_CACHE_DIR, { recursive: true })

  const tsgoArgs = ['exec', 'tsgo', '--noEmit', '--pretty', 'false']
  if (incremental) {
    tsgoArgs.push('--incremental', '--tsBuildInfoFile', buildInfoPath)
  } else {
    tsgoArgs.push('--incremental', 'false')
  }
  tsgoArgs.push('--project', projectPath)

  try {
    const { stdout, stderr } = await execFileAsync('pnpm', tsgoArgs, {
      cwd: projectDirectory,
      env: {
        ...process.env,
        NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192',
      },
      maxBuffer: 1024 * 1024 * 32,
    })

    const rawOutput = `${stdout}${stderr}`.trim()
    return {
      diagnostics: parseDiagnostics(rawOutput, projectDirectory),
      exitCode: 0,
      rawOutput,
    }
  } catch (error) {
    const exitCode =
      typeof error === 'object' && error && 'code' in error && typeof error.code === 'number'
        ? error.code
        : 1
    const stdout =
      typeof error === 'object' && error && 'stdout' in error && typeof error.stdout === 'string'
        ? error.stdout
        : ''
    const stderr =
      typeof error === 'object' && error && 'stderr' in error && typeof error.stderr === 'string'
        ? error.stderr
        : ''
    const rawOutput = `${stdout}${stderr}`.trim()

    return {
      diagnostics: parseDiagnostics(rawOutput, projectDirectory),
      exitCode,
      rawOutput,
    }
  }
}

function parseDiagnostics(rawOutput: string, projectDirectory: string): DiagnosticEntry[] {
  return rawOutput
    .split('\n')
    .map((line) => line.trim())
    .flatMap((line) => {
      const match = line.match(DIAGNOSTIC_PATTERN)
      if (!match) return []

      return [
        {
          code: Number(match[4]),
          fileName: path.resolve(projectDirectory, match[1]!),
          line: Number(match[2]),
          message: match[5] ?? '',
        },
      ]
    })
}

function summarizeCodes(diagnostics: DiagnosticEntry[]): string {
  const counts = new Map<number, number>()
  for (const diagnostic of diagnostics)
    counts.set(diagnostic.code, (counts.get(diagnostic.code) ?? 0) + 1)

  return Array.from(counts.entries())
    .sort((left, right) => right[1] - left[1])
    .slice(0, 8)
    .map(([code, count]) => `TS${code}:${count}`)
    .join(', ')
}

function chunk<T>(items: T[], size: number): T[][] {
  const batches: T[][] = []
  for (let i = 0; i < items.length; i += size) batches.push(items.slice(i, i + size))

  return batches
}

async function runBatchMigration(options: CliOptions) {
  for (let round = 1; round <= options.maxRounds; round += 1) {
    const { diagnostics, exitCode, rawOutput } = await runTypeCheck(options.project)
    if (exitCode === 0) {
      const finalCheck = await runTypeCheck(options.project, { incremental: false })
      if (finalCheck.exitCode !== 0) {
        const finalDiagnostics = finalCheck.diagnostics
        console.log(
          `Final cold type check found ${finalDiagnostics.length} diagnostic(s). ${summarizeCodes(finalDiagnostics)}`,
        )

        if (options.verbose) {
          for (const diagnostic of finalDiagnostics.slice(0, 40))
            console.log(
              `${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`,
            )
        }

        const finalSupportedFiles = Array.from(
          new Set(
            finalDiagnostics
              .filter((diagnostic) => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code))
              .map((diagnostic) => diagnostic.fileName),
          ),
        )

        if (finalSupportedFiles.length > 0) {
          console.log(`  Final pass batch: ${finalSupportedFiles.length} file(s)`)
          let finalResult = await runMigration({
            files: finalSupportedFiles,
            maxIterations: options.batchIterations,
            project: options.project,
            verbose: options.verbose,
            write: true,
          })

          if (finalResult.totalEdits === 0) {
            console.log('    No edits produced; retrying final pass with full project roots.')
            finalResult = await runMigration({
              files: finalSupportedFiles,
              maxIterations: options.batchIterations,
              project: options.project,
              useFullProjectRoots: true,
              verbose: options.verbose,
              write: true,
            })
          }

          if (finalResult.totalEdits > 0) continue
        }

        if (finalCheck.rawOutput) process.stderr.write(`${finalCheck.rawOutput}\n`)
        process.exitCode = 1
        return
      }

      console.log(`Type check passed after ${round - 1} migration round(s).`)
      return
    }

    const supportedDiagnostics = diagnostics.filter((diagnostic) =>
      SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code),
    )
    const unsupportedDiagnostics = diagnostics.filter(
      (diagnostic) => !SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code),
    )
    const supportedFiles = Array.from(
      new Set(supportedDiagnostics.map((diagnostic) => diagnostic.fileName)),
    )

    console.log(
      `Round ${round}: ${diagnostics.length} diagnostic(s). ${summarizeCodes(diagnostics)}`,
    )

    if (options.verbose) {
      for (const diagnostic of diagnostics.slice(0, 40))
        console.log(
          `${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`,
        )
    }

    if (supportedFiles.length === 0) {
      console.error('No supported diagnostics remain to migrate.')
      if (unsupportedDiagnostics.length > 0) {
        console.error('Remaining unsupported diagnostics:')
        for (const diagnostic of unsupportedDiagnostics.slice(0, 40))
          console.error(
            `${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`,
          )
      }
      if (rawOutput) process.stderr.write(`${rawOutput}\n`)
      process.exitCode = 1
      return
    }

    let roundEdits = 0
    const batches = chunk(supportedFiles, options.batchSize)

    for (const [index, batch] of batches.entries()) {
      console.log(`  Batch ${index + 1}/${batches.length}: ${batch.length} file(s)`)
      let result = await runMigration({
        files: batch,
        maxIterations: options.batchIterations,
        project: options.project,
        verbose: options.verbose,
        write: true,
      })

      if (result.totalEdits === 0) {
        console.log('    No edits produced; retrying batch with full project roots.')
        result = await runMigration({
          files: batch,
          maxIterations: options.batchIterations,
          project: options.project,
          useFullProjectRoots: true,
          verbose: options.verbose,
          write: true,
        })
      }

      roundEdits += result.totalEdits
    }

    if (roundEdits === 0) {
      console.error(
        'Migration script made no edits in this round; stopping to avoid an infinite loop.',
      )
      process.exitCode = 1
      return
    }
  }

  console.error(`Reached --max-rounds=${options.maxRounds} before type check passed.`)
  process.exitCode = 1
}

export async function runBatchMigrationCommand(argv: string[]) {
  await runBatchMigration(parseArgs(argv))
}
