/**
 * Tailwind v4 `@theme` block formatter.
 *
 * Tailwind v4 introduced CSS-first configuration: instead of a JS
 * `tailwind.config.js`, you write tokens directly into your CSS with an
 * `@theme inline { ... }` block. Tailwind processes that block to generate
 * utility classes (`bg-primary`, `text-foreground`, etc.) at build time.
 *
 * Output shape:
 *
 *   @theme inline {
 *     --color-primary: #4085F2;
 *     --spacing-md: 16px;
 *     --font-family-sans: "Inter", sans-serif;
 *   }
 *
 *   .dark {
 *     --color-primary: #5599FF;
 *   }
 *
 * Token-to-Tailwind-namespace mapping follows the canonical Tailwind v4
 * convention (see https://tailwindcss.com/docs/theme):
 *
 *   color/*       → --color-*       (maps to bg-*, text-*, border-*, etc.)
 *   spacing/*     → --spacing-*
 *   radius/*      → --radius-*      (maps to rounded-*)
 *   font/*        → --font-*        (font-family namespace)
 *   text/*        → --text-*        (font-size namespace)
 *   font-weight/* → --font-weight-*
 *   tracking/*    → --tracking-*
 *   leading/*     → --leading-*
 *   shadow/*      → --shadow-*
 *
 * Tokens that don't fit a known Tailwind namespace get emitted with their
 * path verbatim (with prefix if configured). They're still valid CSS vars
 * — they just don't generate Tailwind utility classes.
 */

import type { Token, TokenDocument, TokenSet, TokenValue } from "../types.js";
import type { FormatOptions, FormatResult } from "./index.js";

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

  const splitByMode = opts.target.splitByMode ?? false;
  const splitByCollection = opts.target.splitByCollection ?? false;
  const prefix = opts.target.prefix ?? "";

  if (splitByMode && splitByCollection) {
    for (const set of doc.sets) {
      for (const mode of set.modes) {
        files.push({
          path: filenameFor(opts, set, mode),
          content: renderTailwindFile(
            doc.sets.filter((s) => s.name === set.name),
            [mode],
            prefix,
            warnings,
          ),
        });
      }
    }
  } else if (splitByMode) {
    const allModes = new Set<string>();
    for (const set of doc.sets) for (const m of set.modes) allModes.add(m);
    for (const mode of allModes) {
      files.push({
        path: filenameFor(opts, undefined, mode),
        content: renderTailwindFile(doc.sets, [mode], prefix, warnings),
      });
    }
  } else if (splitByCollection) {
    for (const set of doc.sets) {
      files.push({
        path: filenameFor(opts, set),
        content: renderTailwindFile([set], set.modes, prefix, warnings),
      });
    }
  } else {
    // Single file with everything.
    const allModes = new Set<string>();
    for (const set of doc.sets) for (const m of set.modes) allModes.add(m);
    files.push({
      path: filenameFor(opts),
      content: renderTailwindFile(doc.sets, [...allModes], prefix, warnings),
    });
  }

  return { files, warnings };
}

function filenameFor(
  opts: FormatOptions,
  set?: TokenSet,
  mode?: string,
): string {
  if (opts.target.filename) return opts.target.filename;
  const parts: string[] = [];
  if (set) parts.push(slugify(set.name));
  if (mode) parts.push(slugify(mode));
  if (parts.length === 0) parts.push("tailwind.theme");
  return `${parts.join(".")}.css`;
}

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

/**
 * Render a Tailwind v4 CSS file. The "primary" mode (first in the list)
 * goes into `@theme inline { ... }` — this is what Tailwind reads to
 * generate utility classes. Additional modes go under standard CSS
 * selectors (`.dark`, `[data-theme="..."]`) so the user can swap modes
 * at runtime.
 */
function renderTailwindFile(
  sets: TokenSet[],
  modes: string[],
  prefix: string,
  warnings: string[],
): string {
  const lines: string[] = [];
  lines.push("/* Generated by figma-console-mcp — do not edit by hand */");
  lines.push("");

  // Decide which mode is the "primary" (lives inside @theme).
  const primaryMode =
    modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];

  // @theme inline { ... } block for the primary mode.
  lines.push("@theme inline {");
  for (const set of sets) {
    if (!set.modes.includes(primaryMode)) continue;
    for (const token of set.tokens) {
      const value = token.values[primaryMode];
      if (!value) continue;
      emitTailwindTokenLines(token, value, prefix, lines, warnings);
    }
  }
  lines.push("}");
  lines.push("");

  // Mode variants under standard CSS selectors.
  for (const mode of modes) {
    if (mode === primaryMode) continue;
    lines.push(`${selectorFor(mode)} {`);
    for (const set of sets) {
      if (!set.modes.includes(mode)) continue;
      for (const token of set.tokens) {
        const value = token.values[mode];
        if (!value) continue;
        emitTailwindTokenLines(token, value, prefix, lines, warnings);
      }
    }
    lines.push("}");
    lines.push("");
  }

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

function selectorFor(mode: string): string {
  const lower = mode.toLowerCase();
  if (lower === "dark") return ".dark";
  return `[data-theme="${slugify(mode)}"]`;
}

/**
 * Emit one or more CSS custom property declarations for a token, using
 * Tailwind v4's namespace conventions where possible.
 */
function emitTailwindTokenLines(
  token: Token,
  value: TokenValue,
  prefix: string,
  out: string[],
  warnings: string[],
): void {
  const cssName = `--${prefix}${pathToTailwindName(token.path, token.type)}`;

  if (value.reference) {
    // Cross-library alias: skip with a comment.
    const bareRef = value.reference.replace(/^\{|\}$/g, "");
    const libMatch = bareRef.match(/^__library:(.+)$/);
    if (libMatch || bareRef === "unknown") {
      const originalId = libMatch ? libMatch[1] : "unknown";
      warnings.push(
        `Skipped ${token.path.join(".")} in Tailwind v4 — references cross-library variable ${originalId}.`,
      );
      out.push(
        `  /* ${cssName}: skipped — cross-library alias to ${originalId} */`,
      );
      return;
    }
    // Local alias → var() reference using the same namespace mapping.
    const refPath = bareRef.split(".");
    const targetName = pathToTailwindName(refPath, token.type);
    out.push(`  ${cssName}: var(--${prefix}${targetName});`);
    return;
  }

  if (value.literal === undefined || value.literal === null) {
    warnings.push(
      `Token ${token.path.join(".")} has no value — emitting nothing.`,
    );
    return;
  }

  // Composite tokens: typography expands, shadow renders as CSS string.
  if (token.type === "typography" && typeof value.literal === "object") {
    const t = value.literal as Record<string, unknown>;
    for (const subField of ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"]) {
      if (t[subField] !== undefined) {
        out.push(`  ${cssName}-${kebab(subField)}: ${formatValue(t[subField], token.type)};`);
      }
    }
    return;
  }
  if (token.type === "shadow" && typeof value.literal === "object") {
    const css = renderShadow(value.literal);
    if (css) out.push(`  ${cssName}: ${css};`);
    return;
  }

  out.push(`  ${cssName}: ${formatValue(value.literal, token.type)};`);
}

/**
 * Map a token path to a Tailwind v4 CSS custom property name.
 *
 * Strategy:
 *   1. If the path's leading segment matches a Tailwind namespace
 *      (color, spacing, radius, font, text, etc.), emit it as-is — that
 *      tells Tailwind to generate the corresponding utility class family.
 *   2. Otherwise, prepend our type-based namespace heuristically.
 *   3. Always slugify each segment for valid CSS identifiers.
 *
 * Examples:
 *   ["color", "primary"]              → "color-primary"        (bg-primary works)
 *   ["spacing", "md"]                 → "spacing-md"           (p-md works)
 *   ["radius", "lg"]                  → "radius-lg"            (rounded-lg works)
 *   ["text", "base", "font-size"]     → "text-base-font-size"
 *   ["semantic", "bg"]  (color type)  → "color-semantic-bg"    (heuristic prepend)
 */
function pathToTailwindName(path: string[], type: string): string {
  // Tailwind v4's known namespaces (https://tailwindcss.com/docs/theme).
  const KNOWN = new Set([
    "color",
    "colors",
    "spacing",
    "radius",
    "rounded",
    "shadow",
    "font",
    "text",
    "font-weight",
    "tracking",
    "leading",
    "blur",
    "ease",
    "animate",
    "breakpoint",
    "container",
    "aspect",
  ]);
  // Aliases that normalize to a canonical namespace.
  const NORMALIZE: Record<string, string> = {
    colors: "color",
    rounded: "radius",
  };
  const slug = path.map(slugify);
  if (slug.length === 0) return "token";

  // First-segment match: easy case, normalize plural → singular and emit.
  if (KNOWN.has(slug[0])) {
    const head = NORMALIZE[slug[0]] ?? slug[0];
    return [head, ...slug.slice(1)].join("-");
  }

  // Determine the target Tailwind namespace from token type.
  const ns =
    type === "color"
      ? "color"
      : type === "dimension" || type === "number"
        ? "spacing"
        : type === "fontFamily"
          ? "font"
          : type === "fontWeight"
            ? "font-weight"
            : null;

  if (!ns) return slug.join("-");

  // Dedup: if the path already contains the target namespace (e.g. a path
  // like `theme.color.header.background` for a color token would otherwise
  // produce `color-theme-color-header-background`), strip the duplicate
  // segment so the result reads cleanly. Match both the canonical name
  // and any alias that normalizes to it.
  const dupAliases = new Set([ns, ...Object.entries(NORMALIZE)
    .filter(([, target]) => target === ns)
    .map(([alias]) => alias)]);
  const dedupedTail = slug.filter((s) => !dupAliases.has(s));
  return [ns, ...dedupedTail].join("-");
}

function kebab(s: string): string {
  return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}

function formatValue(value: unknown, type: string): string {
  if (typeof value === "number") {
    if (type === "dimension") return `${value}px`;
    return String(value);
  }
  if (typeof value === "string") {
    if (type === "color") return value;
    if (type === "fontFamily" || type === "string") {
      return needsQuoting(value) ? JSON.stringify(value) : value;
    }
    return value;
  }
  if (typeof value === "boolean") return String(value);
  return JSON.stringify(value);
}

function needsQuoting(s: string): boolean {
  if (/^["']/.test(s)) return false;
  if (/^[\d.]+([a-z%]+)?$/.test(s)) return false;
  if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(s)) return false;
  return true;
}

function renderShadow(shadow: unknown): string | null {
  if (Array.isArray(shadow)) {
    return shadow.map(renderShadow).filter(Boolean).join(", ");
  }
  if (!shadow || typeof shadow !== "object") return null;
  const s = shadow as Record<string, unknown>;
  const inset = s.inset ? "inset " : "";
  const x = withPx(s.offsetX);
  const y = withPx(s.offsetY);
  const blur = withPx(s.blur);
  const spread = s.spread !== undefined ? ` ${withPx(s.spread)}` : "";
  const color = s.color;
  if (!x || !y || !blur || typeof color !== "string") return null;
  return `${inset}${x} ${y} ${blur}${spread} ${color}`;
}

function withPx(v: unknown): string {
  if (typeof v === "number") return `${v}px`;
  if (typeof v === "string") return v;
  return "";
}
