import { relative, resolve } from "node:path";
import {
  compileComponentFacts,
  compileGlobalGovernanceFacts,
  FactIndex,
  factId,
  normalizeFinding,
  normalizeSeverity,
  runRules,
  severityLevel,
} from "@fragments-sdk/core";
import type {
  ComponentId,
  Fact,
  FactEvidence,
  FactLocation,
  Finding,
  GovernanceConfig,
  GovernedFragmentDefinition,
  ResolvedGovernedFragmentDefinition,
  Severity,
  SuppressionDirectiveFact,
} from "@fragments-sdk/core";

import { extractJsxFacts, type ComponentImportResolution } from "../usage/jsx.js";
import { extractScssFacts } from "../usage/scss.js";
import { extractCssInJsFacts } from "../usage/css-in-js.js";
import { extractStyleBlockFacts } from "../usage/style-blocks.js";
import { resolveTailwindTokenFacts } from "../tailwind/index.js";
import { parseSuppressionDirectives } from "../ucf/directives.js";
import { normalizePresetNames } from "./policy.js";

export interface PrebuiltFragment {
  componentId: ComponentId;
  fragment: GovernedFragmentDefinition | ResolvedGovernedFragmentDefinition;
}

export interface ScanFacts {
  governConfig?: GovernanceConfig;
  components?: readonly PrebuiltFragment[];
  tokens?: readonly Fact[];
  resolveComponentId?: (resolution: ComponentImportResolution) => ComponentId | null | undefined;
  recognizedClassHelpers?: readonly string[];
}

export interface ScanFile {
  absPath: string;
  relPath: string;
  content: string;
}

export interface ClassnameSummary {
  classnamesScanned: number;
  classnamesDynamic: number;
  classnamesResolved: number;
}

export interface CompilerScanResult {
  findings: Finding[];
  suppressedFindings: Array<{ finding: Finding; directive: SuppressionDirectiveFact }>;
  suppressions: SuppressionDirectiveFact[];
  filesScanned: string[];
  sourceByFile: ReadonlyMap<string, string>;
  exitCode: 0 | 1;
  failOnWarnings: boolean;
  policy: GovernancePolicyMetadata;
  classnames: ClassnameSummary;
}

export interface GovernancePolicyMetadata {
  rulesActive: number;
  presetsLoaded: string[];
}

export interface RunForFileOptions {
  absPath: string;
  content: string;
  rootDir: string;
  policy?: GovernanceConfig;
  facts?: Omit<ScanFacts, "governConfig">;
}

const SCRIPT_EXTENSIONS = new Set([".tsx", ".jsx", ".ts", ".js"]);
const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
const STYLE_EXTENSIONS = new Set([".scss", ".css"]);
const STYLE_BLOCK_EXTENSIONS = new Set([".vue", ".svelte"]);

export function runForFile(options: RunForFileOptions): Finding[] {
  const rootDir = resolve(options.rootDir);
  return scanWithFacts({
    facts: {
      ...options.facts,
      governConfig: options.policy,
    },
    files: [
      {
        absPath: options.absPath,
        relPath: relative(rootDir, options.absPath),
        content: options.content,
      },
    ],
  }).findings;
}

export function scanWithFacts(input: {
  facts: ScanFacts;
  files: readonly ScanFile[];
}): CompilerScanResult {
  const ix = new FactIndex();
  const policyFacts = input.facts.governConfig
    ? compileGlobalGovernanceFacts(input.facts.governConfig)
    : [];

  ix.addMany(policyFacts);

  for (const { componentId, fragment } of input.facts.components ?? []) {
    ix.addMany(compileComponentFacts(componentId, fragment));
  }

  if (input.facts.tokens) {
    ix.addMany(input.facts.tokens as Iterable<Fact>);
  }

  const filesScanned: string[] = [];
  const suppressions: SuppressionDirectiveFact[] = [];
  for (const file of input.files) {
    const ext = fileExtension(file.absPath);
    try {
      if (SCRIPT_EXTENSIONS.has(ext)) {
        if (JSX_EXTENSIONS.has(ext)) {
          ix.addMany(
            extractJsxFacts(file.content, file.relPath, {
              resolveComponentId: input.facts.resolveComponentId,
              recognizedClassHelpers: input.facts.recognizedClassHelpers,
            })
          );
        }
        ix.addMany(extractCssInJsFacts(file.content, file.relPath));
        const fileSuppressions = parseSuppressionDirectives({
          filePath: file.relPath,
          source: file.content,
        });
        suppressions.push(...fileSuppressions);
        ix.addMany(fileSuppressions);
        filesScanned.push(file.relPath);
      } else if (STYLE_EXTENSIONS.has(ext)) {
        ix.addMany(extractScssFacts(file.content, file.relPath));
        const fileSuppressions = parseSuppressionDirectives({
          filePath: file.relPath,
          source: file.content,
        });
        suppressions.push(...fileSuppressions);
        ix.addMany(fileSuppressions);
        filesScanned.push(file.relPath);
      } else if (STYLE_BLOCK_EXTENSIONS.has(ext)) {
        ix.addMany(extractStyleBlockFacts(file.content, file.relPath));
        const fileSuppressions = parseSuppressionDirectives({
          filePath: file.relPath,
          source: file.content,
        });
        suppressions.push(...fileSuppressions);
        ix.addMany(fileSuppressions);
        filesScanned.push(file.relPath);
      }
    } catch (error) {
      console.warn(
        `Governance scan skipped ${file.relPath}: ${
          error instanceof Error ? error.message : String(error)
        }`
      );
    }
  }

  ix.addMany(resolveTailwindTokenFacts(ix));

  const rawFindings = applyFindingOverrides(runRules(ix), input.facts.governConfig);
  const suppressed = applySuppressions(rawFindings, suppressions);
  const findings = [
    ...suppressed.findings,
    ...suppressionDiagnostics(suppressions, suppressed.matchedDirectiveIds),
  ];
  const failOnWarnings = input.facts.governConfig?.ci?.failOnWarnings === true;
  const exitCode =
    findings.some((f) => severityLevel(f.severity) === "error") ||
    (failOnWarnings && findings.some((f) => severityLevel(f.severity) === "warn"))
      ? 1
      : 0;
  const sourceByFile = new Map<string, string>();
  for (const file of input.files) {
    if (filesScanned.includes(file.relPath)) {
      sourceByFile.set(file.relPath, file.content);
    }
  }

  return {
    findings,
    suppressedFindings: suppressed.suppressedFindings,
    suppressions,
    filesScanned,
    sourceByFile,
    exitCode,
    failOnWarnings,
    policy: summarizeGovernancePolicy(input.facts.governConfig, policyFacts),
    classnames: summarizeClassnameFacts(ix),
  };
}

function fileExtension(path: string): string {
  const dot = path.lastIndexOf(".");
  return dot < 0 ? "" : path.slice(dot).toLowerCase();
}

function summarizeClassnameFacts(ix: FactIndex): ClassnameSummary {
  let classnamesScanned = 0;
  for (const fact of ix.byKind("classname_literal")) {
    classnamesScanned += fact.classes.length;
  }
  return {
    classnamesScanned,
    classnamesDynamic: ix.byKind("classname_dynamic").length,
    classnamesResolved: ix
      .byKind("tailwind_token_resolved")
      .filter((fact) => fact.resolved.kind !== "unknown").length,
  };
}

type SeverityOverride = Severity | "error" | "warn" | "warning" | "info";
interface RuleOverrideRecord {
  enabled?: boolean;
  severity?: SeverityOverride;
}

const POLICY_RULE_FAMILIES: Record<string, readonly string[]> = {
  "tokens/hardcoded-values": [
    "styles/no-raw-color",
    "styles/no-raw-spacing",
    "styles/no-raw-typography",
    "tokens/require-dual-fallback",
    "theme/no-theme-coupled-literal",
  ],
  "components/usage": [
    "components/forbidden-prop-value",
    "components/preferred-component",
    "components/unknown-prop",
    "props/invalid-value",
  ],
  "a11y/wcag": ["a11y/required-accessible-name"],
};

function applyFindingOverrides(
  findings: Finding[],
  governConfig: GovernanceConfig | undefined
): Finding[] {
  if (!governConfig) return findings;

  const globalSeverity = parseSeverity(governConfig.severity);
  const rules = normalizeRuleOverrides(governConfig.rules);
  if (!globalSeverity && rules.size === 0) return findings;

  const out: Finding[] = [];
  for (const finding of findings) {
    const rule = rules.get(finding.ruleId);
    if (rule?.enabled === false) continue;

    const severity = parseSeverity(rule?.severity) ?? globalSeverity;
    out.push(severity ? { ...finding, severity } : finding);
  }
  return out;
}

function normalizeRuleOverrides(
  rules: Record<string, unknown> | undefined
): Map<string, { enabled?: boolean; severity?: SeverityOverride }> {
  const out = new Map<string, { enabled?: boolean; severity?: SeverityOverride }>();
  if (!rules) return out;

  for (const [id, raw] of Object.entries(rules)) {
    const override = normalizeRuleOverride(raw);
    if (!override) continue;

    const ruleIds = POLICY_RULE_FAMILIES[id] ?? [id];
    for (const ruleId of ruleIds) {
      out.set(ruleId, override);
    }
  }

  return out;
}

function normalizeRuleOverride(
  raw: unknown
): { enabled?: boolean; severity?: SeverityOverride } | null {
  if (typeof raw === "string") {
    const severity = normalizeSeverityAlias(raw);
    return severity ? { severity } : null;
  }
  if (!raw || typeof raw !== "object") return null;

  const record = raw as RuleOverrideRecord;
  const enabled = typeof record.enabled === "boolean" ? record.enabled : undefined;
  const severity = normalizeSeverityAlias(record.severity);
  if (enabled === undefined && severity === undefined) return null;
  return { enabled, severity };
}

function normalizeSeverityAlias(value: unknown): SeverityOverride | undefined {
  if (value === "critical" || value === "serious" || value === "moderate" || value === "minor") {
    return value;
  }
  if (value === "error" || value === "warn" || value === "info") return value;
  if (value === "warning") return "warning";
  return undefined;
}

function parseSeverity(value: unknown): Severity | undefined {
  const severity = normalizeSeverityAlias(value);
  return severity ? normalizeSeverity(severity) : undefined;
}

function summarizeGovernancePolicy(
  governConfig: GovernanceConfig | undefined,
  policyFacts: readonly Fact[]
): GovernancePolicyMetadata {
  return {
    // Count enforcement-bearing policy facts only. Scales, scale values, and the
    // token vocabulary (`token_definition`) are inputs to rules, not rules — so
    // a project with many tokens no longer inflates "loaded N rules", and the
    // flag-path and config-path resolve to the same honest count (BUG 2/16).
    rulesActive: policyFacts.filter(
      (fact) =>
        fact.kind !== "scale" && fact.kind !== "scale_value" && fact.kind !== "token_definition"
    ).length,
    presetsLoaded: normalizePresetNames(governConfig?.presets ?? []),
  };
}

function applySuppressions(
  findings: readonly Finding[],
  directives: readonly SuppressionDirectiveFact[]
): {
  findings: Finding[];
  suppressedFindings: Array<{ finding: Finding; directive: SuppressionDirectiveFact }>;
  matchedDirectiveIds: Set<string>;
} {
  const active = directives.filter((directive) => !isExpired(directive));
  const matchedDirectiveIds = new Set<string>();
  const suppressedFindings: Array<{ finding: Finding; directive: SuppressionDirectiveFact }> = [];
  const remaining: Finding[] = [];

  for (const finding of findings) {
    const directive = active.find((candidate) => matchesSuppression(candidate, finding));
    if (!directive) {
      remaining.push(finding);
      continue;
    }
    matchedDirectiveIds.add(directive.id);
    suppressedFindings.push({ finding, directive });
  }

  return { findings: remaining, suppressedFindings, matchedDirectiveIds };
}

function matchesSuppression(directive: SuppressionDirectiveFact, finding: Finding): boolean {
  if (finding.code !== directive.code) return false;
  if (normalizePath(finding.location.file) !== normalizePath(directive.file)) return false;
  if (!directive.attachedLine) return true;
  return finding.location.line === directive.attachedLine;
}

function suppressionDiagnostics(
  directives: readonly SuppressionDirectiveFact[],
  matchedDirectiveIds: ReadonlySet<string>
): Finding[] {
  const diagnostics: Finding[] = [];
  for (const directive of directives) {
    if (isExpired(directive)) {
      diagnostics.push(
        systemFinding("expired-suppression", "serious", directive, "Suppression has expired")
      );
      continue;
    }
    if (!matchedDirectiveIds.has(directive.id)) {
      diagnostics.push(
        systemFinding(
          "unused-suppression",
          "minor",
          directive,
          "Suppression did not match a finding"
        )
      );
    }
    // The `fragments-allow` flavor has no expires syntax — flagging it for a
    // missing expiry would make every valid allow-directive emit noise.
    if (!directive.expiresOn && directive.directive !== "allow") {
      diagnostics.push(
        systemFinding("missing-expiry", "minor", directive, "Suppression is missing an expiry date")
      );
    }
  }
  return diagnostics;
}

function systemFinding(
  ruleId: "unused-suppression" | "expired-suppression" | "missing-expiry",
  severity: Severity,
  directive: SuppressionDirectiveFact,
  message: string
): Finding {
  const evidence: FactEvidence[] = [{ factId: directive.id, fact: directive }];
  const location: FactLocation = {
    file: directive.file,
    line: directive.line,
    column: directive.column,
  };
  return normalizeFinding({
    ruleId,
    ruleVersion: "typestyle-suppressions:v1",
    severity,
    message: `${message} for ${directive.code}.`,
    fingerprint: factId(ruleId, {
      file: directive.file,
      line: directive.line,
      column: directive.column,
      code: directive.code,
    }),
    location,
    evidence,
    attributes: {
      suppressedCode: directive.code,
      reason: directive.reason,
      expiresOn: directive.expiresOn,
    },
  });
}

function isExpired(directive: SuppressionDirectiveFact): boolean {
  if (!directive.expiresOn) return false;
  const today = new Date().toISOString().slice(0, 10);
  return directive.expiresOn < today;
}

function normalizePath(path: string): string {
  return path.replace(/\\/g, "/").replace(/^\.\//, "");
}
