import { evaluateGate } from '../../core/gate/evaluateGate';
import type { Fact } from '../../core/facts/Fact';
import type { Finding } from '../../core/gate/Finding';
import type { GateOutcome } from '../../core/gate/GateOutcome';
import type { GatePolicy } from '../../core/gate/GatePolicy';
import type { RuleSet } from '../../core/rules/RuleSet';
import type { SkillsRuleSetLoadResult } from '../config/skillsRuleSet';
import type { ResolvedStagePolicy } from '../gate/stagePolicies';
import type { DetectedPlatforms } from '../platform/detectPlatforms';
import { GitService, type IGitService } from './GitService';
import { EvidenceService, type IEvidenceService } from './EvidenceService';
import {
  resolveActiveGateWaiver,
  type GateWaiverResult,
} from './gateWaiver';
import {
  evaluateAstIntelligenceDualValidation,
  evaluatePlatformGateFindings,
  type AstIntelligenceDualValidationResult,
} from './runPlatformGateEvaluation';
import {
  countScannedFilesFromFacts,
  resolveFactsForGateScope,
  type GateScope,
} from './runPlatformGateFacts';
import { evaluateBrownfieldHotspotFindings } from './brownfieldHotspots';
import { emitPlatformGateEvidence } from './runPlatformGateEvidence';
import { printGateFindings } from './runPlatformGateOutput';
import { evaluateSddPolicy, type SddDecision } from '../sdd';
import type { SnapshotEvaluationMetrics } from '../evidence/schema';
import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
import { createEmptySnapshotRulesCoverage } from '../evidence/rulesCoverage';
import { enforceTddBddPolicy } from '../tdd/enforcement';
import type { TddBddSnapshot } from '../tdd/types';
import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
import {
  filterFactsByPathPrefixes,
  resolveGateScopePathPrefixesFromEnv,
} from './filterFactsByPathPrefixes';
import {
  readPreWriteLeaseStatus,
  toPreWriteEnforcementGapFinding,
} from '../lifecycle/preWriteLease';
import { evaluateContextGate, type ContextGateResult } from '../context/contextGate';
import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification';

export type OperationalMemoryShadowRecommendation = {
  recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
  confidence: number;
  reasonCodes: ReadonlyArray<string>;
};

export type GateServices = {
  git: IGitService;
  evidence: IEvidenceService;
};

export type GateDependencies = {
  evaluateGate: typeof evaluateGate;
  evaluatePlatformGateFindings: typeof evaluatePlatformGateFindings;
  evaluateBrownfieldHotspotFindings: typeof evaluateBrownfieldHotspotFindings;
  resolveFactsForGateScope: typeof resolveFactsForGateScope;
  emitPlatformGateEvidence: typeof emitPlatformGateEvidence;
  printGateFindings: typeof printGateFindings;
  enforceTddBddPolicy: typeof enforceTddBddPolicy;
  evaluateAstIntelligenceDualValidation: typeof evaluateAstIntelligenceDualValidation;
  buildMemoryShadowRecommendation: (params: {
    findings: ReadonlyArray<Finding>;
    tddBddSnapshot?: TddBddSnapshot;
  }) => OperationalMemoryShadowRecommendation | undefined;
  evaluateSddForStage: (
    stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI',
    repoRoot: string
  ) => Pick<SddDecision, 'allowed' | 'code' | 'message'>;
  resolveActiveGateWaiver: (params: {
    repoRoot: string;
    stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
    branch: string | null;
  }) => GateWaiverResult;
  evaluateContextGate: typeof evaluateContextGate;
  notifyGateBlocked: typeof emitGateBlockedNotification;
};

const defaultServices: GateServices = {
  git: new GitService(),
  evidence: new EvidenceService(),
};

const hasPositiveFindingLine = (lines: Finding['lines']): boolean => {
  if (typeof lines === 'number') {
    return Number.isFinite(lines) && lines > 0;
  }
  if (typeof lines === 'string') {
    return lines.trim().length > 0;
  }
  if (Array.isArray(lines)) {
    return lines.some((line) => Number.isFinite(line) && line > 0);
  }
  return false;
};

const hasActionableFindingLocation = (finding: Finding): boolean => {
  return (
    hasPositiveFindingLine(finding.lines) ||
    finding.primary_node !== undefined ||
    (finding.related_nodes?.length ?? 0) > 0
  );
};

const normalizeFindingLines = (lines: Finding['lines']): ReadonlyArray<number> => {
  if (typeof lines === 'number') {
    return Number.isFinite(lines) && lines > 0 ? [Math.trunc(lines)] : [];
  }
  if (Array.isArray(lines)) {
    return [...new Set(lines.filter((line) => Number.isFinite(line) && line > 0).map((line) => Math.trunc(line)))].sort(
      (left, right) => left - right
    );
  }
  if (typeof lines === 'string') {
    return [...new Set(
      lines
        .split(/[^0-9]+/)
        .map((line) => Number.parseInt(line, 10))
        .filter((line) => Number.isFinite(line) && line > 0)
    )].sort((left, right) => left - right);
  }
  return [];
};

const parseChangedLinesByPathFromUnifiedDiff = (diff: string): Map<string, Set<number>> => {
  const changedLinesByPath = new Map<string, Set<number>>();
  let currentPath: string | undefined;

  for (const rawLine of diff.split('\n')) {
    if (rawLine.startsWith('+++ b/')) {
      const nextPath = rawLine.slice('+++ b/'.length).trim();
      currentPath = nextPath.length > 0 && nextPath !== '/dev/null' ? toNormalizedPath(nextPath) : undefined;
      if (currentPath && !changedLinesByPath.has(currentPath)) {
        changedLinesByPath.set(currentPath, new Set<number>());
      }
      continue;
    }

    if (!currentPath || !rawLine.startsWith('@@')) {
      continue;
    }

    const hunkMatch = rawLine.match(/\+(\d+)(?:,(\d+))?/);
    if (!hunkMatch) {
      continue;
    }
    const startLine = Number.parseInt(hunkMatch[1] ?? '', 10);
    const lineCount = hunkMatch[2] === undefined ? 1 : Number.parseInt(hunkMatch[2], 10);
    if (!Number.isFinite(startLine) || startLine <= 0 || !Number.isFinite(lineCount)) {
      continue;
    }
    const changedLines = changedLinesByPath.get(currentPath) ?? new Set<number>();
    for (let offset = 0; offset < Math.max(0, lineCount); offset += 1) {
      changedLines.add(startLine + offset);
    }
    changedLinesByPath.set(currentPath, changedLines);
  }

  return changedLinesByPath;
};

const collectStagedChangedLinesByPath = (
  git: IGitService,
  repoRoot: string
): Map<string, Set<number>> => {
  try {
    return parseChangedLinesByPathFromUnifiedDiff(
      git.runGit(['diff', '--cached', '--unified=0', '--no-ext-diff'], repoRoot)
    );
  } catch {
    return new Map<string, Set<number>>();
  }
};

const isFindingOutsideChangedLines = (
  finding: Finding,
  changedLinesByPath: ReadonlyMap<string, ReadonlySet<number>>
): boolean => {
  if (!finding.filePath) {
    return false;
  }
  const changedLines = changedLinesByPath.get(toNormalizedPath(finding.filePath));
  if (!changedLines || changedLines.size === 0) {
    return false;
  }
  const findingLines = normalizeFindingLines(finding.lines);
  if (findingLines.length === 0) {
    return false;
  }
  return findingLines.every((line) => !changedLines.has(line));
};

const BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD = 20;

const isBroadFileLevelFinding = (finding: Finding): boolean => {
  const findingLines = normalizeFindingLines(finding.lines);
  if (findingLines.length <= BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD) {
    return false;
  }
  const nodeLines = [
    ...normalizeFindingLines(finding.primary_node?.lines),
    ...(finding.related_nodes ?? []).flatMap((node) => normalizeFindingLines(node.lines)),
  ];
  const hasPreciseNode =
    nodeLines.length > 0 && nodeLines.length <= BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD;
  return !hasPreciseNode;
};

const isFindingTooBroadForStagedDiffBlock = (
  finding: Finding,
  changedLinesByPath: ReadonlyMap<string, ReadonlySet<number>>
): boolean => {
  if (!finding.filePath || !isBroadFileLevelFinding(finding)) {
    return false;
  }
  const changedLines = changedLinesByPath.get(toNormalizedPath(finding.filePath));
  return typeof changedLines !== 'undefined' && changedLines.size > 0;
};

const normalizeScopedRuleEngineFindings = (params: {
  findings: ReadonlyArray<Finding>;
  scope: GateScope;
  stage: GatePolicy['stage'];
  changedLinesByPath: ReadonlyMap<string, ReadonlySet<number>>;
  stagedPaths: ReadonlyArray<string>;
}): ReadonlyArray<Finding> => {
  if (
    (params.scope.kind !== 'staged' && params.scope.kind !== 'repoAndStaged') ||
    (params.stage !== 'PRE_WRITE' && params.stage !== 'PRE_COMMIT')
  ) {
    return params.findings;
  }

  const stagedPathSet = new Set(params.stagedPaths.map(toNormalizedPath));
  return params.findings.map((finding) => {
    if (!isAstRuleFinding(finding)) {
      return finding;
    }
    const outsideStagedFileSet =
      params.scope.kind === 'staged' &&
      params.stagedPaths.length > 0 &&
      typeof finding.filePath === 'string' &&
      finding.filePath.length > 0 &&
      !stagedPathSet.has(toNormalizedPath(finding.filePath));
    const outsideChangedLines = isFindingOutsideChangedLines(
      finding,
      params.changedLinesByPath
    );
    const tooBroadForDiffBlock = isFindingTooBroadForStagedDiffBlock(
      finding,
      params.changedLinesByPath
    );
    if (!outsideStagedFileSet && !outsideChangedLines && !tooBroadForDiffBlock) {
      return finding;
    }
    return {
      ...finding,
      blocking: false,
      severity: 'INFO',
      message:
        `${finding.message} ` +
        (outsideStagedFileSet
          ? 'Baseline brownfield outside the staged file set; tracked as advisory for this atomic slice.'
          : tooBroadForDiffBlock
            ? 'Finding is file-level/broad and cannot prove the violation was introduced by the staged diff; tracked as advisory for this atomic slice.'
            : 'Baseline brownfield outside the staged diff; tracked as advisory for this atomic slice.'),
      expected_fix:
        finding.expected_fix ??
        (outsideStagedFileSet
          ? 'Plan a dedicated brownfield remediation slice for the non-staged file, or include it explicitly in the atomic commit.'
          : tooBroadForDiffBlock
          ? 'Emit a precise AST/nodal finding with line-level evidence for the changed node, or plan a dedicated brownfield remediation slice for this file-level debt.'
          : 'Plan a dedicated brownfield remediation slice for this pre-existing finding.'),
    };
  });
};

const normalizeNonBlockingSwiftTestingMigrationWarnings = (
  findings: ReadonlyArray<Finding>
): ReadonlyArray<Finding> => findings.map((finding) => {
  if (
    finding.ruleId !== 'skills.ios.prefer-swift-testing' ||
    finding.severity !== 'WARN'
  ) {
    return finding;
  }
  return {
    ...finding,
    blocking: false,
    expected_fix:
      finding.expected_fix ??
      'Mantén XCTest en suites legacy, UI o brownfield. Planifica una migración a Swift Testing en una slice dedicada si el repo lo decide.',
  };
});

const buildDefaultMemoryShadowRecommendation = (params: {
  findings: ReadonlyArray<Finding>;
  tddBddSnapshot?: TddBddSnapshot;
}): OperationalMemoryShadowRecommendation | undefined => {
  const hasCritical = params.findings.some((finding) => finding.severity === 'CRITICAL');
  const hasError = params.findings.some((finding) => finding.severity === 'ERROR');
  const hasWarn = params.findings.some((finding) => finding.severity === 'WARN');
  const reasonCodes: string[] = [];

  if (hasCritical || hasError) {
    reasonCodes.push('severity.error_or_critical');
  } else if (hasWarn) {
    reasonCodes.push('severity.warn');
  } else {
    reasonCodes.push('severity.clean');
  }

  if (params.tddBddSnapshot?.status === 'blocked') {
    reasonCodes.push('tdd_bdd.blocked');
  } else if (params.tddBddSnapshot?.status === 'advisory') {
    reasonCodes.push('tdd_bdd.advisory');
  } else if (params.tddBddSnapshot?.status === 'passed') {
    reasonCodes.push('tdd_bdd.passed');
  }

  if (hasCritical || hasError || params.tddBddSnapshot?.status === 'blocked') {
    return {
      recommendedOutcome: 'BLOCK',
      confidence: 0.9,
      reasonCodes,
    };
  }
  if (hasWarn) {
    return {
      recommendedOutcome: 'WARN',
      confidence: 0.75,
      reasonCodes,
    };
  }
  if (params.tddBddSnapshot?.status === 'advisory') {
    return {
      recommendedOutcome: 'WARN',
      confidence: 0.7,
      reasonCodes,
    };
  }
  return {
    recommendedOutcome: 'ALLOW',
    confidence: 0.65,
    reasonCodes,
  };
};

const defaultDependencies: GateDependencies = {
  evaluateGate,
  evaluatePlatformGateFindings,
  evaluateBrownfieldHotspotFindings,
  resolveFactsForGateScope,
  emitPlatformGateEvidence,
  printGateFindings,
  enforceTddBddPolicy,
  evaluateAstIntelligenceDualValidation,
  buildMemoryShadowRecommendation: buildDefaultMemoryShadowRecommendation,
  evaluateSddForStage: (stage, repoRoot) =>
    evaluateSddPolicy({
      stage,
      repoRoot,
    }).decision,
  resolveActiveGateWaiver: ({ repoRoot, stage, branch }) =>
    resolveActiveGateWaiver({
      repoRoot,
      stage,
      branch,
    }),
  evaluateContextGate,
  notifyGateBlocked: emitGateBlockedNotification,
};

const readSymbolicBranchRef = (git: IGitService, repoRoot: string): string | null => {
  try {
    const symbolicBranch = git.runGit(['symbolic-ref', '--short', 'HEAD'], repoRoot).trim();
    return symbolicBranch.length > 0 ? symbolicBranch : null;
  } catch {
    return null;
  }
};

const resolveCurrentBranch = (git: IGitService, repoRoot: string): string | null => {
  const symbolic = readSymbolicBranchRef(git, repoRoot);
  if (symbolic !== null) {
    return symbolic;
  }
  try {
    const branch = git.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim();
    if (branch.length === 0 || branch === 'HEAD') {
      return null;
    }
    return branch;
  } catch {
    return null;
  }
};

const toSddBlockingFinding = (decision: Pick<SddDecision, 'code' | 'message'>): Finding => ({
  ruleId: 'sdd.policy.blocked',
  severity: 'ERROR',
  code: decision.code,
  message: decision.message,
  filePath: 'openspec/changes',
  matchedBy: 'SddPolicy',
  source: 'sdd-policy',
});

const toRulesCoverageBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  activeRuleIds: ReadonlyArray<string>;
  evaluatedRuleIds: ReadonlyArray<string>;
  unevaluatedRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  if (params.unevaluatedRuleIds.length === 0) {
    return undefined;
  }
  const active = params.activeRuleIds.length;
  const evaluated = params.evaluatedRuleIds.length;
  const coverageRatio = active === 0 ? 1 : Number((evaluated / active).toFixed(6));
  const unevaluatedRuleIds = [...params.unevaluatedRuleIds].sort().join(', ');

  return {
    ruleId: 'governance.rules.coverage.incomplete',
    severity: 'ERROR',
    code: 'RULES_COVERAGE_INCOMPLETE_HIGH',
    message:
      `Coverage incomplete at ${params.stage}: unevaluated_rule_ids=[${unevaluatedRuleIds}]` +
      ` coverage_ratio=${coverageRatio}. Evaluate all active rules before proceeding.`,
    filePath: '.ai_evidence.json',
    matchedBy: 'RulesCoverageGuard',
    source: 'rules-coverage',
  };
};

const toSkillsUnsupportedAutoRulesBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  unsupportedAutoRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  if (params.unsupportedAutoRuleIds.length === 0) {
    return undefined;
  }

  const unsupportedAutoRuleIds = [...params.unsupportedAutoRuleIds].sort().join(', ');

  return {
    ruleId: 'governance.skills.detector-mapping.incomplete',
    severity: 'ERROR',
    code: 'SKILLS_DETECTOR_MAPPING_INCOMPLETE_HIGH',
    message:
      `Skills detector mapping incomplete at ${params.stage}: ` +
      `unsupported_auto_rule_ids=[${unsupportedAutoRuleIds}]. ` +
      'Map every AUTO skill rule to an AST detector before proceeding.',
    filePath: '.ai_evidence.json',
    matchedBy: 'SkillsDetectorMappingGuard',
    source: 'skills-detector-mapping',
  };
};

const toSkillsDeclarativeRulesClassificationFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  declarativeRuleIds: ReadonlyArray<string>;
  facts: ReadonlyArray<Fact>;
}): Finding | undefined => {
  if (process.env.PUMUKI_ENFORCE_DECLARATIVE_RULE_CLASSIFICATION !== '1') {
    return undefined;
  }
  if (params.declarativeRuleIds.length === 0) {
    return undefined;
  }
  if (collectObservedCodePathsFromFacts(params.facts).length === 0) {
    return undefined;
  }

  const sampleRuleIds = [...params.declarativeRuleIds].sort().slice(0, 12).join(', ');

  return {
    ruleId: 'governance.skills.declarative-rules.classification-required',
    severity: 'ERROR',
    code: 'SKILLS_DECLARATIVE_RULES_CLASSIFICATION_REQUIRED',
    message:
      `Skills declarative rules require AST/nodal classification at ${params.stage}: ` +
      `total=${params.declarativeRuleIds.length} sample_rule_ids=[${sampleRuleIds}]. ` +
      'Classify every rule as IMPLEMENTAR_DETECTOR, REQUIERE_ESTUDIO, or NO_ES_REGLA_DE_CODIGO; code skills cannot remain as hidden declarative/advisory rules.',
    filePath: 'skills.lock.json',
    matchedBy: 'SkillsDeclarativeRulesClassificationGuard',
    source: 'skills-declarative-rules-classification',
  };
};

const PLATFORM_SKILLS_RULE_PREFIXES: Record<
  'ios' | 'android' | 'backend' | 'frontend',
  string
> = {
  ios: 'skills.ios.',
  android: 'skills.android.',
  backend: 'skills.backend.',
  frontend: 'skills.frontend.',
};

const PLATFORM_REQUIRED_SKILLS_BUNDLES: Record<
  'ios' | 'android' | 'backend' | 'frontend',
  ReadonlyArray<string>
> = {
  ios: [
    'ios-guidelines',
    'ios-concurrency-guidelines',
    'ios-swiftui-expert-guidelines',
    'ios-swift-testing-guidelines',
    'ios-core-data-guidelines',
  ],
  android: ['android-guidelines'],
  backend: ['backend-guidelines'],
  frontend: ['frontend-guidelines'],
};

const isCriticalProfileSeverity = (severity: string): boolean => {
  return severity === 'CRITICAL' || severity === 'ERROR';
};

const toNormalizedPath = (value: string): string => value.replace(/\\/g, '/').trim();

const CODE_FILE_EXTENSIONS = new Set<string>([
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.mjs',
  '.cjs',
  '.swift',
  '.kt',
  '.kts',
]);

const isObservedCodePath = (path: string): boolean => {
  const normalized = toNormalizedPath(path).toLowerCase();
  if (normalized.length === 0) {
    return false;
  }
  if (
    normalized.startsWith('apps/backend/')
    || normalized.startsWith('apps/frontend/')
    || normalized.startsWith('apps/web/')
    || normalized.startsWith('apps/admin-dashboard/')
    || normalized.startsWith('apps/ios/')
    || normalized.startsWith('apps/android/')
  ) {
    return true;
  }
  for (const extension of CODE_FILE_EXTENSIONS) {
    if (normalized.endsWith(extension)) {
      return true;
    }
  }
  return false;
};

const collectObservedPathsFromFacts = (
  facts: ReadonlyArray<Fact>
): ReadonlyArray<string> => {
  const observedPaths = new Set<string>();
  for (const fact of facts) {
    if (fact.kind === 'FileChange' || fact.kind === 'FileContent') {
      const normalized = toNormalizedPath(fact.path);
      if (normalized.length > 0) {
        observedPaths.add(normalized);
      }
      continue;
    }
    if (fact.kind === 'Heuristic' && typeof fact.filePath === 'string') {
      const normalized = toNormalizedPath(fact.filePath);
      if (normalized.length > 0) {
        observedPaths.add(normalized);
      }
    }
  }
  return [...observedPaths].sort();
};

const collectObservedCodePathsFromFacts = (
  facts: ReadonlyArray<Fact>
): ReadonlyArray<string> => {
  const codePaths = collectObservedPathsFromFacts(facts).filter((path) => isObservedCodePath(path));
  return [...new Set(codePaths)].sort();
};

const isSkillsContractCarrierPath = (path: string): boolean => {
  const normalized = toNormalizedPath(path).toLowerCase();
  return (
    normalized === 'agents.md' ||
    normalized === 'skills.lock.json' ||
    normalized === 'skills.sources.json' ||
    normalized.startsWith('vendor/skills/') ||
    normalized.startsWith('docs/codex-skills/') ||
    normalized === '.pumuki/policy-as-code.json'
  );
};

const collectStagedPaths = (git: IGitService, repoRoot: string): ReadonlyArray<string> => {
  try {
    return git.runGit(['diff', '--cached', '--name-only'], repoRoot)
      .split('\n')
      .map((line) => toNormalizedPath(line))
      .filter((line) => line.length > 0);
  } catch {
    return [];
  }
};

const shouldAugmentStagedSkillsContractFactsWithRepoFacts = (params: {
  scope: GateScope;
  facts: ReadonlyArray<Fact>;
  stagedPaths: ReadonlyArray<string>;
}): boolean => {
  if (params.scope.kind !== 'staged') {
    return false;
  }
  if (collectObservedCodePathsFromFacts(params.facts).length > 0) {
    return false;
  }
  return params.stagedPaths.some((path) => isSkillsContractCarrierPath(path));
};

type IosTestFileContent = {
  path: string;
  content: string;
};

const isIosSwiftTestPath = (path: string): boolean => {
  const normalized = toNormalizedPath(path).toLowerCase();
  if (!normalized.endsWith('.swift')) {
    return false;
  }
  return normalized.includes('/tests/') || normalized.includes('/uitests/');
};

const collectIosTestFileContents = (
  facts: ReadonlyArray<Fact>
): ReadonlyArray<IosTestFileContent> => {
  const filesByPath = new Map<string, string>();
  for (const fact of facts) {
    if (fact.kind !== 'FileContent') {
      continue;
    }
    if (!isIosSwiftTestPath(fact.path)) {
      continue;
    }
    const normalizedPath = toNormalizedPath(fact.path);
    filesByPath.set(normalizedPath, fact.content);
  }
  return [...filesByPath.entries()]
    .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
    .map(([path, content]) => ({ path, content }));
};

const isXCTestSource = (content: string): boolean => {
  return /\bclass\s+\w+\s*:\s*XCTestCase\b/.test(content);
};

const hasMakeSUTPattern = (content: string): boolean => /\bmakeSUT\s*\(/.test(content);

const hasTrackForMemoryLeaksPattern = (content: string): boolean =>
  /\btrackForMemoryLeaks\s*\(/.test(content);

const IOS_CRITICAL_TEST_QUALITY_RULE_ID = 'skills.ios.critical-test-quality';

const hasIosTestQualityScope = (facts: ReadonlyArray<Fact>): boolean =>
  collectIosTestFileContents(facts).length > 0;

const appendUniqueRuleId = (
  ruleIds: ReadonlyArray<string>,
  ruleId: string
): readonly string[] => {
  if (ruleIds.includes(ruleId)) {
    return [...ruleIds];
  }
  return [...ruleIds, ruleId].sort((left, right) => left.localeCompare(right));
};

const toIosTestsQualityBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  facts: ReadonlyArray<Fact>;
}): Finding | undefined => {
  const testFiles = collectIosTestFileContents(params.facts);
  if (testFiles.length === 0) {
    return undefined;
  }

  const invalidFiles: string[] = [];
  for (const testFile of testFiles) {
    if (!isXCTestSource(testFile.content)) {
      continue;
    }
    const missingMarkers: string[] = [];
    if (!hasMakeSUTPattern(testFile.content)) {
      missingMarkers.push('makeSUT()');
    }
    if (!hasTrackForMemoryLeaksPattern(testFile.content)) {
      missingMarkers.push('trackForMemoryLeaks()');
    }
    if (missingMarkers.length === 0) {
      continue;
    }
    invalidFiles.push(`${testFile.path}{missing=[${missingMarkers.join(', ')}]}`);
  }

  if (invalidFiles.length === 0) {
    return undefined;
  }

  const sampleFiles = invalidFiles.slice(0, 3).join(' | ');
  return {
    ruleId: 'governance.skills.ios-test-quality.incomplete',
    severity: 'ERROR',
    code: 'IOS_TEST_QUALITY_PATTERN_MISSING_HIGH',
    message:
      `iOS test quality enforcement incomplete at ${params.stage}: ${sampleFiles}. ` +
      'Use makeSUT() factory pattern and trackForMemoryLeaks() in XCTest sources.',
    filePath: '.ai_evidence.json',
    matchedBy: 'IosTestsQualityGuard',
    source: 'skills-ios-test-quality',
  };
};

const toActiveRulesEmptyForCodeChangesBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  facts: ReadonlyArray<Fact>;
  activeRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  if (params.activeRuleIds.length > 0) {
    return undefined;
  }
  const codePaths = collectObservedCodePathsFromFacts(params.facts);
  if (codePaths.length === 0) {
    return undefined;
  }
  const samplePaths = codePaths.slice(0, 5).join(', ');
  return {
    ruleId: 'governance.rules.active-rule-coverage.empty',
    severity: 'ERROR',
    code: 'ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH',
    message:
      `Active rules coverage is empty at ${params.stage} while code changes were detected. ` +
      `sample_paths=[${samplePaths}]. Ensure skill/project rules are active before allowing this stage.`,
    filePath: '.ai_evidence.json',
    matchedBy: 'ActiveRulesCoverageGuard',
    source: 'rules-coverage',
  };
};

const detectRequiredSkillsScopesFromPaths = (
  observedPaths: ReadonlyArray<string>
): Record<'ios' | 'android' | 'backend' | 'frontend', ReadonlyArray<string>> => {
  const scopes: Record<'ios' | 'android' | 'backend' | 'frontend', string[]> = {
    ios: [],
    android: [],
    backend: [],
    frontend: [],
  };

  for (const observedPath of observedPaths) {
    const normalized = observedPath.toLowerCase();
    if (normalized.startsWith('apps/ios/') || normalized.endsWith('.swift')) {
      scopes.ios.push(observedPath);
    }
    if (
      normalized.startsWith('apps/android/')
      || normalized.endsWith('.kt')
      || normalized.endsWith('.kts')
    ) {
      scopes.android.push(observedPath);
    }
    if (normalized.startsWith('apps/backend/')) {
      scopes.backend.push(observedPath);
    }
    if (
      normalized.startsWith('apps/frontend/')
      || normalized.startsWith('apps/web/')
      || normalized.startsWith('apps/admin-dashboard/')
    ) {
      scopes.frontend.push(observedPath);
    }
  }

  return {
    ios: [...new Set(scopes.ios)].sort(),
    android: [...new Set(scopes.android)].sort(),
    backend: [...new Set(scopes.backend)].sort(),
    frontend: [...new Set(scopes.frontend)].sort(),
  };
};

const toSkillsScopeComplianceBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  facts: ReadonlyArray<Fact>;
  activeRuleIds: ReadonlyArray<string>;
  evaluatedRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  const observedPaths = collectObservedPathsFromFacts(params.facts);
  const requiredScopes = detectRequiredSkillsScopesFromPaths(observedPaths);
  const missingScopes: string[] = [];

  for (const scope of ['ios', 'android', 'backend', 'frontend'] as const) {
    const scopePaths = requiredScopes[scope];
    if (scopePaths.length === 0) {
      continue;
    }
    const prefix = PLATFORM_SKILLS_RULE_PREFIXES[scope];
    const hasActiveRules = params.activeRuleIds.some((ruleId) => ruleId.startsWith(prefix));
    const hasEvaluatedRules = params.evaluatedRuleIds.some((ruleId) => ruleId.startsWith(prefix));
    if (hasActiveRules && hasEvaluatedRules) {
      continue;
    }
    const reasons: string[] = [];
    if (!hasActiveRules) {
      reasons.push(`active_rules_prefix=${prefix} missing`);
    }
    if (!hasEvaluatedRules) {
      reasons.push(`evaluated_rules_prefix=${prefix} missing`);
    }
    const samplePaths = scopePaths.slice(0, 3).join(', ');
    missingScopes.push(`${scope}{${reasons.join('; ')} sample_paths=[${samplePaths}]}`);
  }

  if (missingScopes.length === 0) {
    return undefined;
  }

  return {
    ruleId: 'governance.skills.scope-compliance.incomplete',
    severity: 'ERROR',
    code: 'SKILLS_SCOPE_COMPLIANCE_INCOMPLETE_HIGH',
    message:
      `Skills scope compliance incomplete at ${params.stage}: ${missingScopes.join(' | ')}. ` +
      'Map changed file scopes to required skill rules and ensure those prefixes are active/evaluated.',
    filePath: '.ai_evidence.json',
    matchedBy: 'SkillsScopeComplianceGuard',
    source: 'skills-scope-compliance',
  };
};

const toPolicyAsCodeBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  policyTrace?: ResolvedStagePolicy['trace'];
}): Finding | undefined => {
  const validation = params.policyTrace?.validation;
  if (!validation || validation.status === 'valid' || !validation.strict) {
    return undefined;
  }

  return {
    ruleId: 'governance.policy-as-code.invalid',
    severity: 'ERROR',
    code: validation.code,
    message:
      `Policy-as-code validation failed at ${params.stage}: ${validation.message} ` +
      `policy_source=${params.policyTrace?.policySource ?? 'n/a'} ` +
      `policy_version=${params.policyTrace?.version ?? 'n/a'}.`,
    filePath: '.pumuki/policy-as-code.json',
    matchedBy: 'PolicyAsCodeGuard',
    source: 'policy-as-code',
  };
};

const toDegradedModeFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  policyTrace?: ResolvedStagePolicy['trace'];
}): Finding | undefined => {
  const degraded = params.policyTrace?.degraded;
  if (!degraded?.enabled) {
    return undefined;
  }
  if (degraded.action === 'block') {
    return {
      ruleId: 'governance.degraded-mode.blocked',
      severity: 'ERROR',
      code: degraded.code,
      message:
        `Degraded mode is active at ${params.stage} with fail-closed action=block. ` +
        `reason=${degraded.reason} source=${degraded.source}.`,
      filePath: '.pumuki/degraded-mode.json',
      matchedBy: 'DegradedModeGuard',
      source: 'degraded-mode',
    };
  }
  return {
    ruleId: 'governance.degraded-mode.active',
    severity: 'INFO',
    code: degraded.code,
    message:
      `Degraded mode is active at ${params.stage} with fail-open action=allow. ` +
      `reason=${degraded.reason} source=${degraded.source}.`,
    filePath: '.pumuki/degraded-mode.json',
    matchedBy: 'DegradedModeGuard',
    source: 'degraded-mode',
  };
};

const toGateWaiverAppliedFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  waiver: Extract<GateWaiverResult, { kind: 'applied' }>['waiver'];
}): Finding => ({
  ruleId: 'governance.waiver.applied',
  severity: 'INFO',
  code: 'GATE_WAIVER_APPLIED',
  message:
    `Gate waiver applied at ${params.stage}: waiver_id=${params.waiver.id} owner=${params.waiver.owner} ` +
    `expires_at=${params.waiver.expires_at}.`,
  filePath: '.pumuki/waivers/gate.json',
  matchedBy: 'GateWaiverGuard',
  source: 'gate-waiver',
});

const toGateWaiverExpiredFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  waiver: Extract<GateWaiverResult, { kind: 'expired' }>['waiver'];
}): Finding => ({
  ruleId: 'governance.waiver.expired',
  severity: 'ERROR',
  code: 'GATE_WAIVER_EXPIRED',
  message:
    `Gate waiver expired at ${params.stage}: waiver_id=${params.waiver.id} owner=${params.waiver.owner} ` +
    `expired_at=${params.waiver.expires_at}. Provide a valid waiver or fix blocking findings.`,
  filePath: '.pumuki/waivers/gate.json',
  matchedBy: 'GateWaiverGuard',
  source: 'gate-waiver',
});

const toGateWaiverInvalidFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  reason: string;
}): Finding => ({
  ruleId: 'governance.waiver.invalid',
  severity: 'ERROR',
  code: 'GATE_WAIVER_INVALID',
  message:
    `Gate waiver file is invalid at ${params.stage}: ${params.reason}. ` +
    'Fix waiver schema or remove invalid waiver before continuing.',
  filePath: '.pumuki/waivers/gate.json',
  matchedBy: 'GateWaiverGuard',
  source: 'gate-waiver',
});

const toPlatformSkillsCoverageBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  detectedPlatforms: DetectedPlatforms;
  activeBundles: ReadonlyArray<SkillsRuleSetLoadResult['activeBundles'][number]>;
  activeRuleIds: ReadonlyArray<string>;
  evaluatedRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  const detectedPlatformKeys = (
    ['ios', 'android', 'backend', 'frontend'] as const
  ).filter((platform) => params.detectedPlatforms[platform]?.detected === true);

  if (detectedPlatformKeys.length === 0) {
    return undefined;
  }

  const activeBundleNames = new Set(params.activeBundles.map((bundle) => bundle.name));
  const gaps: string[] = [];

  for (const platform of detectedPlatformKeys) {
    const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform];
    const missingBundles = requiredBundles.filter((bundleName) => !activeBundleNames.has(bundleName));
    const rulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
    const hasActiveRules = params.activeRuleIds.some((ruleId) => ruleId.startsWith(rulePrefix));
    const hasEvaluatedRules = params.evaluatedRuleIds.some((ruleId) => ruleId.startsWith(rulePrefix));

    if (missingBundles.length === 0 && hasActiveRules && hasEvaluatedRules) {
      continue;
    }

    const reasons: string[] = [];
    if (missingBundles.length > 0) {
      reasons.push(`missing_bundles=[${missingBundles.join(', ')}]`);
    }
    if (!hasActiveRules) {
      reasons.push(`active_rules_prefix=${rulePrefix} missing`);
    }
    if (!hasEvaluatedRules) {
      reasons.push(`evaluated_rules_prefix=${rulePrefix} missing`);
    }
    gaps.push(`${platform}{${reasons.join('; ')}}`);
  }

  if (gaps.length === 0) {
    return undefined;
  }

  return {
    ruleId: 'governance.skills.platform-coverage.incomplete',
    severity: 'ERROR',
    code: 'SKILLS_PLATFORM_COVERAGE_INCOMPLETE_HIGH',
    message:
      `Platform skills coverage incomplete at ${params.stage}: ${gaps.join(' | ')}. ` +
      'Activate required bundles and ensure platform skill rules are active/evaluated.',
    filePath: '.ai_evidence.json',
    matchedBy: 'SkillsPlatformCoverageGuard',
    source: 'skills-platform-coverage',
  };
};

const toCrossPlatformCriticalEnforcementBlockingFinding = (params: {
  stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
  detectedPlatforms: DetectedPlatforms;
  skillsRules: SkillsRuleSetLoadResult['rules'];
  evaluatedRuleIds: ReadonlyArray<string>;
}): Finding | undefined => {
  const detectedPlatformKeys = (
    ['ios', 'android', 'backend', 'frontend'] as const
  ).filter((platform) => params.detectedPlatforms[platform]?.detected === true);

  if (detectedPlatformKeys.length === 0) {
    return undefined;
  }

  return undefined;
};

const shouldBlockFromFinding = (finding: Finding | undefined): boolean => {
  if (!finding) {
    return false;
  }
  return finding.blocking !== false;
};

const isAstRuleFinding = (finding: Finding): boolean => {
  const ruleId = finding.ruleId.toLowerCase();
  const source = finding.source?.toLowerCase() ?? '';
  const matchedBy = finding.matchedBy?.toLowerCase() ?? '';
  if (ruleId.startsWith('governance.')) {
    return false;
  }
  return (
    ruleId.startsWith('skills.') ||
    ruleId.startsWith('heuristics.') ||
    source.includes('heuristic') ||
    matchedBy.includes('heuristic') ||
    matchedBy.includes('ast')
  );
};

const resolvePrimaryBlockingFinding = (
  findings: ReadonlyArray<Finding>
): Finding | undefined => (
  findings.find(
    (finding) =>
      shouldBlockFromFinding(finding) &&
      isAstRuleFinding(finding) &&
      finding.severity === 'CRITICAL'
  ) ??
  findings.find(
    (finding) =>
      shouldBlockFromFinding(finding) &&
      isAstRuleFinding(finding) &&
      finding.severity === 'ERROR'
  ) ??
  findings.find((finding) => shouldBlockFromFinding(finding) && isAstRuleFinding(finding)) ??
  findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'CRITICAL') ??
  findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'ERROR') ??
  findings.find((finding) => shouldBlockFromFinding(finding)) ??
  findings[0]
);

const resolveBlockingNotificationFindings = (
  findings: ReadonlyArray<Finding>
): ReadonlyArray<Finding> => {
  const blockingFindings = findings.filter((finding) => shouldBlockFromFinding(finding));
  const astFindings = blockingFindings.filter(isAstRuleFinding);
  const nonAstFindings = blockingFindings.filter((finding) => !isAstRuleFinding(finding));
  return [...astFindings, ...nonAstFindings];
};

const applySkillsFindingEnforcement = (
  finding: Finding | undefined
): Finding | undefined => {
  if (!finding) {
    return undefined;
  }
  return finding;
};

export async function runPlatformGate(params: {
  policy: GatePolicy;
  auditMode?: 'gate' | 'engine';
  policyTrace?: ResolvedStagePolicy['trace'];
  scope: GateScope;
  silent?: boolean;
  sddShortCircuit?: boolean;
  sddDecisionOverride?: Pick<SddDecision, 'allowed' | 'code' | 'message'>;
  services?: Partial<GateServices>;
  dependencies?: Partial<GateDependencies>;
}): Promise<number> {
  const git = params.services?.git ?? defaultServices.git;
  const evidence = params.services?.evidence ?? defaultServices.evidence;
  const dependencies: GateDependencies = {
    ...defaultDependencies,
    ...params.dependencies,
  };
  const repoRoot = git.resolveRepoRoot();
  const auditMode = params.auditMode ?? 'gate';
  const shouldShortCircuitSdd = params.sddShortCircuit ?? false;
  const contextGate: ContextGateResult = dependencies.evaluateContextGate({
    repoRoot,
    stage: params.policy.stage,
  });
  const contextBlockingFinding = contextGate.finding;
  let sddDecision:
    | Pick<SddDecision, 'allowed' | 'code' | 'message'>
    | undefined;
  let sddBlockingFinding: Finding | undefined;

  if (
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
  ) {
    sddDecision = params.sddDecisionOverride
      ?? dependencies.evaluateSddForStage(
        params.policy.stage,
        repoRoot
      );
    if (!sddDecision.allowed) {
      if (params.silent !== true) {
        process.stdout.write(`[pumuki][sdd] ${sddDecision.code}: ${sddDecision.message}\n`);
      }
      sddBlockingFinding = toSddBlockingFinding(sddDecision);
      if (shouldShortCircuitSdd) {
        const emptyDetectedPlatforms: DetectedPlatforms = {};
        const emptySkillsRuleSet: SkillsRuleSetLoadResult = {
          rules: [],
          activeBundles: [],
          mappedHeuristicRuleIds: new Set<string>(),
          requiresHeuristicFacts: false,
        };
        const emptyRuleSet: RuleSet = [];
        dependencies.emitPlatformGateEvidence({
          stage: params.policy.stage,
          auditMode,
          policyTrace: params.policyTrace,
          findings: [sddBlockingFinding],
          gateOutcome: 'BLOCK',
          filesScanned: 0,
          evaluationMetrics: createEmptyEvaluationMetrics(),
          rulesCoverage: createEmptySnapshotRulesCoverage(params.policy.stage),
          repoRoot,
          detectedPlatforms: emptyDetectedPlatforms,
          skillsRuleSet: emptySkillsRuleSet,
          projectRules: emptyRuleSet,
          heuristicRules: emptyRuleSet,
          evidenceService: evidence,
          sddDecision,
        });
        return 1;
      }
    }
  }

  const gateScopePathPrefixes = resolveGateScopePathPrefixesFromEnv();
  const facts = filterFactsByPathPrefixes(
    await dependencies.resolveFactsForGateScope({
      scope: params.scope,
      git,
    }),
    gateScopePathPrefixes
  );
  const stagedPaths = collectStagedPaths(git, repoRoot);
  const stagedChangedLinesByPath = collectStagedChangedLinesByPath(git, repoRoot);
  const factsForPlatformEvaluation = shouldAugmentStagedSkillsContractFactsWithRepoFacts({
    scope: params.scope,
    facts,
    stagedPaths,
  })
    ? [
      ...facts,
      ...(await dependencies.resolveFactsForGateScope({
        scope: {
          kind: 'repo',
        },
        git,
      })),
    ]
    : facts;
  const filesScanned = countScannedFilesFromFacts(factsForPlatformEvaluation);
  const observedCodePaths = collectObservedCodePathsFromFacts(facts);
  const preWriteLeaseFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toPreWriteEnforcementGapFinding({
          stage: params.policy.stage,
          status: readPreWriteLeaseStatus({
            repoRoot,
            git,
          }),
        })
      : undefined;

  const platformEvaluation = dependencies.evaluatePlatformGateFindings({
    facts: factsForPlatformEvaluation,
    stage: params.policy.stage,
    repoRoot,
  });
  const aiGateRepoPolicyFindings = collectAiGateRepoPolicyFindings({
    repoRoot,
    stage: params.policy.stage,
  });
  const {
    detectedPlatforms,
    skillsRuleSet,
    projectRules,
    heuristicRules,
    coverage,
    evaluationFacts = factsForPlatformEvaluation,
    findings: ruleEngineFindings,
  } = platformEvaluation;
  const scopedRuleEngineFindings = normalizeScopedRuleEngineFindings({
    findings: ruleEngineFindings,
    scope: params.scope,
    stage: params.policy.stage,
    changedLinesByPath: stagedChangedLinesByPath,
    stagedPaths,
  });
  const findings = normalizeNonBlockingSwiftTestingMigrationWarnings([
    ...aiGateRepoPolicyFindings,
    ...scopedRuleEngineFindings,
  ]);
  const evaluationMetrics: SnapshotEvaluationMetrics = coverage
    ? {
      facts_total: coverage.factsTotal,
      rules_total: coverage.rulesTotal,
      baseline_rules: coverage.baselineRules,
      heuristic_rules: coverage.heuristicRules,
      skills_rules: coverage.skillsRules,
      project_rules: coverage.projectRules,
      matched_rules: coverage.matchedRules,
      unmatched_rules: coverage.unmatchedRules,
      evaluated_rule_ids: [...coverage.evaluatedRuleIds],
      matched_rule_ids: [...coverage.matchedRuleIds],
      unmatched_rule_ids: [...coverage.unmatchedRuleIds],
    }
    : createEmptyEvaluationMetrics();
  const coverageBlockingFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toRulesCoverageBlockingFinding({
        stage: params.policy.stage,
        activeRuleIds: coverage?.activeRuleIds ?? [],
        evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [],
        unevaluatedRuleIds: coverage?.unevaluatedRuleIds ?? [],
      })
      : undefined;
  const unsupportedSkillsMappingFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toSkillsUnsupportedAutoRulesBlockingFinding({
        stage: params.policy.stage,
        unsupportedAutoRuleIds: skillsRuleSet.unsupportedAutoRuleIds ?? [],
      })
      : undefined;
  const declarativeRulesClassificationFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toSkillsDeclarativeRulesClassificationFinding({
          stage: params.policy.stage,
          declarativeRuleIds: skillsRuleSet.registryCoverage?.declarativeRuleIds ?? [],
          facts: factsForPlatformEvaluation,
        })
      : undefined;
  const effectiveUnsupportedSkillsMappingInput = applySkillsFindingEnforcement(
    unsupportedSkillsMappingFinding
  );
  const platformSkillsCoverageFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toPlatformSkillsCoverageBlockingFinding({
          stage: params.policy.stage,
          detectedPlatforms,
          activeBundles: skillsRuleSet.activeBundles,
          activeRuleIds: coverage?.activeRuleIds ?? [],
          evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [],
        })
      : undefined;
  const effectivePlatformSkillsCoverageInput = applySkillsFindingEnforcement(
    platformSkillsCoverageFinding
  );
  const crossPlatformCriticalFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toCrossPlatformCriticalEnforcementBlockingFinding({
          stage: params.policy.stage,
          detectedPlatforms,
          skillsRules: skillsRuleSet.rules,
          evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [],
        })
      : undefined;
  const effectiveCrossPlatformCriticalInput = applySkillsFindingEnforcement(
    crossPlatformCriticalFinding
  );
  const skillsScopeComplianceFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toSkillsScopeComplianceBlockingFinding({
          stage: params.policy.stage,
          facts,
          activeRuleIds: coverage?.activeRuleIds ?? [],
        evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [],
      })
      : undefined;
  const effectiveSkillsScopeComplianceInput = applySkillsFindingEnforcement(
    skillsScopeComplianceFinding
  );
  const activeRulesEmptyForCodeChangesFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toActiveRulesEmptyForCodeChangesBlockingFinding({
        stage: params.policy.stage,
        facts,
        activeRuleIds: coverage?.activeRuleIds ?? [],
      })
      : undefined;
  const iosTestsQualityFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toIosTestsQualityBlockingFinding({
          stage: params.policy.stage,
          facts,
        })
      : undefined;
  const effectiveIosTestsQualityFinding = applySkillsFindingEnforcement(
    iosTestsQualityFinding
  );
  const policyAsCodeBlockingFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toPolicyAsCodeBlockingFinding({
          stage: params.policy.stage,
          policyTrace: params.policyTrace,
        })
      : undefined;
  const degradedModeFinding =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? toDegradedModeFinding({
          stage: params.policy.stage,
          policyTrace: params.policyTrace,
        })
      : undefined;
  const astIntelligenceDualValidation:
    | AstIntelligenceDualValidationResult
    | undefined =
    params.policy.stage === 'PRE_COMMIT'
      || params.policy.stage === 'PRE_PUSH'
      || params.policy.stage === 'CI'
      ? dependencies.evaluateAstIntelligenceDualValidation({
          stage: params.policy.stage,
          skillsRules: skillsRuleSet.rules,
          facts: evaluationFacts,
          legacyFindings: findings,
        })
      : undefined;
  const astIntelligenceDualFinding = astIntelligenceDualValidation?.finding;
  if (astIntelligenceDualValidation && astIntelligenceDualValidation.mode !== 'off') {
    const summary = astIntelligenceDualValidation.summary;
    if (params.silent !== true) {
      process.stdout.write(
        `[pumuki][ast-intelligence] mode=${astIntelligenceDualValidation.mode}` +
        ` mapped_rules=${summary.mapped_rules}` +
        ` compared_rules=${summary.compared_rules}` +
        ` divergences=${summary.divergences}` +
        ` false_positives=${summary.false_positives}` +
        ` false_negatives=${summary.false_negatives}` +
        ` latency_ms=${summary.latency_ms}` +
        ` languages=[${summary.languages.join(',') || 'none'}]\n`
      );
    }
  }
  const degradedModeBlocks = params.policyTrace?.degraded?.action === 'block';
  const coverageActiveRuleIds =
    coverage && hasIosTestQualityScope(facts)
      ? appendUniqueRuleId(coverage.activeRuleIds, IOS_CRITICAL_TEST_QUALITY_RULE_ID)
      : coverage?.activeRuleIds;
  const coverageEvaluatedRuleIds =
    coverage && hasIosTestQualityScope(facts)
      ? appendUniqueRuleId(coverage.evaluatedRuleIds, IOS_CRITICAL_TEST_QUALITY_RULE_ID)
      : coverage?.evaluatedRuleIds;
  const rulesCoverage = coverage
    ? {
      stage: params.policy.stage,
      contract: skillsRuleSet.registryCoverage?.contract ?? 'AUTO_RUNTIME_RULES_FOR_STAGE',
      scope_note:
        'rules_coverage reports AUTO runtime rules applicable to this stage; it does not claim full DECLARATIVE registry execution.',
      active_rule_ids: [...(coverageActiveRuleIds ?? coverage.activeRuleIds)],
      evaluated_rule_ids: [...(coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds)],
      matched_rule_ids: [...coverage.matchedRuleIds],
      unevaluated_rule_ids: [...coverage.unevaluatedRuleIds],
      ...(skillsRuleSet.registryCoverage
        ? {
          registry_totals: skillsRuleSet.registryCoverage.registryTotals,
          stage_applicable_auto_rule_ids: [
            ...skillsRuleSet.registryCoverage.stageApplicableAutoRuleIds,
          ],
          declarative_rule_ids: [...skillsRuleSet.registryCoverage.declarativeRuleIds],
          declarative_excluded_reason:
            skillsRuleSet.registryCoverage.excludedDeclarativeReason,
        }
        : {}),
      ...((skillsRuleSet.unsupportedAutoRuleIds?.length ?? 0) > 0
        ? {
          unsupported_auto_rule_ids: [...(skillsRuleSet.unsupportedAutoRuleIds ?? [])],
        }
        : {}),
      counts: {
        active: (coverageActiveRuleIds ?? coverage.activeRuleIds).length,
        evaluated: (coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds).length,
        matched: coverage.matchedRuleIds.length,
        unevaluated: coverage.unevaluatedRuleIds.length,
        ...(skillsRuleSet.registryCoverage
          ? {
            registry_total: skillsRuleSet.registryCoverage.registryTotals.total,
            registry_auto: skillsRuleSet.registryCoverage.registryTotals.auto,
            registry_declarative:
              skillsRuleSet.registryCoverage.registryTotals.declarative,
            stage_applicable_auto:
              skillsRuleSet.registryCoverage.stageApplicableAutoRuleIds.length,
          }
          : {}),
        ...((skillsRuleSet.unsupportedAutoRuleIds?.length ?? 0) > 0
          ? {
            unsupported_auto: (skillsRuleSet.unsupportedAutoRuleIds ?? []).length,
          }
          : {}),
      },
      coverage_ratio:
        (coverageActiveRuleIds ?? coverage.activeRuleIds).length === 0
          ? 1
          : Number(
              (
                (coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds).length /
                (coverageActiveRuleIds ?? coverage.activeRuleIds).length
              ).toFixed(6)
            ),
    }
    : createEmptySnapshotRulesCoverage(params.policy.stage);
  const brownfieldHotspotFindings = dependencies.evaluateBrownfieldHotspotFindings({
    repoRoot,
    stage: params.policy.stage,
    facts,
  });
  const currentBranch = resolveCurrentBranch(git, repoRoot);
  const tddBddEvaluation = applyTddBddEnforcement(
    dependencies.enforceTddBddPolicy({
      facts,
      repoRoot,
      branch: currentBranch,
    })
  );
  const tddBddSnapshot: TddBddSnapshot | undefined = tddBddEvaluation.snapshot.scope.in_scope
    ? tddBddEvaluation.snapshot
    : undefined;
  const hasTddBddBlockingFinding = tddBddEvaluation.findings.some(
    (finding) => shouldBlockFromFinding(finding)
  );
  const hasNativeBlockingFinding = findings.some(
    (finding) => shouldBlockFromFinding(finding)
  );
  const effectiveUnsupportedSkillsMappingFinding = effectiveUnsupportedSkillsMappingInput;
  const effectivePlatformSkillsCoverageFinding = effectivePlatformSkillsCoverageInput;
  const effectiveCrossPlatformCriticalFinding = effectiveCrossPlatformCriticalInput;
  const effectiveSkillsScopeComplianceFinding = effectiveSkillsScopeComplianceInput;
  const effectiveFindings = sddBlockingFinding
    ? [
      ...(contextBlockingFinding ? [contextBlockingFinding] : []),
      sddBlockingFinding,
      ...(degradedModeFinding ? [degradedModeFinding] : []),
      ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
      ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []),
      ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []),
      ...(effectiveCrossPlatformCriticalFinding ? [effectiveCrossPlatformCriticalFinding] : []),
      ...(effectiveSkillsScopeComplianceFinding ? [effectiveSkillsScopeComplianceFinding] : []),
      ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []),
      ...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []),
      ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
      ...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
      ...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []),
      ...brownfieldHotspotFindings,
      ...tddBddEvaluation.findings,
      ...findings,
      ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
    ]
    : effectiveUnsupportedSkillsMappingFinding
      || effectivePlatformSkillsCoverageFinding
      || effectiveCrossPlatformCriticalFinding
      || effectiveSkillsScopeComplianceFinding
      || preWriteLeaseFinding
      || activeRulesEmptyForCodeChangesFinding
      || effectiveIosTestsQualityFinding
      || astIntelligenceDualFinding
      || coverageBlockingFinding
      || declarativeRulesClassificationFinding
      || brownfieldHotspotFindings.length > 0
      || policyAsCodeBlockingFinding
      || contextBlockingFinding
      || degradedModeFinding
      || tddBddEvaluation.findings.length > 0
      ? [
        ...(contextBlockingFinding ? [contextBlockingFinding] : []),
        ...(degradedModeFinding ? [degradedModeFinding] : []),
        ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
        ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []),
        ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []),
        ...(effectiveCrossPlatformCriticalFinding ? [effectiveCrossPlatformCriticalFinding] : []),
        ...(effectiveSkillsScopeComplianceFinding ? [effectiveSkillsScopeComplianceFinding] : []),
        ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []),
        ...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []),
        ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
        ...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
        ...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []),
        ...brownfieldHotspotFindings,
        ...tddBddEvaluation.findings,
        ...findings,
        ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
      ]
      : brownfieldHotspotFindings.length > 0
        ? [...brownfieldHotspotFindings, ...findings]
        : findings;
  const hasAstIntelligenceBlockingFinding = shouldBlockFromFinding(astIntelligenceDualFinding);
  const gateDecisionFindings = effectiveFindings.filter((finding) => finding.blocking !== false);
  const decision = dependencies.evaluateGate([...gateDecisionFindings], params.policy);
  const hasNonBlockingAdvisoryFinding = effectiveFindings.some(
    (finding) =>
      finding.blocking === false &&
      (finding.severity === 'WARN' ||
        finding.severity === 'ERROR' ||
        finding.severity === 'CRITICAL')
  );
  const baseGateOutcome =
    sddBlockingFinding ||
    shouldBlockFromFinding(contextBlockingFinding) ||
    degradedModeBlocks ||
    shouldBlockFromFinding(preWriteLeaseFinding) ||
    shouldBlockFromFinding(policyAsCodeBlockingFinding) ||
    shouldBlockFromFinding(effectiveUnsupportedSkillsMappingFinding) ||
    shouldBlockFromFinding(effectivePlatformSkillsCoverageFinding) ||
    shouldBlockFromFinding(effectiveCrossPlatformCriticalFinding) ||
    shouldBlockFromFinding(effectiveSkillsScopeComplianceFinding) ||
    shouldBlockFromFinding(activeRulesEmptyForCodeChangesFinding) ||
    shouldBlockFromFinding(effectiveIosTestsQualityFinding) ||
    hasAstIntelligenceBlockingFinding ||
    shouldBlockFromFinding(coverageBlockingFinding) ||
    shouldBlockFromFinding(declarativeRulesClassificationFinding) ||
    brownfieldHotspotFindings.some((finding) => shouldBlockFromFinding(finding)) ||
    hasTddBddBlockingFinding
      ? 'BLOCK'
      : (decision.outcome === 'PASS' &&
          (tddBddSnapshot?.status === 'advisory' || hasNonBlockingAdvisoryFinding)
          ? 'WARN'
          : decision.outcome);
  const gateWaiverStage =
    params.policy.stage === 'PRE_COMMIT' ||
    params.policy.stage === 'PRE_PUSH' ||
    params.policy.stage === 'CI'
      ? params.policy.stage
      : undefined;
  const gateWaiverResult = gateWaiverStage
    ? dependencies.resolveActiveGateWaiver({
        repoRoot,
        stage: gateWaiverStage,
        branch: currentBranch,
      })
    : ({
        kind: 'none',
        path: '.pumuki/waivers/gate.json',
      } as const);
  let gateWaiverFinding: Finding | undefined;
  let gateOutcome = baseGateOutcome;
  if (baseGateOutcome === 'BLOCK' && gateWaiverStage) {
    if (gateWaiverResult.kind === 'applied') {
      gateWaiverFinding = toGateWaiverAppliedFinding({
        stage: gateWaiverStage,
        waiver: gateWaiverResult.waiver,
      });
      gateOutcome = 'PASS';
    } else if (gateWaiverResult.kind === 'expired') {
      gateWaiverFinding = toGateWaiverExpiredFinding({
        stage: gateWaiverStage,
        waiver: gateWaiverResult.waiver,
      });
    } else if (gateWaiverResult.kind === 'invalid') {
      gateWaiverFinding = toGateWaiverInvalidFinding({
        stage: gateWaiverStage,
        reason: gateWaiverResult.reason,
      });
    }
  }
  const findingsWithWaiver = gateWaiverFinding
    ? [...effectiveFindings, gateWaiverFinding]
    : effectiveFindings;
  const shadowFlagRaw = process.env.PUMUKI_OPERATIONAL_MEMORY_SHADOW_ENABLED?.trim().toLowerCase();
  const isMemoryShadowEnabled = shadowFlagRaw === '1' || shadowFlagRaw === 'true';
  let memoryShadowRecommendation: OperationalMemoryShadowRecommendation | undefined;
  if (isMemoryShadowEnabled) {
    try {
      memoryShadowRecommendation = dependencies.buildMemoryShadowRecommendation({
        findings: findingsWithWaiver,
        ...(tddBddSnapshot ? { tddBddSnapshot } : {}),
      });
    } catch (error) {
      const rawReason = error instanceof Error ? error.message : String(error);
      const reason = rawReason.trim().replace(/\s+/g, ' ');
      if (params.silent !== true) {
        process.stdout.write(
          `[pumuki][memory-shadow] unavailable reason=${reason.length > 0 ? reason : 'unknown_error'}\n`
        );
      }
    }
  }
  const memoryShadow:
    | {
      recommended_outcome: GateOutcome;
      actual_outcome: GateOutcome;
      confidence: number;
      reason_codes: string[];
    }
    | undefined =
    memoryShadowRecommendation
      ? {
        recommended_outcome:
          memoryShadowRecommendation.recommendedOutcome === 'ALLOW'
            ? 'PASS'
            : memoryShadowRecommendation.recommendedOutcome,
        actual_outcome: gateOutcome,
        confidence: memoryShadowRecommendation.confidence,
        reason_codes: [...memoryShadowRecommendation.reasonCodes],
      }
      : undefined;

  if (memoryShadowRecommendation) {
    if (params.silent !== true) {
      process.stdout.write(
        `[pumuki][memory-shadow] recommended=${memoryShadowRecommendation.recommendedOutcome}` +
        ` confidence=${memoryShadowRecommendation.confidence.toFixed(2)}` +
        ` reasons=${memoryShadowRecommendation.reasonCodes.join(',')}\n`
      );
    }
  }

  dependencies.emitPlatformGateEvidence({
    stage: params.policy.stage,
    auditMode,
    policyTrace: params.policyTrace,
    findings: findingsWithWaiver,
    gateOutcome,
    filesScanned,
    evaluationMetrics,
    rulesCoverage,
    ...(tddBddSnapshot ? { tddBdd: tddBddSnapshot } : {}),
    ...(memoryShadow ? { memoryShadow } : {}),
    repoRoot,
    detectedPlatforms,
    skillsRuleSet,
    projectRules,
    heuristicRules,
    evidenceService: evidence,
    sddDecision,
  });

  if (gateOutcome === 'BLOCK') {
    const notificationFindings = resolveBlockingNotificationFindings(findingsWithWaiver);
    const primaryBlockingFinding =
      notificationFindings[0] ?? resolvePrimaryBlockingFinding(findingsWithWaiver);
    if (
      primaryBlockingFinding &&
      (
        params.policy.stage === 'PRE_COMMIT' ||
        params.policy.stage === 'PRE_PUSH' ||
        params.policy.stage === 'CI'
      )
    ) {
      const notificationResult = dependencies.notifyGateBlocked({
        repoRoot,
        stage: params.policy.stage,
        totalViolations: findingsWithWaiver.length,
        causeCode: primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId,
        causeMessage: primaryBlockingFinding.message,
        remediation:
          primaryBlockingFinding.expected_fix ??
          'Corrige la causa bloqueante y reejecuta el gate.',
        blockingCauses: notificationFindings.map((finding) => ({
          code: finding.code ?? finding.ruleId,
          message: finding.message,
          ruleId: finding.ruleId,
          file: finding.filePath,
          remediation: finding.expected_fix,
        })),
      });
      process.stderr.write(
        `[pumuki][blocked] code=${primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId} ` +
        `stage=${params.policy.stage} reason=${primaryBlockingFinding.message}\n`
      );
      for (const finding of notificationFindings) {
        process.stderr.write(
          `[pumuki][blocked-cause] code=${finding.code ?? finding.ruleId}` +
          ` rule=${finding.ruleId}` +
          ` file=${finding.filePath ?? 'n/a'}` +
          ` reason=${finding.message}` +
          ` fix=${finding.expected_fix ?? 'Corrige la causa bloqueante y reejecuta el gate.'}\n`
        );
      }
      process.stderr.write(
        `[pumuki][notification] delivered=${notificationResult.delivered ? 'yes' : 'no'} ` +
        `reason=${notificationResult.reason}\n`
      );
    }
    if (params.silent !== true) {
    dependencies.printGateFindings(findingsWithWaiver);
    }
    return 1;
  }

  return 0;
}
