import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import type { Result } from '@harness-engineering/core';
import { Ok } from '@harness-engineering/core';
import { resolveConfig } from '../config/loader';
import { findFiles } from '../utils/files';
import { OutputFormatter, OutputMode, type OutputModeType } from '../output/formatter';
import { logger } from '../output/logger';
import { CLIError, ExitCode } from '../utils/errors';

interface CheckPhaseGateOptions {
  cwd?: string;
  configPath?: string;
  json?: boolean;
  verbose?: boolean;
  quiet?: boolean;
}

export interface CheckPhaseGateResult {
  pass: boolean;
  skipped: boolean;
  severity?: 'error' | 'warning';
  missingSpecs: Array<{ implFile: string; expectedSpec: string }>;
  checkedFiles: number;
}

/**
 * Resolve an implementation file path to an expected spec path.
 *
 * Default mapping: `src/<feature>/file.ts` -> `docs/changes/<feature>/proposal.md`
 * The specPattern supports `{feature}` as a placeholder for the first
 * directory segment under the impl pattern's base.
 */
function resolveSpecPath(
  implFile: string,
  implPattern: string,
  specPattern: string,
  cwd: string
): string {
  // Get relative path from cwd and normalize to forward slashes for consistent matching
  const relImpl = path.relative(cwd, implFile).replace(/\\/g, '/');

  // Determine the base directory of the impl pattern (everything before the first glob)
  const implBase = (implPattern.split('*')[0] ?? '').replace(/\/+$/, '');

  // Extract the portion after the base
  const afterBase = relImpl.startsWith(implBase + '/')
    ? relImpl.slice(implBase.length + 1)
    : relImpl;

  // The "feature" is the first directory segment after the base
  const segments = afterBase.split('/');
  const firstSegment = segments[0] ?? '';
  const feature =
    segments.length > 1 ? firstSegment : path.basename(firstSegment, path.extname(firstSegment));

  // Replace {feature} placeholder in specPattern
  const specRelative = specPattern.replace('{feature}', feature);

  return path.resolve(cwd, specRelative);
}

function checkSpecContent(
  specPath: string,
  relImpl: string,
  relSpec: string,
  missingSpecs: Array<{ implFile: string; expectedSpec: string }>
): void {
  const content = fs.readFileSync(specPath, 'utf-8');
  const sectionPattern = /^#+\s*(Observable Truths|Success Criteria|Acceptance Criteria)/im;
  const sectionMatch = sectionPattern.exec(content);
  if (!sectionMatch) {
    missingSpecs.push({
      implFile: relImpl,
      expectedSpec: `${relSpec} (missing requirements section: expected Observable Truths, Success Criteria, or Acceptance Criteria heading)`,
    });
    return;
  }
  const sectionStart = sectionMatch.index + sectionMatch[0].length;
  const nextHeadingMatch = /^#+\s/m.exec(content.slice(sectionStart));
  const sectionContent = nextHeadingMatch
    ? content.slice(sectionStart, sectionStart + nextHeadingMatch.index)
    : content.slice(sectionStart);
  if (!/^\s*\d+\.\s+/m.test(sectionContent)) {
    missingSpecs.push({
      implFile: relImpl,
      expectedSpec: `${relSpec} (requirements section "${sectionMatch[1]}" has no numbered items)`,
    });
  }
}

export async function runCheckPhaseGate(
  options: CheckPhaseGateOptions
): Promise<Result<CheckPhaseGateResult, CLIError>> {
  // Load config
  const configResult = resolveConfig(options.configPath);
  if (!configResult.ok) {
    return configResult;
  }
  const config = configResult.value;

  const cwd =
    options.cwd ??
    (options.configPath ? path.dirname(path.resolve(options.configPath)) : process.cwd());

  // If phase gates not enabled, skip
  if (!config.phaseGates?.enabled) {
    return Ok({
      pass: true,
      skipped: true,
      missingSpecs: [],
      checkedFiles: 0,
    });
  }

  const phaseGates = config.phaseGates;
  const missingSpecs: Array<{ implFile: string; expectedSpec: string }> = [];
  let checkedFiles = 0;

  // Process each mapping
  for (const mapping of phaseGates.mappings) {
    const implFiles = await findFiles(mapping.implPattern, cwd);

    for (const implFile of implFiles) {
      checkedFiles++;
      const expectedSpec = resolveSpecPath(implFile, mapping.implPattern, mapping.specPattern, cwd);

      const relImpl = path.relative(cwd, implFile).replace(/\\/g, '/');
      const relSpec = path.relative(cwd, expectedSpec).replace(/\\/g, '/');

      if (!fs.existsSync(expectedSpec)) {
        missingSpecs.push({ implFile: relImpl, expectedSpec: relSpec });
      } else if (mapping.contentValidation) {
        checkSpecContent(expectedSpec, relImpl, relSpec, missingSpecs);
      }
    }
  }

  const pass = missingSpecs.length === 0;

  return Ok({
    pass,
    skipped: false,
    severity: phaseGates.severity,
    missingSpecs,
    checkedFiles,
  });
}

function resolvePhaseGateOutputMode(globalOpts: Record<string, unknown>): OutputModeType {
  if (globalOpts.json) return OutputMode.JSON;
  if (globalOpts.quiet) return OutputMode.QUIET;
  if (globalOpts.verbose) return OutputMode.VERBOSE;
  return OutputMode.TEXT;
}

function handlePhaseGateError(error: CLIError, mode: OutputModeType): never {
  if (mode === OutputMode.JSON) {
    console.log(JSON.stringify({ error: error.message }));
  } else {
    logger.error(error.message);
  }
  process.exit(error.exitCode);
}

function handlePhaseGateSkipped(
  value: CheckPhaseGateResult,
  mode: OutputModeType,
  formatter: OutputFormatter
): never {
  if (mode === OutputMode.JSON) {
    console.log(formatter.format(value));
  } else if (mode !== OutputMode.QUIET) {
    logger.dim('Phase gates not enabled, skipping.');
  }
  process.exit(ExitCode.SUCCESS);
}

function printPhaseGateResult(value: CheckPhaseGateResult, formatter: OutputFormatter): void {
  const output = formatter.formatValidation({
    valid: value.pass,
    issues: value.missingSpecs.map((m) => ({
      file: m.implFile,
      message: `Missing spec: ${m.expectedSpec}`,
    })),
  });
  if (output) console.log(output);

  const summary = formatter.formatSummary(
    'Phase gate check',
    `${value.checkedFiles} files checked, ${value.missingSpecs.length} missing specs`,
    value.pass
  );
  if (summary) console.log(summary);
}

export function createCheckPhaseGateCommand(): Command {
  const command = new Command('check-phase-gate')
    .description('Verify that implementation files have matching spec documents')
    .action(async (_opts, cmd) => {
      const globalOpts = cmd.optsWithGlobals();
      const mode = resolvePhaseGateOutputMode(globalOpts);
      const formatter = new OutputFormatter(mode);

      const result = await runCheckPhaseGate({
        configPath: globalOpts.config,
        json: globalOpts.json,
        verbose: globalOpts.verbose,
        quiet: globalOpts.quiet,
      });

      if (!result.ok) handlePhaseGateError(result.error, mode);

      const value = result.value;
      if (value.skipped) handlePhaseGateSkipped(value, mode, formatter);

      printPhaseGateResult(value, formatter);

      // Severity determines exit code: warning always passes, error fails
      if (!value.pass && value.severity === 'error') {
        process.exit(ExitCode.VALIDATION_FAILED);
      }
      process.exit(ExitCode.SUCCESS);
    });

  return command;
}
