/**
 * Tailwind v3 config formatter.
 *
 * Output shape:
 *
 *   // tailwind.tokens.js
 *   module.exports = {
 *     colors: {
 *       primary: "#4085F2",
 *       brand: { primary: "#FF00AA" }
 *     },
 *     spacing: {
 *       md: "16px"
 *     }
 *   };
 *
 * Designed to be spread into `tailwind.config.js`:
 *
 *   const tokens = require('./src/styles/generated/tailwind.tokens.js');
 *   module.exports = { theme: { extend: tokens } };
 *
 * Token-to-namespace mapping mirrors Tailwind v3's theme keys:
 *
 *   color/*       → colors.*
 *   spacing/*     → spacing.*
 *   font/*        → fontFamily.*
 *   text/*        → fontSize.*
 *   radius/*      → borderRadius.*
 *
 * Multi-mode tokens flatten to the primary mode (Tailwind v3 doesn't have
 * a native multi-mode model — dark-mode variants come from `tailwindcss`'s
 * `darkMode: 'class'` + separate CSS variable files).
 */

import type { Token, TokenDocument, TokenSet, TokenValue } from "../types.js";
import { buildTokenIndex, resolveAliasChain } from "../alias-resolver.js";
import type { FormatOptions, FormatResult } from "./index.js";

export function formatTailwindV3(
  doc: TokenDocument,
  opts: FormatOptions,
): FormatResult {
  const warnings: string[] = [];
  const files: FormatResult["files"] = [];

  const splitByCollection = opts.target.splitByCollection ?? false;
  // Tailwind v3 config is consumed by Tailwind's build step, which needs
  // literal values — no runtime alias resolution. Build a full-document
  // token index so we can resolve alias chains at export time. Even when
  // splitByCollection writes one file per set, aliases may target tokens
  // in OTHER sets (e.g. semantic → primitives) so the index must span
  // every set, not just the file's own.
  const tokenIndex = buildTokenIndex(doc);

  if (splitByCollection) {
    for (const set of doc.sets) {
      files.push({
        path: filenameFor(opts, set),
        content: renderConfigFile([set], opts, tokenIndex, warnings),
      });
    }
  } else {
    files.push({
      path: filenameFor(opts),
      content: renderConfigFile(doc.sets, opts, tokenIndex, warnings),
    });
  }

  return { files, warnings };
}

function filenameFor(opts: FormatOptions, set?: TokenSet): string {
  if (opts.target.filename) return opts.target.filename;
  if (set) return `${slugify(set.name)}.tailwind.tokens.js`;
  return "tailwind.tokens.js";
}

function slugify(s: string): string {
  return s
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function jsKey(segment: string): string {
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment)) return segment;
  return JSON.stringify(segment);
}

interface TwTree {
  [key: string]: TwTree | string;
}

function renderConfigFile(
  sets: TokenSet[],
  opts: FormatOptions,
  tokenIndex: Map<string, Token>,
  warnings: string[],
): string {
  const lines: string[] = [];
  lines.push("/**");
  lines.push(" * Generated by figma-console-mcp — do not edit by hand.");
  lines.push(" * Spread this into your tailwind.config.js theme.extend block:");
  lines.push(" *");
  lines.push(" *   const tokens = require('./path/to/this-file');");
  lines.push(" *   module.exports = { theme: { extend: tokens } };");
  lines.push(" */");
  lines.push("");

  // Group tokens by Tailwind theme namespace.
  const namespaces: Record<string, TwTree> = {};

  for (const set of sets) {
    const primaryMode = pickPrimaryMode(set.modes);
    for (const token of set.tokens) {
      const value = token.values[primaryMode];
      if (!value) continue;
      const formattedValue = formatTokenValue(value, token, primaryMode, tokenIndex, warnings);
      if (formattedValue === null) continue;
      const { namespace, subPath } = mapToNamespace(token);
      if (!namespaces[namespace]) namespaces[namespace] = {};
      writeIntoTree(namespaces[namespace], subPath, formattedValue);
    }
  }

  lines.push("module.exports = {");
  const namespaceEntries = Object.entries(namespaces).sort(([a], [b]) => a.localeCompare(b));
  for (let i = 0; i < namespaceEntries.length; i++) {
    const [ns, tree] = namespaceEntries[i];
    lines.push(`  ${jsKey(ns)}: ${renderTree(tree, 1)}${i < namespaceEntries.length - 1 ? "," : ""}`);
  }
  lines.push("};");
  lines.push("");

  return lines.join("\n");
}

function pickPrimaryMode(modes: string[]): string {
  return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
}

function mapToNamespace(token: Token): {
  namespace: string;
  subPath: string[];
} {
  const slug = token.path.map(slugify);
  if (slug.length === 0) return { namespace: "extend", subPath: [] };

  // First-segment aliases that should be re-namespaced to Tailwind theme keys.
  const NAMESPACE_MAP: Record<string, string> = {
    color: "colors",
    colors: "colors",
    spacing: "spacing",
    space: "spacing",
    radius: "borderRadius",
    "border-radius": "borderRadius",
    rounded: "borderRadius",
    font: "fontFamily",
    "font-family": "fontFamily",
    text: "fontSize",
    "font-size": "fontSize",
    "font-weight": "fontWeight",
    shadow: "boxShadow",
    "box-shadow": "boxShadow",
    opacity: "opacity",
    "z-index": "zIndex",
    "line-height": "lineHeight",
    leading: "lineHeight",
    tracking: "letterSpacing",
    "letter-spacing": "letterSpacing",
  };

  if (NAMESPACE_MAP[slug[0]]) {
    return { namespace: NAMESPACE_MAP[slug[0]], subPath: slug.slice(1) };
  }

  // Heuristic by type when no first-segment match.
  if (token.type === "color") return { namespace: "colors", subPath: slug };
  if (token.type === "dimension") return { namespace: "spacing", subPath: slug };
  if (token.type === "fontFamily") return { namespace: "fontFamily", subPath: slug };
  if (token.type === "fontWeight") return { namespace: "fontWeight", subPath: slug };
  // Anything else: dump under extend.{path}
  return { namespace: "extend", subPath: slug };
}

function writeIntoTree(tree: TwTree, path: string[], value: string): void {
  if (path.length === 0) return;
  let cursor: TwTree = tree;
  for (let i = 0; i < path.length - 1; i++) {
    const segment = path[i];
    const next = cursor[segment];
    if (!next || typeof next === "string") {
      const newNode: TwTree = {};
      cursor[segment] = newNode;
      cursor = newNode;
    } else {
      cursor = next;
    }
  }
  const leafKey = path[path.length - 1];
  cursor[leafKey] = value;
}

function renderTree(tree: TwTree, indent: number): string {
  const ind = "  ".repeat(indent);
  const innerInd = "  ".repeat(indent + 1);
  const entries: string[] = [];

  const sortedKeys = Object.keys(tree).sort();
  for (const key of sortedKeys) {
    const value = tree[key];
    const k = jsKey(key);
    if (typeof value === "string") {
      entries.push(`${innerInd}${k}: ${value}`);
    } else {
      entries.push(`${innerInd}${k}: ${renderTree(value, indent + 1)}`);
    }
  }

  if (entries.length === 0) return "{}";
  return `{\n${entries.join(",\n")},\n${ind}}`;
}

/**
 * Format a token value as a JavaScript literal (quoted string for most
 * things). Resolves alias chains to their literal target value via the
 * document-wide token index — Tailwind v3 config is read at Tailwind's
 * build time and needs concrete values (no runtime cascade).
 *
 * Returns null only for cross-library aliases or genuinely empty values,
 * after emitting a warning explaining what got skipped.
 */
function formatTokenValue(
  value: TokenValue,
  token: Token,
  mode: string,
  tokenIndex: Map<string, Token>,
  warnings: string[],
): string | null {
  let effective: TokenValue | null = value;
  if (value.reference) {
    effective = resolveAliasChain(value, mode, tokenIndex);
    if (!effective) {
      const bare = value.reference.replace(/^\{|\}$/g, "");
      const reason = bare.startsWith("__library:") || bare === "unknown"
        ? "cross-library alias"
        : "alias target not found in any set";
      warnings.push(
        `Skipped ${token.path.join(".")} in Tailwind v3 — ${reason}: ${value.reference}.`,
      );
      return null;
    }
  }
  if (!effective || effective.literal === undefined || effective.literal === null) {
    return null;
  }

  if (typeof effective.literal === "number") {
    if (token.type === "dimension") return JSON.stringify(`${effective.literal}px`);
    return String(effective.literal);
  }
  if (typeof effective.literal === "string") return JSON.stringify(effective.literal);
  if (typeof effective.literal === "boolean") return String(effective.literal);
  // Composite: emit as JSON object literal
  return JSON.stringify(effective.literal);
}
