/**
 * packages/extract/src/token-source/detect-hardcoded.ts — hardcoded value detection + token scoring.
 * Exports: detectHardcodedValues, TokenCandidate.
 * Multi-signal scoring picks the best suggested token for each hardcoded value.
 */

import { inferCategory, normalizeColorValue } from './categorize.js';
import type { HardcodedFinding } from './types.js';

const HEX_COLOR = /#[0-9a-f]{3,8}/i;
const PIXEL_VALUE = /\b\d+px\b/;
const REM_VALUE = /\b\d+(\.\d+)?rem\b/;
const NAMED_COLORS = new Set([
  'red',
  'blue',
  'green',
  'yellow',
  'orange',
  'purple',
  'pink',
  'black',
  'white',
  'gray',
  'grey',
  'cyan',
  'magenta',
  'brown',
  'navy',
  'teal',
  'aqua',
  'lime',
  'maroon',
  'olive',
  'silver',
  'fuchsia',
  'coral',
  'salmon',
  'tomato',
  'gold',
  'indigo',
  'violet',
  'crimson',
  'khaki',
]);
const TRIVIAL_VALUES = new Set([
  '0',
  'none',
  'inherit',
  'transparent',
  'auto',
  'initial',
  'unset',
  'currentcolor',
]);

export interface TokenCandidate {
  name: string;
  category: string;
}

// Multi-signal ranking so that `padding: 2rem` suggests a spacing
// token instead of whichever `2rem` token was parsed first.
const CSS_PROPERTY_CATEGORIES: Record<string, string> = {
  padding: 'spacing',
  'padding-top': 'spacing',
  'padding-right': 'spacing',
  'padding-bottom': 'spacing',
  'padding-left': 'spacing',
  'padding-inline': 'spacing',
  'padding-block': 'spacing',
  margin: 'spacing',
  'margin-top': 'spacing',
  'margin-right': 'spacing',
  'margin-bottom': 'spacing',
  'margin-left': 'spacing',
  'margin-inline': 'spacing',
  'margin-block': 'spacing',
  gap: 'spacing',
  'row-gap': 'spacing',
  'column-gap': 'spacing',
  inset: 'spacing',
  top: 'spacing',
  right: 'spacing',
  bottom: 'spacing',
  left: 'spacing',
  'font-size': 'typography',
  'line-height': 'typography',
  'letter-spacing': 'typography',
  'word-spacing': 'typography',
  'font-weight': 'typography',
  'font-family': 'typography',
  color: 'color',
  'background-color': 'color',
  'border-color': 'color',
  'outline-color': 'color',
  fill: 'color',
  stroke: 'color',
  'text-decoration-color': 'color',
  'accent-color': 'color',
  'caret-color': 'color',
  'border-radius': 'radius',
  'border-top-left-radius': 'radius',
  'border-top-right-radius': 'radius',
  'border-bottom-left-radius': 'radius',
  'border-bottom-right-radius': 'radius',
  width: 'sizing',
  height: 'sizing',
  'min-width': 'sizing',
  'min-height': 'sizing',
  'max-width': 'sizing',
  'max-height': 'sizing',
  'flex-basis': 'sizing',
  'box-shadow': 'shadow',
  'text-shadow': 'shadow',
  'border-width': 'border',
  'border-top-width': 'border',
  'border-right-width': 'border',
  'border-bottom-width': 'border',
  'border-left-width': 'border',
  'outline-width': 'border',
  'stroke-width': 'border',
  'z-index': 'z-index',
  'animation-duration': 'animation',
  'animation-delay': 'animation',
  'transition-duration': 'animation',
  'transition-delay': 'animation',
};

const PROPERTY_NAME_STEMS: Record<string, RegExp> = {
  spacing: /space|gap|pad|margin|gutter|indent|offset/i,
  typography: /font|text|heading|body|type|letter|line-height|display/i,
  color: /color|bg|background|fill|stroke|tint|hue/i,
  radius: /radius|round|corner/i,
  shadow: /shadow|elevation|depth/i,
  sizing: /size|width|height|dimension/i,
  border: /border|outline|stroke-w|divider/i,
  animation: /duration|delay|timing|ease|speed/i,
  'z-index': /z-index|layer|stack|overlay/i,
};

const CONTRADICTING_STEMS: Record<string, RegExp> = {
  spacing: /font|text|heading|shadow|color|radius/i,
  typography: /space|gap|pad|margin|shadow|radius|border-w/i,
  color: /space|gap|pad|margin|font-size|radius|shadow/i,
  radius: /space|gap|pad|margin|font|shadow|color/i,
};

function categoryForProperty(cssProperty: string): string | null {
  const direct = CSS_PROPERTY_CATEGORIES[cssProperty];
  if (direct) return direct;
  for (const [prop, cat] of Object.entries(CSS_PROPERTY_CATEGORIES)) {
    if (cssProperty.startsWith(prop)) return cat;
  }
  return null;
}

function scoreTokenForProperty(
  token: TokenCandidate,
  cssProperty: string,
): number {
  let score = 0;
  const expectedCategory = categoryForProperty(cssProperty);
  if (!expectedCategory) return 0;

  if (token.category === expectedCategory) {
    score += 10;
  } else if (
    (expectedCategory === 'spacing' && token.category === 'sizing') ||
    (expectedCategory === 'sizing' && token.category === 'spacing')
  ) {
    score += 3;
  }

  const positive = PROPERTY_NAME_STEMS[expectedCategory];
  if (positive && positive.test(token.name)) {
    score += 5;
  }

  const negative = CONTRADICTING_STEMS[expectedCategory];
  if (negative && negative.test(token.name)) {
    score -= 8;
  }

  return score;
}

function pickBestToken(
  tokens: TokenCandidate[],
  cssProperty: string,
): TokenCandidate {
  if (tokens.length === 1) return tokens[0];
  let best = tokens[0];
  let bestScore = scoreTokenForProperty(best, cssProperty);
  for (let i = 1; i < tokens.length; i++) {
    const s = scoreTokenForProperty(tokens[i], cssProperty);
    if (s > bestScore) {
      best = tokens[i];
      bestScore = s;
    }
  }
  return best;
}

export function detectHardcodedValues(
  content: string,
  filePath: string,
  tokenLookup: Map<string, TokenCandidate[]>,
) {
  const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '');
  const findings: HardcodedFinding[] = [];

  stripped.split('\n').forEach((line, index) => {
    const trimmed = line.trim();
    if (/--[\w-]+\s*:/.test(trimmed) || /^\$[\w-]+\s*:/.test(trimmed)) return;
    if (
      /^\s*[\w-]+\s*:\s*var\(/.test(trimmed) ||
      /^\s*[\w-]+\s*:\s*\$[\w-]+/.test(trimmed)
    ) {
      return;
    }

    const declMatch = trimmed.match(/^([\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
    if (!declMatch) return;

    const property = declMatch[1];
    const rawValue = declMatch[2];
    if (/^var\(/.test(rawValue) || /^\$[\w-]+/.test(rawValue)) return;

    const candidates: Array<{ raw: string; normalized: string }> = [];
    const hexMatch = rawValue.match(HEX_COLOR);
    if (hexMatch) {
      candidates.push({
        raw: hexMatch[0],
        normalized: normalizeColorValue(hexMatch[0]),
      });
    }
    const lowerValue = rawValue.toLowerCase().trim();
    if (NAMED_COLORS.has(lowerValue)) {
      candidates.push({ raw: rawValue.trim(), normalized: lowerValue });
    }
    const pxMatch = rawValue.match(PIXEL_VALUE);
    if (pxMatch) {
      candidates.push({
        raw: pxMatch[0],
        normalized: pxMatch[0].toLowerCase(),
      });
    }
    const remMatch = rawValue.match(REM_VALUE);
    if (remMatch) {
      candidates.push({
        raw: remMatch[0],
        normalized: remMatch[0].toLowerCase(),
      });
    }

    for (const { raw, normalized } of candidates) {
      if (TRIVIAL_VALUES.has(normalized)) continue;
      const matchingTokens = tokenLookup.get(normalized);
      if (!matchingTokens?.length) continue;
      const best = pickBestToken(matchingTokens, property);
      findings.push({
        file: filePath,
        line: index + 1,
        property,
        hardcodedValue: raw,
        suggestedToken: best.name,
        suggestedTokenValue: normalized,
        category: inferCategory(property),
        fingerprint: `hardcoded::${filePath}::${property}::${normalized}`,
      });
    }
  });

  return findings;
}
