import { exec } from 'child_process';
import { promisify } from 'util';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { assertPathInAllowedRoots, InvalidShellArgError, runNpmScript, type NpmLifecycle } from '../../lib/shell.js';
import type { CodeQualityReport, CodeQualityCheckParams, CodeQualityIssue } from './types.js';

const execAsync = promisify(exec);

// Allow-listed roots inside which `/code-quality/check` may run
// `npm run typecheck/lint/build/test:run`. Anything outside these roots is
// rejected before a subprocess is spawned. Configure via
// `CODE_QUALITY_ALLOWED_ROOTS` (colon-separated) for non-default deployments.
const DEFAULT_ALLOWED_ROOTS = ['/opt/bytelyst'];
function getAllowedRoots(): string[] {
  const raw = process.env.CODE_QUALITY_ALLOWED_ROOTS?.trim();
  if (!raw) return DEFAULT_ALLOWED_ROOTS;
  return raw.split(':').map((s) => s.trim()).filter(Boolean);
}

// Run an `npm run <script>` invocation through the shell allow-list and
// always resolve, even on non-zero exit (the parsers downstream want to
// inspect stdout+stderr regardless of exit code).
async function runScriptCapturingOutput(
  script: NpmLifecycle,
  cwd: string,
  timeoutMs: number,
): Promise<{ output: string; ok: boolean }> {
  try {
    const { stdout, stderr } = await runNpmScript(script, {
      allowedRoots: getAllowedRoots(),
      cwd,
      timeoutMs,
    });
    return { output: `${stdout}${stderr}`, ok: true };
  } catch (error) {
    if (error instanceof InvalidShellArgError) throw error;
    const e = error as { stdout?: string; stderr?: string; message?: string };
    return { output: `${e.stdout ?? ''}${e.stderr ?? ''}` || (e.message ?? ''), ok: false };
  }
}

export async function runCodeQualityCheck(params: CodeQualityCheckParams): Promise<CodeQualityReport> {
  const { projectId, projectPath, checks } = params;

  // Reject paths outside the allow-list before spawning anything.
  // `assertPathInAllowedRoots` returns the resolved absolute form so we
  // pass that into the npm wrapper rather than the raw input.
  let resolvedPath: string;
  try {
    resolvedPath = assertPathInAllowedRoots(projectPath, getAllowedRoots());
  } catch (error) {
    if (error instanceof InvalidShellArgError) {
      throw new Error(
        `projectPath is not inside an allowed root (${getAllowedRoots().join(', ')}); refusing to run lifecycle scripts there.`,
      );
    }
    throw error;
  }
  const issues: CodeQualityIssue[] = [];
  const summary = {
    totalIssues: 0,
    errors: 0,
    warnings: 0,
    infos: 0,
  };
  const categories: any = {
    typescript: { errors: 0, warnings: 0, duration: 0 },
    eslint: { errors: 0, warnings: 0, duration: 0 },
    build: { success: true, duration: 0, errors: 0 },
    test: { success: true, passed: 0, failed: 0, duration: 0 },
  };

  const startTime = Date.now();

  // TypeScript check
  if (checks.includes('typescript')) {
    const tsStart = Date.now();
    const { output } = await runScriptCapturingOutput('typecheck', resolvedPath, 60000);
    const tsIssues = parseTypeScriptOutput(output, resolvedPath);
    issues.push(...tsIssues);
    categories.typescript.duration = Date.now() - tsStart;
    categories.typescript.errors = tsIssues.filter(i => i.type === 'error').length;
    categories.typescript.warnings = tsIssues.filter(i => i.type === 'warning').length;
  }

  // ESLint check
  if (checks.includes('eslint')) {
    const eslintStart = Date.now();
    const { output } = await runScriptCapturingOutput('lint', resolvedPath, 60000);
    const eslintIssues = parseEslintOutput(output, resolvedPath);
    issues.push(...eslintIssues);
    categories.eslint.duration = Date.now() - eslintStart;
    categories.eslint.errors = eslintIssues.filter(i => i.type === 'error').length;
    categories.eslint.warnings = eslintIssues.filter(i => i.type === 'warning').length;
  }

  // Build check
  if (checks.includes('build')) {
    const buildStart = Date.now();
    const { output, ok } = await runScriptCapturingOutput('build', resolvedPath, 120000);
    categories.build.duration = Date.now() - buildStart;
    if (ok) {
      categories.build.success = true;
    } else {
      categories.build.success = false;
      const buildIssues = parseBuildOutput(output, resolvedPath);
      issues.push(...buildIssues);
      categories.build.errors = buildIssues.length;
    }
  }

  // Test check
  if (checks.includes('test')) {
    const testStart = Date.now();
    const { output, ok } = await runScriptCapturingOutput('test:run', resolvedPath, 120000);
    const testResults = parseTestOutput(output);
    categories.test.duration = Date.now() - testStart;
    categories.test.passed = testResults.passed;
    categories.test.failed = testResults.failed;
    if (ok) {
      categories.test.success = testResults.failed === 0;
    } else {
      categories.test.success = false;
      const testIssues = parseTestOutputErrors(output, resolvedPath);
      issues.push(...testIssues);
    }
  }

  // Calculate summary
  summary.totalIssues = issues.length;
  summary.errors = issues.filter(i => i.type === 'error').length;
  summary.warnings = issues.filter(i => i.type === 'warning').length;
  summary.infos = issues.filter(i => i.type === 'info').length;

  const projectName = resolvedPath.split('/').pop() || resolvedPath;

  return {
    id: `cq-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
    projectId,
    projectName,
    projectPath: resolvedPath,
    timestamp: new Date().toISOString(),
    summary,
    categories,
    issues,
  };
}

function parseTypeScriptOutput(output: string, projectPath: string): CodeQualityIssue[] {
  const issues: CodeQualityIssue[] = [];
  const lines = output.split('\n');

  for (const line of lines) {
    // TS error format: file.ts(line,col): error TScode: message
    const tsErrorMatch = line.match(/(.+\.ts)\((\d+),(\d+)\):\s(error|warning)\sTS(\d+):\s(.+)/);
    if (tsErrorMatch) {
      issues.push({
        id: `ts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
        type: tsErrorMatch[4] as 'error' | 'warning',  // group 4 = type; group 3 = column
        category: 'typescript',
        file: tsErrorMatch[1],
        line: parseInt(tsErrorMatch[2]),
        column: parseInt(tsErrorMatch[3]),
        message: tsErrorMatch[6],
        rule: `TS${tsErrorMatch[5]}`,
      });
    }
  }

  return issues;
}

function parseEslintOutput(output: string, projectPath: string): CodeQualityIssue[] {
  const issues: CodeQualityIssue[] = [];
  const lines = output.split('\n');

  for (const line of lines) {
    // ESLint unix format: file:line:col: message [rule]
    // Rule part in brackets may or may not be present depending on formatter
    const eslintMatch = line.match(/(.+\.tsx?):(\d+):(\d+)[:\s]+(.+?)(?:\s+\[([^\]]+)\])?$/);
    if (eslintMatch) {
      const msgAndLevel = eslintMatch[4];
      const severity = /\berror\b/i.test(msgAndLevel) ? 'error' : 'warning';
      issues.push({
        id: `eslint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
        type: severity,
        category: 'eslint',
        file: eslintMatch[1],
        line: parseInt(eslintMatch[2]),
        column: parseInt(eslintMatch[3]),
        message: msgAndLevel,
        rule: eslintMatch[5] ?? 'unknown',
      });
    }
  }

  return issues;
}

function parseBuildOutput(output: string, projectPath: string): CodeQualityIssue[] {
  const issues: CodeQualityIssue[] = [];
  const lines = output.split('\n');

  for (const line of lines) {
    if (line.includes('error') || line.includes('Error')) {
      issues.push({
        id: `build-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
        type: 'error',
        category: 'build',
        file: 'build',
        message: line.trim(),
      });
    }
  }

  return issues;
}

function parseTestOutput(output: string): { passed: number; failed: number } {
  let passed = 0;
  let failed = 0;

  // Try to parse Vitest output — use "Tests" line (individual tests), not "Test Files" line
  // Format: " Tests  3 failed | 5 passed (8)"  or  " Tests  8 passed (8)"
  const vitestFailMatch = output.match(/\bTests\b\s+(\d+)\s+failed[^|]*\|\s*(\d+)\s+passed/);
  const vitestPassMatch = output.match(/\bTests\b\s+(\d+)\s+passed/);
  if (vitestFailMatch) {
    failed = parseInt(vitestFailMatch[1]);
    passed = parseInt(vitestFailMatch[2]);
  } else if (vitestPassMatch) {
    passed = parseInt(vitestPassMatch[1]);
    failed = 0;
  }

  // Try to parse Jest output: "Tests: 5 passed, 2 failed" or "Tests: 2 failed, 5 passed"
  const jestPassMatch = output.match(/Tests:.*?(\d+)\s+passed/);
  const jestFailMatch = output.match(/Tests:.*?(\d+)\s+failed/);
  if (jestPassMatch || jestFailMatch) {
    passed = jestPassMatch ? parseInt(jestPassMatch[1]) : 0;
    failed = jestFailMatch ? parseInt(jestFailMatch[1]) : 0;
  }

  return { passed, failed };
}

function parseTestOutputErrors(output: string, projectPath: string): CodeQualityIssue[] {
  const issues: CodeQualityIssue[] = [];
  const lines = output.split('\n');

  for (const line of lines) {
    if (line.includes('FAIL') || line.includes('failed')) {
      issues.push({
        id: `test-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
        type: 'error',
        category: 'test',
        file: 'test',
        message: line.trim(),
      });
    }
  }

  return issues;
}
