/**
 * packages/extract/src/token-source/parse.ts — format-specific token parsers.
 * Exports: parseCSSTokens, parseSCSSVariables, parseDTCGTokens,
 *   parseTailwindV4Theme, deduplicateBySelector.
 * See ./INDEX.md for split rationale.
 */

import { inferCategory } from './categorize.js';
import { computeIdentityKey } from './classify.js';
import type { ParsedToken } from './types.js';

type SelectorContext = ':root' | '.dark' | 'other';

interface ContextualToken extends ParsedToken {
  selector: SelectorContext;
}

function classifySelector(selector: string): SelectorContext {
  const trimmed = selector.trim();
  if (
    trimmed === ':root' ||
    trimmed === ':root,' ||
    trimmed.startsWith(':root ')
  )
    return ':root';
  if (/\.dark\b/.test(trimmed)) return '.dark';
  return 'other';
}

export function parseCSSTokens(
  content: string,
  filePath: string,
): ContextualToken[] {
  const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '');
  const lines = stripped.split('\n');
  const tokens: ContextualToken[] = [];
  let currentSelector: SelectorContext = 'other';
  let braceDepth = 0;
  const selectorStack: Array<{ selector: SelectorContext; depth: number }> = [];

  for (let idx = 0; idx < lines.length; idx++) {
    const line = lines[idx];
    const trimmed = line.trim();

    if (trimmed.startsWith('@theme')) continue;

    const lineRegex = /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
    let match: RegExpExecArray | null;
    while ((match = lineRegex.exec(line)) !== null) {
      const value = match[2].trim();
      const name = `--${match[1]}`;
      tokens.push({
        name,
        value,
        category: inferCategory(match[1], value),
        file: filePath,
        line: idx + 1,
        tier: 'unknown',
        identityKey: computeIdentityKey(name),
        selector: currentSelector,
      });
    }

    for (let ci = 0; ci < line.length; ci++) {
      if (line[ci] === '{') {
        const before = line.substring(0, ci).trim() || trimmed;
        if (!before.startsWith('@')) {
          selectorStack.push({
            selector: classifySelector(before),
            depth: braceDepth,
          });
          currentSelector = classifySelector(before);
        }
        braceDepth++;
      } else if (line[ci] === '}') {
        braceDepth--;
        while (
          selectorStack.length &&
          selectorStack[selectorStack.length - 1].depth >= braceDepth
        ) {
          selectorStack.pop();
        }
        currentSelector = selectorStack.length
          ? selectorStack[selectorStack.length - 1].selector
          : 'other';
      }
    }
  }

  return tokens;
}

export function deduplicateBySelector(
  tokens: ContextualToken[],
): ParsedToken[] {
  const PRIORITY: Record<SelectorContext, number> = {
    ':root': 0,
    other: 1,
    '.dark': 2,
  };
  const seen = new Map<string, ContextualToken>();
  for (const token of tokens) {
    const existing = seen.get(token.name);
    if (!existing || PRIORITY[token.selector] < PRIORITY[existing.selector]) {
      seen.set(token.name, token);
    }
  }
  return [...seen.values()];
}

export function parseSCSSVariables(
  content: string,
  filePath: string,
): ParsedToken[] {
  const lines = content.split('\n');
  const tokens: ParsedToken[] = [];
  let braceDepth = 0;
  let pending:
    | {
        name: string;
        valueLines: string[];
        line: number;
      }
    | undefined;

  const pushToken = (name: string, rawValue: string, line: number) => {
    // Sass variables beginning with `_` or `-` are private module details.
    // They are often maps or implementation intermediates, not design tokens.
    if (name.startsWith('_') || name.startsWith('-')) return;

    const value = rawValue
      .replace(/!default\s*$/, '')
      .replace(/;\s*$/, '')
      .trim();
    if (!value) return;
    // Skip Sass maps. They can contain many literal values, but the map itself
    // is not a leaf token value that the UI can preview or apply.
    if (value.startsWith('(')) return;

    const fullName = `$${name}`;
    tokens.push({
      name: fullName,
      value,
      category: inferCategory(name, value),
      file: filePath,
      line,
      tier: 'unknown',
      identityKey: computeIdentityKey(fullName),
    });
  };

  lines.forEach((line, idx) => {
    const stripped = line.replace(/\/\/.*$/, '');

    if (pending) {
      pending.valueLines.push(stripped);
      if (stripped.includes(';')) {
        pushToken(pending.name, pending.valueLines.join('\n'), pending.line);
        pending = undefined;
      }
    } else if (braceDepth === 0) {
      const match = stripped.match(/^\s*\$([a-zA-Z0-9_-]+)\s*:\s*(.*)$/);
      if (match) {
        if (match[2].includes(';')) {
          pushToken(match[1], match[2], idx + 1);
        } else {
          pending = {
            name: match[1],
            valueLines: [match[2]],
            line: idx + 1,
          };
        }
      }
    }

    if (!pending) {
      for (const ch of stripped) {
        if (ch === '{') braceDepth++;
        else if (ch === '}') braceDepth = Math.max(0, braceDepth - 1);
      }
    }
  });

  return tokens;
}

function isStyleDictionaryLeaf(obj: Record<string, unknown>): boolean {
  return 'value' in obj && !('$value' in obj) && typeof obj.value === 'string';
}

export function parseDTCGTokens(
  content: string,
  filePath: string,
): ParsedToken[] {
  let parsed: unknown;
  try {
    parsed = JSON.parse(content);
  } catch {
    return [];
  }

  const tokens: ParsedToken[] = [];
  const walk = (node: unknown, path: string[]) => {
    if (!node || typeof node !== 'object' || Array.isArray(node)) return;
    const obj = node as Record<string, unknown>;
    if ('$value' in obj) {
      const name = path.join('.');
      const value =
        typeof obj.$value === 'string'
          ? obj.$value
          : JSON.stringify(obj.$value);
      tokens.push({
        name,
        value,
        category: inferCategory(path[0] ?? name, value),
        file: filePath,
        line: 0,
        tier: 'unknown',
        identityKey: computeIdentityKey(name),
      });
      return;
    }

    if (isStyleDictionaryLeaf(obj)) {
      const name = path.join('.');
      const value = obj.value as string;
      tokens.push({
        name,
        value,
        category: inferCategory(path[0] ?? name, value),
        file: filePath,
        line: 0,
        tier: 'unknown',
        identityKey: computeIdentityKey(name),
      });
      return;
    }

    for (const [key, child] of Object.entries(obj)) {
      if (key.startsWith('$')) continue;
      if (typeof child !== 'string') {
        walk(child, [...path, key]);
      }
    }
  };

  walk(parsed, []);
  return tokens;
}

export function parseTailwindV4Theme(
  content: string,
  filePath: string,
): ParsedToken[] {
  const stripped = content
    .replace(/\/\*[\s\S]*?\*\//g, '')
    .split('\n')
    .filter((line) => !line.trim().startsWith('//'))
    .join('\n');

  const tokens: ParsedToken[] = [];
  const lines = stripped.split('\n');
  let inTheme = false;
  let braceDepth = 0;
  let themeDepth = -1;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const trimmed = line.trim();

    if (!inTheme && /^@theme\b/.test(trimmed)) {
      inTheme = true;
      themeDepth = braceDepth;
    }

    for (const ch of line) {
      if (ch === '{') braceDepth++;
      else if (ch === '}') braceDepth--;
    }

    if (!inTheme) continue;
    if (braceDepth <= themeDepth) {
      inTheme = false;
      themeDepth = -1;
      continue;
    }

    const match = trimmed.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
    if (!match) continue;
    const value = match[2].replace(/;$/, '').trim();
    tokens.push({
      name: match[1],
      value,
      category: inferCategory(match[1].replace(/^--/, ''), value),
      file: filePath,
      line: i + 1,
      tier: 'unknown',
      identityKey: computeIdentityKey(match[1]),
    });
  }

  return tokens;
}
