/**
 * migration-policy.ts — PURE 3-tier risk policy for EF Core migration operations.
 *
 * Replaces the blanket "never touch migrations" rule with a verdict graded by actual
 * IRREVERSIBILITY, decomposed along the axes that truly carry risk:
 *
 *   • target database   — local dev (throwaway, recreatable) vs shared/staging/prod
 *   • branch / history   — feature·develop (malleable) vs main·release·hotfix (protected)
 *   • destructiveness    — additive Up() vs Drop/narrowing (data loss)
 *
 * Three tiers:
 *   🟢 green  — autonomous (reversible / local / additive on a non-protected branch)
 *   🟡 yellow — perform, but SURFACE the diff and require explicit confirmation first
 *   🔴 red    — refuse autonomously; a human decides (irreversible / shared / history)
 *
 * FAIL-CLOSED: any ambiguity resolves to the safest (most gated) tier. The worst case
 * we protect hardest is "apply to a database we WRONGLY thought was local" — so DB
 * classification is allowlist-based and a non-provably-local target never gates green.
 *
 * Pure — no I/O. The caller gathers inputs (branch via git, connection string from
 * appsettings, the generated Up() body) and ENFORCES the verdict. Branch typing reuses
 * `determineBaseBranch` (squash-base) so there is one source of truth for branch tiers.
 */

import { determineBaseBranch, type SquashBranchType, type BranchNames } from './squash-base.js';

export type { BranchNames };

export type MigrationOp =
  | 'create' //  dotnet ef migrations add
  | 'remove' //  dotnet ef migrations remove (drop the last, unapplied migration)
  | 'apply' //   dotnet ef database update (→ latest)
  | 'revert' //  dotnet ef database update <earlier target> (down-migration)
  | 'squash'; //  history rewrite (governed by the dedicated squash policy)

export type DbTarget = 'local' | 'remote' | 'unknown';
export type Tier = 'green' | 'yellow' | 'red';
export type PolicyAxis = 'branch' | 'db-target' | 'destructive' | 'history';

export interface DbClassification {
  target: DbTarget;
  /** The host token we classified (instance/port stripped), for the reason string. */
  host: string | null;
  reason: string;
}

export interface MigrationActionInput {
  op: MigrationOp;
  /** Current git branch (e.g. `feature/x`, `develop`, `main`, `release/5.1.0`). */
  branch: string;
  /** Optional main/develop name overrides (from the gitflow config). */
  branchNames?: BranchNames;
  /** Target DB classification — REQUIRED for `apply`/`revert`; ignored otherwise. */
  dbTarget?: DbTarget;
  /** For `create`: does the generated Up() drop or narrow (data loss)? */
  destructive?: boolean;
}

export interface PolicyVerdict {
  tier: Tier;
  /** Autonomous refusal (red). A human may still authorize — this only means "not on my own". */
  blocked: boolean;
  /** Yellow (and red): the operation must be shown + explicitly confirmed before proceeding. */
  requiresConfirmation: boolean;
  /** Which axis drove the verdict — for a precise, non-generic message. */
  axis: PolicyAxis;
  reason: string;
}

// Loopback / local-instance servers a dev DB can sit on. Everything else fails closed.
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '.', '(local)', '(localdb)']);

/**
 * Classify a connection string's target database. ALLOWLIST-based + fail-closed:
 * only a well-known loopback/localdb host is `local`; an Azure/FQDN/non-loopback-IP
 * host is `remote`; a bare hostname or an absent server token is `unknown`. Both
 * `remote` and `unknown` gate as non-local (never green for an apply).
 */
export function classifyDbTarget(connString: string | null | undefined): DbClassification {
  if (!connString || !connString.trim()) {
    return { target: 'unknown', host: null, reason: 'no connection string provided' };
  }
  const m = connString.match(/(?:server|data\s*source)\s*=\s*([^;]+)/i);
  if (!m) {
    return { target: 'unknown', host: null, reason: 'no Server= / Data Source= token in the connection string' };
  }
  // Strip a `tcp:` prefix, then drop the instance (`\X`) and port (`,1433`) → bare host.
  const host = m[1].replace(/^tcp:/i, '').split(/[\\,]/)[0].trim().toLowerCase();

  if (LOCAL_HOSTS.has(host) || host.startsWith('(localdb)')) {
    return { target: 'local', host, reason: `local dev database (server "${host}") — throwaway / recreatable` };
  }
  const looksRemote =
    host.endsWith('.database.windows.net') || // Azure SQL
    /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || //      a non-loopback IPv4
    host.includes('.'); //                          any FQDN
  if (looksRemote) {
    return { target: 'remote', host, reason: `shared / remote database (server "${host}") — changes are irreversible` };
  }
  // A bare machine name (e.g. MYPC\SQLEXPRESS) — cannot PROVE it is local → fail closed.
  return { target: 'unknown', host, reason: `server "${host}" is not a provably-local host — treated as non-local` };
}

const green = (axis: PolicyAxis, reason: string): PolicyVerdict =>
  ({ tier: 'green', blocked: false, requiresConfirmation: false, axis, reason });
const yellow = (axis: PolicyAxis, reason: string): PolicyVerdict =>
  ({ tier: 'yellow', blocked: false, requiresConfirmation: true, axis, reason });
const red = (axis: PolicyAxis, reason: string): PolicyVerdict =>
  ({ tier: 'red', blocked: true, requiresConfirmation: true, axis, reason });

function isProtected(t: SquashBranchType): boolean {
  return t === 'main' || t === 'release' || t === 'hotfix';
}

/**
 * Grade ONE migration operation. Total + deterministic. Branch type is resolved via
 * `determineBaseBranch` (the squash golden-rule classifier) so tiers never diverge.
 */
export function decideMigrationAction(input: MigrationActionInput): PolicyVerdict {
  const branchType = determineBaseBranch(input.branch, input.branchNames).branchType;
  const db = input.dbTarget ?? 'unknown';

  switch (input.op) {
    case 'create': {
      // History axis dominates: never mint a migration on a protected branch.
      if (branchType === 'main') {
        return red('branch', `"${input.branch}" is a protected production branch — never create a migration there.`);
      }
      if (branchType === 'release' || branchType === 'hotfix') {
        return yellow('branch', `creating a migration on a ${branchType} branch is unusual and ships fast toward prod — confirm it is intended.`);
      }
      // feature / develop / unknown: green unless the diff loses data.
      if (input.destructive) {
        return yellow('destructive', 'the generated Up() drops or narrows columns (data loss) — confirm this is an intended schema change, not an unintended rename.');
      }
      return green('branch', `additive migration on ${branchType} — reversible code artifact; the snapshot is reconciled by squash-before-PR.`);
    }

    case 'apply': {
      // Target-DB axis dominates: a local dev DB is throwaway; anything else is a deploy.
      if (db === 'local') {
        return green('db-target', 'applying pending migrations to the LOCAL dev database (recreatable) — no shared state, no history change.');
      }
      return red('db-target', `applying migrations to a ${db} database is irreversible — a human deploys to non-local targets.`);
    }

    case 'revert': {
      // Down-migration: drops are possible even locally, but local stays recoverable.
      if (db === 'local') {
        return yellow('db-target', 'a down-migration can drop columns even on the local DB — confirm before reverting.');
      }
      return red('db-target', `reverting migrations on a ${db} database is irreversible data loss — a human decides.`);
    }

    case 'remove': {
      if (branchType === 'main') {
        return red('branch', `"${input.branch}" is protected — never remove a migration there.`);
      }
      return yellow('history', 'removing the last migration is safe ONLY if it was never pushed or applied elsewhere — confirm.');
    }

    case 'squash':
      // History rewrite — out of scope here; the squash CLI enforces its own
      // missing-ref / behind-base / brute-force gates (squash-policy.ts).
      return red('history', 'a migration squash rewrites history — governed by the dedicated squash policy, never an autonomous action.');

    default:
      return red('history', `unknown migration operation "${(input as MigrationActionInput).op}" — refusing by default (fail-closed).`);
  }
}

// Builder calls in a migration's Up() that DROP or destroy data. Mirrors the intent
// of the gitflow commit guard's Up()-only destructive scan. Index drops are omitted
// (no data loss); AlterColumn is omitted (widening is common + safe, and narrowing is
// too noisy to gate on without type analysis).
const DESTRUCTIVE_OPS = ['DropColumn', 'DropTable', 'DropForeignKey', 'DropPrimaryKey', 'DropSchema', 'DropSequence'];

/**
 * Does the given Up() body contain a data-losing operation? Pure string scan over the
 * real builder call (`.DropColumn(`), robust to a frozen SQL literal that merely
 * mentions the word. The caller is responsible for passing ONLY the Up() body (the
 * Down() inverse legitimately contains the opposite ops).
 */
export function hasDestructiveUp(upBody: string | null | undefined): boolean {
  if (!upBody) return false;
  return DESTRUCTIVE_OPS.some((op) => upBody.includes(`.${op}(`));
}
