/**
 * packages/extract/src/token-source/classify.ts — token provenance classification
 * and identity-based deduplication.
 *
 * Assigns a tier (canonical / extended / unknown) to each ParsedToken based on
 * framework-agnostic signals: file path, detected prefix, and known app paths.
 * Then collapses tokens that represent the same logical design decision across
 * different syntactic forms ($var, --var, dtcg.path) into a single entry.
 */

import type { ParsedToken, TokenProvenanceConfig, TokenTier } from './types.js';

const APP_PATH_SEGMENTS = new Set([
  'app',
  'apps',
  'pages',
  'routes',
  'screens',
]);

// ── Identity key ────────────────────────────────────────────────

/**
 * Compute a format-agnostic identity key for a token.
 *
 * `$fui-space-1`, `--fui-space-1`, and DTCG path `spacing.fui.space.1` all
 * refer to the same logical token. The identity key normalizes away the
 * syntax so deduplication can collapse them.
 */
export function computeIdentityKey(name: string): string {
  let key = name;
  if (key.startsWith('--')) key = key.slice(2);
  else if (key.startsWith('$')) key = key.slice(1);
  key = key.replace(/\./g, '-');
  return key.toLowerCase();
}

// ── Classification ──────────────────────────────────────────────

function normalizePath(p: string) {
  return p.replace(/^\.?\//, '').toLowerCase();
}

function fileMatchesSources(
  filePath: string,
  sources: string[] | undefined,
): boolean {
  if (!sources?.length) return false;
  const normalized = normalizePath(filePath);
  return sources.some((src) => {
    const normalizedSrc = normalizePath(src);
    return (
      normalized === normalizedSrc || normalized.endsWith(`/${normalizedSrc}`)
    );
  });
}

function isAppPath(filePath: string, appPaths?: string[]): boolean {
  const segments = filePath.toLowerCase().split('/').filter(Boolean);
  if (appPaths?.length) {
    const normalized = normalizePath(filePath);
    return appPaths.some((ap) => normalized.startsWith(normalizePath(ap)));
  }
  return segments.some((s) => APP_PATH_SEGMENTS.has(s));
}

function nameMatchesPrefix(name: string, prefix: string | undefined): boolean {
  if (!prefix) return false;
  const normalizedPrefix = prefix.replace(/^[-$]+/, '').toLowerCase();
  if (!normalizedPrefix) return false;
  let normalizedName = name;
  if (normalizedName.startsWith('--')) normalizedName = normalizedName.slice(2);
  else if (normalizedName.startsWith('$'))
    normalizedName = normalizedName.slice(1);
  return normalizedName.toLowerCase().startsWith(normalizedPrefix);
}

/**
 * Classify a single token. Rules are applied in priority order:
 *
 * 1. Primary source file + prefix match → canonical
 * 2. Primary source file, no prefix match → extended (build intermediate)
 * 3. Any configured source file → extended (secondary source)
 * 4. App path → extended (consumer override)
 * 5. Prefix match in any file → canonical (DS token defined elsewhere)
 * 6. Fallback → unknown
 */
function classifyOne(
  token: ParsedToken,
  config: TokenProvenanceConfig,
): TokenTier {
  const inPrimary = fileMatchesSources(token.file, config.primarySources);
  const prefixMatch = nameMatchesPrefix(token.name, config.prefix);

  if (inPrimary && prefixMatch) return 'canonical';
  if (inPrimary) return 'extended';

  const inAnySource = fileMatchesSources(token.file, config.allSources);
  if (inAnySource) return 'extended';

  if (isAppPath(token.file, config.appPaths)) return 'extended';

  if (prefixMatch) return 'canonical';

  return 'unknown';
}

/**
 * Assign `tier` to every token in the array (mutates in place).
 * When `config` is undefined or empty, all tokens stay 'unknown'.
 */
export function classifyTokenTiers(
  tokens: ParsedToken[],
  config?: TokenProvenanceConfig,
): void {
  if (!config) return;
  const hasAnySignal =
    config.primarySources?.length ||
    config.allSources?.length ||
    config.prefix ||
    config.appPaths?.length;
  if (!hasAnySignal) return;

  for (const token of tokens) {
    token.tier = classifyOne(token, config);
  }
}

// ── Identity deduplication ──────────────────────────────────────

const TIER_PRIORITY: Record<TokenTier, number> = {
  canonical: 3,
  extended: 2,
  unknown: 1,
};

const FORMAT_PRIORITY: Record<string, number> = {
  css: 3,
  scss: 2,
  dtcg: 1,
};

function tokenFormat(name: string): string {
  if (name.startsWith('--')) return 'css';
  if (name.startsWith('$')) return 'scss';
  return 'dtcg';
}

/**
 * Collapse tokens that represent the same logical design decision.
 *
 * When `$fui-space-1` (SCSS) and `--fui-space-1` (CSS) both exist, keep
 * the one with the higher tier, breaking ties by format preference
 * (CSS var > SCSS > DTCG path) since that's the runtime-facing form.
 */
export function deduplicateByIdentity(tokens: ParsedToken[]): ParsedToken[] {
  const byKey = new Map<string, ParsedToken>();
  for (const token of tokens) {
    const key = token.identityKey;
    const existing = byKey.get(key);
    if (!existing) {
      byKey.set(key, token);
      continue;
    }
    const tierDelta = TIER_PRIORITY[token.tier] - TIER_PRIORITY[existing.tier];
    if (tierDelta > 0) {
      byKey.set(key, token);
      continue;
    }
    if (tierDelta === 0) {
      const fmtDelta =
        (FORMAT_PRIORITY[tokenFormat(token.name)] ?? 0) -
        (FORMAT_PRIORITY[tokenFormat(existing.name)] ?? 0);
      if (fmtDelta > 0) {
        byKey.set(key, token);
      }
    }
  }
  return [...byKey.values()];
}
