/**
 * GitFlow EF Core — Migration detection and validation.
 * Self-contained: only uses node built-ins.
 */

import { readFileSync, existsSync } from 'fs';
import type { EfCoreValidation, MigrationInfo } from './types.js';

const MIGRATION_PATTERN = /Migrations?\//i;
const DESIGNER_SUFFIX = '.Designer.cs';
const SNAPSHOT_SUFFIX = 'ModelSnapshot.cs';

// GitFlow 4.x list — ONLY genuinely data-destroying operations trigger a
// confirmation. DropIndex / DropForeignKey / DropPrimaryKey / DropSchema /
// RenameTable / RenameColumn are ROUTINE in legitimate migrations (a rename, a
// type change, an FK restructuring all emit them) and were never flagged in
// 4.x — they stay OUT to avoid the false-positive storm the v5 9-op list caused.
const DESTRUCTIVE_OPS = ['DropTable', 'DropColumn', 'DeleteData'];

export function detectMigrations(changedFiles: string[]): MigrationInfo[] {
  const migrationFiles = changedFiles.filter(f => MIGRATION_PATTERN.test(f) && f.endsWith('.cs'));
  const migrations = new Map<string, MigrationInfo>();

  for (const file of migrationFiles) {
    if (file.endsWith(SNAPSHOT_SUFFIX)) continue;

    let name: string;
    if (file.endsWith(DESIGNER_SUFFIX)) {
      name = file.replace(DESIGNER_SUFFIX, '');
    } else {
      name = file.replace('.cs', '');
    }

    const baseName = name.split(/[/\\]/).pop() || name;

    if (!migrations.has(baseName)) {
      migrations.set(baseName, { name: baseName, mainFile: '' });
    }

    const info = migrations.get(baseName)!;
    if (file.endsWith(DESIGNER_SUFFIX)) {
      info.designerFile = file;
    } else {
      info.mainFile = file;
    }
  }

  const snapshotFiles = migrationFiles.filter(f => f.endsWith(SNAPSHOT_SUFFIX));
  for (const migration of migrations.values()) {
    migration.snapshotFile = snapshotFiles[0];
  }

  return Array.from(migrations.values());
}

export function validateMigration(
  migration: MigrationInfo,
  allChangedFiles: string[],
): { valid: boolean; missing: string[] } {
  const missing: string[] = [];
  if (!migration.mainFile) missing.push(`${migration.name}.cs`);
  if (!migration.designerFile) missing.push(`${migration.name}.Designer.cs`);

  const hasSnapshot = allChangedFiles.some(f => f.endsWith(SNAPSHOT_SUFFIX));
  if (!hasSnapshot) missing.push('ModelSnapshot.cs');

  return { valid: missing.length === 0, missing };
}

/**
 * Isolate the FORWARD (`Up`) region of a migration — from the
 * `Up(MigrationBuilder …)` signature up to (but excluding) the `Down(…)`
 * rollback. EF Core scaffolds `Down()` as the exact inverse of `Up()`, so a
 * migration that CREATEs N tables forward mechanically emits N `DropTable`
 * calls in `Down()`. Those drops run ONLY on an explicit rollback, never on a
 * normal `database update` — scanning them as "destructive" is a guaranteed
 * false positive (EVERY InitialCreate would trip it). Fail-open: with no `Up`
 * signature we scan from the top; with no `Down` we scan to EOF.
 */
export function forwardMigrationBody(content: string): string {
  const up = /protected\s+override\s+void\s+Up\s*\(/.exec(content);
  const down = /protected\s+override\s+void\s+Down\s*\(/.exec(content);
  const start = up ? up.index : 0;
  const end = down && down.index > start ? down.index : content.length;
  return content.slice(start, end);
}

/**
 * Destructive ops present in the FORWARD (`Up`) body only. Matches the actual
 * builder CALL (`.DropTable(`), not a bare substring — so an inlined frozen SQL
 * object (`migrationBuilder.Sql(@"… DropTable …")`), a comment, or a SQL name
 * like `usp_DeleteDataArchive` can't trip it. A genuine forward drop (the
 * developer really removes a table/column/data in `Up()`) is still caught; the
 * auto-generated `Down()` inverse is excluded. Pure: no I/O.
 */
export function scanDestructiveOps(content: string): string[] {
  const forward = forwardMigrationBody(content);
  return DESTRUCTIVE_OPS.filter(op => new RegExp(`\\.${op}\\s*\\(`).test(forward));
}

export function detectDestructiveOps(filePath: string): string[] {
  if (!existsSync(filePath)) return [];
  return scanDestructiveOps(readFileSync(filePath, 'utf-8'));
}

export function validateEfCore(changedFiles: string[]): EfCoreValidation {
  const migrations = detectMigrations(changedFiles);

  if (migrations.length === 0) {
    return { detected: false, valid: true, destructive: false, migrations: [], missing: [], destructiveOps: [] };
  }

  const allMissing: string[] = [];
  const allDestructive: string[] = [];
  let allValid = true;

  for (const migration of migrations) {
    const { valid, missing } = validateMigration(migration, changedFiles);
    if (!valid) {
      allValid = false;
      allMissing.push(...missing);
    }

    if (migration.mainFile && existsSync(migration.mainFile)) {
      const ops = detectDestructiveOps(migration.mainFile);
      allDestructive.push(...ops);
    }
  }

  return {
    detected: true,
    valid: allValid,
    destructive: allDestructive.length > 0,
    migrations: migrations.map(m => m.name),
    missing: allMissing,
    destructiveOps: allDestructive,
  };
}

export interface EfcorePolicyConfig {
  enabled?: boolean;
  validateOnCommit?: boolean;
  blockDestructive?: boolean;
}

export interface EfcorePolicy {
  /** Hard block — commit is refused outright (incomplete migration). */
  block: boolean;
  /** Needs explicit user confirmation before proceeding (destructive ops, 4.x AskUserQuestion). */
  requiresConfirmation: boolean;
  error?: string;
  warnings: string[];
}

/**
 * Decide what a commit should do about detected migrations — config-driven,
 * restoring the 4.x semantics:
 *  - incomplete migration changeset → hard block IF validateOnCommit (4.x STOP),
 *    else a warning;
 *  - destructive ops → require confirmation IF blockDestructive (4.x
 *    AskUserQuestion), else a warning;
 *  - `enabled === false` disables all blocking/confirmation (warn-only).
 * Pure: no I/O, fully unit-testable.
 */
export function decideEfcorePolicy(v: EfCoreValidation, cfg: EfcorePolicyConfig): EfcorePolicy {
  const warnings: string[] = [];
  if (!v.detected) {
    return { block: false, requiresConfirmation: false, warnings };
  }

  const enabled = cfg.enabled !== false; // 4.x default: on; only explicit false disables

  if (!v.valid) {
    if (enabled && cfg.validateOnCommit) {
      return {
        block: true,
        requiresConfirmation: false,
        error: `Incomplete migration changeset — missing: ${v.missing.join(', ')}. Required: Migration.cs + Designer.cs + ModelSnapshot.cs.`,
        warnings,
      };
    }
    warnings.push(`EF Core: incomplete migration changeset (missing ${v.missing.join(', ')}).`);
  }

  if (v.destructive) {
    if (enabled && cfg.blockDestructive) {
      // Surface for confirmation (4.x AskUserQuestion) — NOT a silent hard block.
      return {
        block: false,
        requiresConfirmation: true,
        error: `Destructive EF Core operations detected: ${v.destructiveOps.join(', ')}.`,
        warnings,
      };
    }
    warnings.push(`EF Core: destructive operations (${v.destructiveOps.join(', ')}) — review before merge.`);
  }

  return { block: false, requiresConfirmation: false, warnings };
}

/**
 * 4.x start-efcore-preflight rule: a migration filename must follow
 * `{context}_v{version}_{seq3}_{Description}` (an optional EF 14-digit timestamp
 * prefix is allowed). Accepts both dot (`v2.0.0`) and underscore (`v2_0_0`)
 * version separators. Pure — feed it a basename or full path.
 */
export function isCompliantMigrationName(fileName: string): boolean {
  const base = (fileName.split(/[/\\]/).pop() || fileName).replace(/\.cs$/i, '');
  const namePart = base.replace(/^[0-9]{14}_/, '');
  return /^[a-zA-Z]+_v[0-9]+[._][0-9]+[._][0-9]+_[0-9]{3}_[A-Za-z]/.test(namePart);
}

export function validateAppsettings(
  currentContent: string,
  previousContent?: string,
): { valid: boolean; removedSections: string[]; secrets: string[] } {
  const secrets: string[] = [];
  const removedSections: string[] = [];

  const secretPatterns = [
    { pattern: /Server=.*;.*Password=/i, label: 'Connection string with password' },
    { pattern: /["']sk-[a-zA-Z0-9]{20,}["']/i, label: 'API key' },
    { pattern: /["'][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}["']/i, label: 'GUID (potential secret)' },
  ];

  for (const { pattern, label } of secretPatterns) {
    if (pattern.test(currentContent)) secrets.push(label);
  }

  if (previousContent) {
    try {
      const current = JSON.parse(currentContent);
      const previous = JSON.parse(previousContent);
      for (const key of Object.keys(previous)) {
        if (!(key in current)) removedSections.push(key);
      }
    } catch { /* skip parse errors */ }
  }

  return { valid: secrets.length === 0 && removedSections.length === 0, removedSections, secrets };
}
