// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

/**
 * Resolve Policy - Single Source of Truth for Methodology Decisions
 *
 * WU-1259: Provides a unified policy resolver that both wu:spawn and gates use.
 *
 * This module:
 * - Defines the methodology.* config schema
 * - Implements resolvePolicy() to produce ResolvedPolicy
 * - Applies precedence: template defaults -> methodology.overrides -> explicit gates.* -> CLI flags
 *
 * @module resolve-policy
 */

import { z } from 'zod';
import { WU_TYPES, WU_TYPE_VALUES, type WUType } from '../constants/wu-statuses.js';

/**
 * Testing methodology options
 */
export const TESTING_METHODOLOGY = {
  TDD: 'tdd',
  TEST_AFTER: 'test-after',
  NONE: 'none',
} as const;

export type TestingMethodology = (typeof TESTING_METHODOLOGY)[keyof typeof TESTING_METHODOLOGY];

/**
 * Architecture methodology options
 */
export const ARCHITECTURE_METHODOLOGY = {
  HEXAGONAL: 'hexagonal',
  LAYERED: 'layered',
  NONE: 'none',
} as const;

export type ArchitectureMethodology =
  (typeof ARCHITECTURE_METHODOLOGY)[keyof typeof ARCHITECTURE_METHODOLOGY];

/**
 * Coverage mode options
 */
export const COVERAGE_MODE = {
  BLOCK: 'block',
  WARN: 'warn',
  OFF: 'off',
} as const;

export type CoverageMode = (typeof COVERAGE_MODE)[keyof typeof COVERAGE_MODE];

/**
 * Zod schema for testing methodology enum
 */
export const TestingMethodologySchema = z.enum(['tdd', 'test-after', 'none']);

/**
 * Zod schema for architecture methodology enum
 */
export const ArchitectureMethodologySchema = z.enum(['hexagonal', 'layered', 'none']);

/**
 * Zod schema for coverage mode enum
 */
export const CoverageModeSchema = z.enum(['block', 'warn', 'off']);

export const TddDiffEvidenceConfigSchema = z.object({
  mode: CoverageModeSchema.optional(),
  applies_to_types: z.array(z.enum(WU_TYPE_VALUES)).min(1).optional(),
  exempt_paths: z.array(z.string().min(1)).optional(),
});

export type TddDiffEvidenceConfig = z.infer<typeof TddDiffEvidenceConfigSchema>;

/**
 * WU-2650: Commit-order (RED-first) policy schema.
 *
 * Sibling to TddDiffEvidenceConfigSchema. Where the diff-evidence policy
 * checks that tests EXIST somewhere in the branch diff, this policy checks
 * that tests APPEARED BEFORE (or alongside) the runtime code in commit order.
 *
 * Defaults: when methodology.testing = 'tdd', mode resolves to 'block'.
 * Otherwise 'off'. applies_to_types defaults to [feature, bug] so docs /
 * chore / process / tooling / refactor WUs bypass automatically.
 */
export const TddOrderingConfigSchema = z.object({
  mode: CoverageModeSchema.optional(),
  applies_to_types: z.array(z.enum(WU_TYPE_VALUES)).min(1).optional(),
  min_runtime_loc_per_commit_without_tests: z.number().int().positive().optional(),
  grace_commits: z.number().int().nonnegative().optional(),
  exempt_paths: z.array(z.string().min(1)).optional(),
});

export type TddOrderingConfig = z.infer<typeof TddOrderingConfigSchema>;

/**
 * Methodology overrides schema
 *
 * These allow tweaking template defaults without changing methodology.
 */
export const MethodologyOverridesSchema = z.object({
  /** Override the default coverage threshold from the testing methodology */
  coverage_threshold: z.number().min(0).max(100).optional(),
  /** Override the default coverage mode from the testing methodology */
  coverage_mode: CoverageModeSchema.optional(),
});

export type MethodologyOverrides = z.infer<typeof MethodologyOverridesSchema>;

/**
 * WU-1899: Work classification UI pattern configuration
 *
 * Allows extending default UI detection patterns with project-specific
 * code path patterns and lane hints. Custom values EXTEND defaults,
 * they do not replace them.
 *
 * @example
 * ```yaml
 * methodology:
 *   work_classification:
 *     ui:
 *       code_path_patterns:
 *         - 'src/widgets/*.tsx'
 *       lane_hints:
 *         - 'Design'
 * ```
 */
export const WorkClassificationUiSchema = z.object({
  /**
   * Additional glob patterns for detecting UI work via code paths.
   * These extend the built-in defaults (CSS, SCSS, LESS, components, pages, etc.).
   * Uses minimatch glob syntax.
   */
  code_path_patterns: z.array(z.string()).optional(),

  /**
   * Additional lane parent hints for detecting UI work.
   * These extend the built-in defaults (Experience, Frontend, UI, Design).
   * Matched case-insensitively against the lane parent (part before the colon).
   */
  lane_hints: z.array(z.string()).optional(),
});

/** WU-1899: TypeScript type for work classification UI config */
export type WorkClassificationUi = z.infer<typeof WorkClassificationUiSchema>;

/**
 * WU-1899: Work classification configuration schema
 *
 * Controls signal-based work domain detection for code-path-aware
 * UI/backend/docs/infra classification.
 *
 * @example
 * ```yaml
 * methodology:
 *   work_classification:
 *     ui:
 *       code_path_patterns:
 *         - 'src/widgets/*.tsx'
 *       lane_hints:
 *         - 'Design'
 * ```
 */
export const WorkClassificationConfigSchema = z.object({
  /** UI detection pattern overrides (extend defaults) */
  ui: WorkClassificationUiSchema.optional(),
});

/** WU-1899: TypeScript type for work classification config */
export type WorkClassificationSchemaConfig = z.infer<typeof WorkClassificationConfigSchema>;

/**
 * Main methodology configuration schema
 *
 * Config example in workspace.yaml:
 * ```yaml
 * methodology:
 *   testing: 'tdd'              # tdd | test-after | none
 *   architecture: 'hexagonal'   # hexagonal | layered | none
 *   overrides:
 *     coverage_threshold: 85    # Override TDD's default 90%
 *     coverage_mode: 'warn'     # Override TDD's default 'block'
 *   work_classification:
 *     ui:
 *       code_path_patterns:
 *         - 'src/widgets/*.tsx'
 *       lane_hints:
 *         - 'Design'
 * ```
 */
export const MethodologyConfigSchema = z.object({
  /** Testing methodology (default: 'tdd') */
  testing: TestingMethodologySchema.default('tdd'),
  /** Architecture methodology (default: 'hexagonal') */
  architecture: ArchitectureMethodologySchema.default('hexagonal'),
  /** Optional overrides for template defaults */
  overrides: MethodologyOverridesSchema.optional(),
  /**
   * WU-1899: Work classification configuration.
   * Extends default signal patterns for UI/backend/docs/infra detection.
   * Custom patterns extend defaults, they do not replace them.
   */
  work_classification: WorkClassificationConfigSchema.optional(),
});

export type MethodologyConfig = z.infer<typeof MethodologyConfigSchema>;

/**
 * Template defaults by testing methodology
 *
 * These define the baseline behavior for each testing approach.
 *
 * | Methodology | Coverage Threshold | Coverage Mode | Tests Required |
 * |-------------|-------------------|---------------|----------------|
 * | tdd         | 90%               | block         | true           |
 * | test-after  | 70%               | warn          | true           |
 * | none        | 0%                | off           | false          |
 */
interface TestingTemplateDefaults {
  coverage_threshold: number;
  coverage_mode: CoverageMode;
  tests_required: boolean;
}

const DEFAULT_TDD_DIFF_APPLIES_TO_TYPES = [
  WU_TYPES.FEATURE,
  WU_TYPES.BUG,
] as const satisfies readonly WUType[];
const DEFAULT_TDD_DIFF_EXEMPT_PATHS: readonly string[] = [];

const TESTING_TEMPLATE_DEFAULTS: Record<TestingMethodology, TestingTemplateDefaults> = {
  tdd: {
    coverage_threshold: 90,
    coverage_mode: 'block',
    tests_required: true,
  },
  'test-after': {
    coverage_threshold: 70,
    coverage_mode: 'warn',
    tests_required: true,
  },
  none: {
    coverage_threshold: 0,
    coverage_mode: 'off',
    tests_required: false,
  },
};

/**
 * The resolved policy used by wu:spawn and gates
 *
 * This is the single source of truth for methodology decisions.
 * All consumers (spawn prompts, gate runners, etc.) should use this type.
 */
export interface ResolvedPolicy {
  /** Active testing methodology */
  testing: TestingMethodology;
  /** Active architecture methodology */
  architecture: ArchitectureMethodology;
  /** Resolved coverage threshold (0-100) */
  coverage_threshold: number;
  /** Resolved coverage mode */
  coverage_mode: CoverageMode;
  /** Whether tests are required for completion */
  tests_required: boolean;
  /** Resolved TDD diff evidence applicability */
  tdd_diff_evidence: TddDiffEvidencePolicy;
  /** WU-2650: Resolved commit-order (RED-first) policy */
  tdd_ordering: TddOrderingPolicy;
}

export interface TddDiffEvidencePolicy {
  mode: CoverageMode;
  applies_to_types: WUType[];
  exempt_paths: string[];
}

/**
 * WU-2650: Resolved commit-order policy.
 */
export interface TddOrderingPolicy {
  mode: CoverageMode;
  applies_to_types: WUType[];
  min_runtime_loc_per_commit_without_tests: number;
  grace_commits: number;
  exempt_paths: string[];
}

/**
 * WU-2650: Default threshold for "non-trivial runtime LOC" in a single commit
 * before the commit-order gate flags a missing preceding test commit.
 *
 * Rationale: WU-2641 committed 1198 LOC of runtime ahead of tests. 50 is low
 * enough to catch meaningful feature work but high enough to ignore small
 * config-only edits and typo fixes.
 */
const DEFAULT_TDD_ORDERING_MIN_RUNTIME_LOC = 50;
const DEFAULT_TDD_ORDERING_GRACE_COMMITS = 0;

/**
 * Options for resolvePolicy
 */
export interface ResolvePolicyOptions {
  /**
   * Raw config object before Zod defaults were applied.
   * Used to detect explicit vs default values for gates.* fields.
   *
   * When provided, only EXPLICIT gates.* settings override methodology.
   * When not provided, UnsafeAny gates.* value (including defaults) overrides methodology.
   */
  rawConfig?: {
    gates?: {
      minCoverage?: number;
      enableCoverage?: boolean;
      tdd_diff_evidence?: TddDiffEvidenceConfig;
      tdd_ordering?: TddOrderingConfig;
    };
  };
}

interface PolicyConfigInput {
  methodology?: MethodologyConfig;
  gates: {
    minCoverage: number;
    enableCoverage: boolean;
    tdd_diff_evidence?: TddDiffEvidenceConfig;
    tdd_ordering?: TddOrderingConfig;
  };
}

function resolveDefaultTddDiffEvidenceMode(testing: TestingMethodology): CoverageMode {
  return testing === TESTING_METHODOLOGY.TDD ? COVERAGE_MODE.BLOCK : COVERAGE_MODE.OFF;
}

function cloneWuTypes(value: readonly WUType[] | undefined): WUType[] {
  return [...(value ?? DEFAULT_TDD_DIFF_APPLIES_TO_TYPES)];
}

function cloneExemptPaths(value: readonly string[] | undefined): string[] {
  return [...(value ?? DEFAULT_TDD_DIFF_EXEMPT_PATHS)];
}

function resolveTddDiffEvidencePolicy(
  testing: TestingMethodology,
  config: TddDiffEvidenceConfig | undefined,
): TddDiffEvidencePolicy {
  return {
    mode: config?.mode ?? resolveDefaultTddDiffEvidenceMode(testing),
    applies_to_types: cloneWuTypes(config?.applies_to_types),
    exempt_paths: cloneExemptPaths(config?.exempt_paths),
  };
}

/**
 * WU-2650: Resolve the commit-order policy. Defaults mirror the diff-evidence
 * policy: enforce under TDD, off otherwise; applies to feature/bug only.
 */
function resolveTddOrderingPolicyInternal(
  testing: TestingMethodology,
  config: TddOrderingConfig | undefined,
): TddOrderingPolicy {
  return {
    mode: config?.mode ?? resolveDefaultTddDiffEvidenceMode(testing),
    applies_to_types: cloneWuTypes(config?.applies_to_types),
    min_runtime_loc_per_commit_without_tests:
      config?.min_runtime_loc_per_commit_without_tests ?? DEFAULT_TDD_ORDERING_MIN_RUNTIME_LOC,
    grace_commits: config?.grace_commits ?? DEFAULT_TDD_ORDERING_GRACE_COMMITS,
    exempt_paths: cloneExemptPaths(config?.exempt_paths),
  };
}

/**
 * Resolve the effective policy from configuration
 *
 * Precedence (highest to lowest):
 * 1. CLI flags (not handled here - handled by command layer)
 * 2. Explicit gates.* configuration (only if rawConfig provided to detect explicit vs default)
 * 3. methodology.overrides
 * 4. methodology template defaults
 *
 * This ensures backwards compatibility: existing users with explicit
 * gates.* config see no change, while new users can use methodology
 * config for a higher-level abstraction.
 *
 * @param config - The full LumenFlow configuration
 * @param options - Options including rawConfig for explicit detection
 * @returns The resolved policy for use by wu:spawn and gates
 *
 * @example
 * ```typescript
 * import { getConfig } from './lumenflow-config.js';
 * import { resolvePolicy } from './resolve-policy.js';
 *
 * const config = getConfig();
 * const policy = resolvePolicy(config);
 *
 * console.log(policy.coverage_threshold); // 90 (or configured value)
 * console.log(policy.testing); // 'tdd' (or configured value)
 * ```
 *
 * @example With raw config for explicit detection
 * ```typescript
 * const rawConfig = { methodology: { testing: 'test-after' } };
 * const config = parseConfig(rawConfig);
 * const policy = resolvePolicy(config, { rawConfig });
 * // policy.coverage_threshold will be 70 (test-after template default)
 * // because gates.minCoverage wasn't EXPLICITLY set
 * ```
 */
export function resolvePolicy(
  config: PolicyConfigInput,
  options: ResolvePolicyOptions = {},
): ResolvedPolicy {
  const { rawConfig } = options;

  // Parse methodology config (provides defaults if not specified)
  const methodology = MethodologyConfigSchema.parse(config.methodology ?? {});

  // Get template defaults based on testing methodology
  const templateDefaults = TESTING_TEMPLATE_DEFAULTS[methodology.testing];

  // Layer 1: Start with template defaults
  let coverage_threshold = templateDefaults.coverage_threshold;
  let coverage_mode = templateDefaults.coverage_mode;
  const tests_required = templateDefaults.tests_required;

  // Layer 2: Apply methodology.overrides (if specified)
  if (methodology.overrides?.coverage_threshold !== undefined) {
    coverage_threshold = methodology.overrides.coverage_threshold;
  }
  if (methodology.overrides?.coverage_mode !== undefined) {
    coverage_mode = methodology.overrides.coverage_mode;
  }

  // Layer 3: Apply explicit gates.* configuration (highest precedence)
  // This ensures backwards compatibility with existing gates config
  //
  // Key insight: We only want EXPLICIT gates.* to override methodology.
  // If rawConfig is provided, we check if gates values were explicitly set.
  // If rawConfig is NOT provided (legacy mode), we check if methodology
  // was specified - if so, methodology controls unless gates differ from default.

  const gates = config.gates;

  // Determine if gates.minCoverage was explicitly set
  const gatesMinCoverageExplicit =
    rawConfig !== undefined ? rawConfig.gates?.minCoverage !== undefined : false;

  // Determine if gates.enableCoverage was explicitly set
  const gatesEnableCoverageExplicit =
    rawConfig !== undefined ? rawConfig.gates?.enableCoverage !== undefined : false;

  // Apply gates.minCoverage only if explicitly set, or if no methodology was specified
  // (for backwards compatibility with pre-methodology configs)
  const methodologySpecified = config.methodology !== undefined;

  if (gatesMinCoverageExplicit || (!methodologySpecified && rawConfig === undefined)) {
    // gates.minCoverage overrides methodology coverage_threshold
    coverage_threshold = gates.minCoverage;
  }

  // gates.enableCoverage: false effectively sets coverage_mode to 'off'
  if (gatesEnableCoverageExplicit || (!methodologySpecified && rawConfig === undefined)) {
    if (gates?.enableCoverage === false) {
      coverage_mode = 'off';
    }
  }

  const tdd_diff_evidence = resolveTddDiffEvidencePolicy(
    methodology.testing,
    config.gates?.tdd_diff_evidence,
  );

  const tdd_ordering = resolveTddOrderingPolicyInternal(
    methodology.testing,
    config.gates?.tdd_ordering,
  );

  return {
    testing: methodology.testing,
    architecture: methodology.architecture,
    coverage_threshold,
    coverage_mode,
    tests_required,
    tdd_diff_evidence,
    tdd_ordering,
  };
}

/**
 * Create a default resolved policy
 *
 * Convenience function for when no config is available.
 * Returns strict TDD defaults.
 */
export function getDefaultPolicy(): ResolvedPolicy {
  return {
    testing: 'tdd',
    architecture: 'hexagonal',
    coverage_threshold: 90,
    coverage_mode: 'block',
    tests_required: true,
    tdd_diff_evidence: {
      mode: COVERAGE_MODE.BLOCK,
      applies_to_types: [...DEFAULT_TDD_DIFF_APPLIES_TO_TYPES],
      exempt_paths: [...DEFAULT_TDD_DIFF_EXEMPT_PATHS],
    },
    tdd_ordering: {
      mode: COVERAGE_MODE.BLOCK,
      applies_to_types: [...DEFAULT_TDD_DIFF_APPLIES_TO_TYPES],
      min_runtime_loc_per_commit_without_tests: DEFAULT_TDD_ORDERING_MIN_RUNTIME_LOC,
      grace_commits: DEFAULT_TDD_ORDERING_GRACE_COMMITS,
      exempt_paths: [...DEFAULT_TDD_DIFF_EXEMPT_PATHS],
    },
  };
}
