import { existsSync } from "node:fs";
import { dirname, isAbsolute, resolve } from "node:path";
import {
  g,
  governanceConfigSchema,
  normalizeGovernanceConfig,
  type FragmentsConfig,
  type GovernanceConfig,
} from "@fragments-sdk/core";

export type GovernancePresetName = "universal" | "fragments" | "tailwind";

export type FragmentsScanConfig = Pick<
  FragmentsConfig,
  "components" | "exclude" | "include" | "recognizedClassHelpers" | "tokens" | "topology"
> & {
  govern?: GovernanceConfig;
};

export interface LoadFragmentsConfigOptions {
  rootDir: string;
  configPath?: string;
}

export interface LoadFragmentsConfigResult {
  config: FragmentsScanConfig | null;
  configPath: string | null;
  configDir: string;
}

export interface ResolveGovernancePolicyOptions {
  preset?: GovernancePresetName;
  fallback?: GovernanceConfig;
}

export interface ResolvedGovernancePolicy {
  policy: GovernanceConfig | undefined;
  source: "config" | "preset" | "fallback" | "none";
  presetsLoaded: string[];
}

export interface LoadGovernancePolicyOptions
  extends LoadFragmentsConfigOptions, ResolveGovernancePolicyOptions {}

export interface LoadedGovernancePolicy extends ResolvedGovernancePolicy {
  configPath: string | null;
  configDir: string;
}

export const FRAGMENTS_CONFIG_FILENAMES = [
  "fragments.config.ts",
  "fragments.config.tsx",
  "fragments.config.mts",
  "fragments.config.cts",
  "fragments.config.js",
  "fragments.config.jsx",
  "fragments.config.mjs",
  "fragments.config.cjs",
  "fragments.config.json",
  "segments.config.ts",
  "segments.config.js",
] as const;

export function findFragmentsConfigFile(rootDir: string, configPath?: string): string | null {
  if (configPath) {
    const candidate = isAbsolute(configPath) ? configPath : resolve(rootDir, configPath);
    return existsSync(candidate) ? candidate : null;
  }

  let current = resolve(rootDir);
  while (true) {
    for (const filename of FRAGMENTS_CONFIG_FILENAMES) {
      const candidate = resolve(current, filename);
      if (existsSync(candidate)) return candidate;
    }

    const parent = dirname(current);
    if (parent === current) return null;
    current = parent;
  }
}

export async function loadFragmentsConfig(
  options: LoadFragmentsConfigOptions
): Promise<LoadFragmentsConfigResult> {
  const rootDir = resolve(options.rootDir);
  const found = findFragmentsConfigFile(rootDir, options.configPath);
  if (!found) {
    return { config: null, configPath: null, configDir: rootDir };
  }

  const raw = await importFragmentsConfig(found);
  const config = unwrapDefaultExport(raw) as FragmentsScanConfig;

  if (!isObjectLike(config)) {
    throw new Error(`Invalid fragments config in ${found}: expected an exported object.`);
  }

  const normalized = normalizeGovernanceConfig(config) as FragmentsScanConfig;
  if (!normalized.govern) {
    return { config: normalized, configPath: found, configDir: dirname(found) };
  }

  const parsed = governanceConfigSchema.safeParse(normalized.govern);
  if (!parsed.success) {
    const errors = parsed.error.errors
      .map((error) => `  - ${error.path.join(".") || "govern"}: ${error.message}`)
      .join("\n");
    throw new Error(`Invalid govern config in ${found}:\n${errors}`);
  }

  return {
    config: { ...normalized, govern: parsed.data },
    configPath: found,
    configDir: dirname(found),
  };
}

export async function loadGovernancePolicy(
  options: LoadGovernancePolicyOptions
): Promise<LoadedGovernancePolicy> {
  const loaded = await loadFragmentsConfig(options);
  const resolved = resolveGovernancePolicy(loaded.config?.govern, options);
  return {
    ...resolved,
    configPath: loaded.configPath,
    configDir: loaded.configDir,
  };
}

export function resolveGovernancePolicy(
  configGovern: GovernanceConfig | undefined,
  options: ResolveGovernancePolicyOptions = {}
): ResolvedGovernancePolicy {
  const presetConfigs: GovernanceConfig[] = [];

  if (options.preset) {
    presetConfigs.push(governancePresetConfig(options.preset));
  }

  if (shouldApplyConfigPresets(configGovern)) {
    for (const preset of configGovern?.presets ?? []) {
      const resolved = resolveGovernancePresetName(preset);
      if (resolved) presetConfigs.push(governancePresetConfig(resolved));
    }
  }

  const explicitGovernConfig = configGovern ? { ...configGovern, presets: undefined } : undefined;
  const policy = mergeGovernanceConfigs(...presetConfigs, explicitGovernConfig) ?? options.fallback;

  return {
    policy,
    source: configGovern
      ? "config"
      : presetConfigs.length > 0
        ? "preset"
        : options.fallback
          ? "fallback"
          : "none",
    presetsLoaded: normalizePresetNames(policy?.presets ?? []),
  };
}

export function shouldApplyConfigPresets(govern: GovernanceConfig | undefined): boolean {
  if (!govern?.presets?.length) return false;
  return !govern.scales && !govern.styles?.length && !govern.jsx?.length && !govern.rules;
}

export function normalizePresetNames(presets: readonly string[]): string[] {
  return [
    ...new Set(
      presets
        .map(resolveGovernancePresetName)
        .filter((name): name is GovernancePresetName => !!name)
    ),
  ];
}

export function resolveGovernancePresetName(raw: string): GovernancePresetName | null {
  const name = raw.split("@")[0];
  switch (name) {
    case "universal":
    case "universal/recommended":
      return "universal";
    case "fragments":
    case "fragments/recommended":
      return "fragments";
    case "tailwind":
    case "tailwind/recommended":
      return "tailwind";
    default:
      return null;
  }
}

export function mergeGovernanceConfigs(
  ...configs: Array<GovernanceConfig | undefined>
): GovernanceConfig | undefined {
  const present = configs.filter((config): config is GovernanceConfig => !!config);
  if (present.length === 0) return undefined;

  const merged: GovernanceConfig = {};
  for (const config of present) {
    merged.extends = mergeStringArrays(merged.extends, config.extends);
    merged.presets = mergeStringArrays(merged.presets, normalizePresetNames(config.presets ?? []));
    merged.severity = config.severity ?? merged.severity;
    merged.scales = { ...(merged.scales ?? {}), ...(config.scales ?? {}) };
    merged.rules = { ...(merged.rules ?? {}), ...(config.rules ?? {}) };
    merged.agents = { ...(merged.agents ?? {}), ...(config.agents ?? {}) };
    merged.audit = { ...(merged.audit ?? {}), ...(config.audit ?? {}) };
    merged.runners = { ...(merged.runners ?? {}), ...(config.runners ?? {}) };
    merged.agent = { ...(merged.agent ?? {}), ...(config.agent ?? {}) };
    merged.ci = { ...(merged.ci ?? {}), ...(config.ci ?? {}) };
    merged.styles = [...(merged.styles ?? []), ...(config.styles ?? [])];
    merged.jsx = [...(merged.jsx ?? []), ...(config.jsx ?? [])];
    merged.tailwind = {
      ...(merged.tailwind ?? {}),
      ...(config.tailwind ?? {}),
      palette: {
        ...(merged.tailwind?.palette ?? {}),
        ...(config.tailwind?.palette ?? {}),
      },
    };
  }
  return merged;
}

export function governancePresetConfig(name: GovernancePresetName): GovernanceConfig {
  const base: GovernanceConfig = {
    presets: [name],
    rules: {
      "tokens/hardcoded-values": { enabled: true, severity: "warn" },
    },
    scales: {
      space: {
        kind: "scale",
        unit: "px",
        values: range(0, 256, 4),
      },
    },
    styles: [
      {
        kind: "style.rawColors.forbid",
        except: ["transparent", "currentColor", "inherit", "initial", "unset"],
        prefer: name === "fragments" ? "css-variable" : "token",
        severity: "warn",
      },
      g.styles.rawSpacing().mustMatchScale("space", {
        appliesTo: [
          "gap",
          "row-gap",
          "column-gap",
          "margin",
          "margin-top",
          "margin-right",
          "margin-bottom",
          "margin-left",
          "padding",
          "padding-top",
          "padding-right",
          "padding-bottom",
          "padding-left",
        ],
        severity: "warn",
      }),
    ],
    jsx: [
      {
        kind: "jsx.inlineStyle.forbidRaw",
        properties: [
          "color",
          "backgroundColor",
          "borderColor",
          "margin",
          "marginTop",
          "marginRight",
          "marginBottom",
          "marginLeft",
          "padding",
          "paddingTop",
          "paddingRight",
          "paddingBottom",
          "paddingLeft",
          "gap",
        ],
        severity: "warn",
      },
    ],
  };

  if (name === "fragments") {
    base.styles = [
      ...(base.styles ?? []),
      { kind: "style.cssVars.mustBeDefined", severity: "warn" },
    ];
  }

  if (name === "tailwind") {
    base.tailwind = {
      palette: {
        allow: ["brand-*", "neutral-*"],
        deny: ["red-*", "green-*", "blue-*"],
      },
    };
    // `tailwind/unknown-class` is deliberately NOT enabled here. It is a
    // closed-world strict check (any class our parser doesn't recognize is
    // "unknown"), so in real Tailwind v4 projects it false-positives on
    // plugin/imported utilities (tw-animate-css, shadcn/*) and user `@utility`
    // classes that a static list can't know about. It stays opt-in — users who
    // want strict typo-catching turn it on explicitly in their config.
  }

  return base;
}

function mergeStringArrays(
  a: readonly string[] | undefined,
  b: readonly string[] | undefined
): string[] | undefined {
  const merged = [...(a ?? []), ...(b ?? [])];
  return merged.length > 0 ? [...new Set(merged)] : undefined;
}

async function importFragmentsConfig(path: string): Promise<unknown> {
  const { createJiti } = await import("jiti");
  const jiti = createJiti(import.meta.url, {
    interopDefault: true,
    moduleCache: false,
  });
  return jiti.import(path);
}

function unwrapDefaultExport(value: unknown): unknown {
  if (isObjectLike(value) && "default" in value && value.default) return value.default;
  return value;
}

function isObjectLike(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === "object";
}

function range(start: number, end: number, step: number): number[] {
  const out: number[] = [];
  for (let value = start; value <= end; value += step) out.push(value);
  return out;
}
