/**
 * TypeScript module formatter.
 *
 * Output shape:
 *
 *   // tokens.ts
 *   export const tokens = {
 *     color: {
 *       primary: "#4085F2",
 *       brand: { primary: "#FF00AA" }
 *     },
 *     spacing: {
 *       md: "16px"
 *     }
 *   } as const;
 *
 *   export type Tokens = typeof tokens;
 *
 * Multi-mode tokens emit as nested objects keyed by mode name:
 *
 *   export const tokens = {
 *     mode: {
 *       primary: { Light: "#FFFFFF", Dark: "#000000" }
 *     }
 *   } as const;
 *
 * Aliases get resolved at write time to the literal value (TypeScript
 * can't natively express references). Cross-library aliases get emitted
 * as `null` with a "TODO: cross-library alias unresolved" comment so
 * consumers see the gap.
 */

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 formatTsModule(
  doc: TokenDocument,
  opts: FormatOptions,
): FormatResult {
  const warnings: string[] = [];
  const files: FormatResult["files"] = [];

  const splitByCollection = opts.target.splitByCollection ?? false;
  // splitByMode doesn't make conceptual sense for a TypeScript module
  // (the value is held in-memory at runtime, not selected by CSS cascade).

  // TypeScript modules can't natively express alias references — runtime
  // code reading tokens.color.X gets a string at runtime, no resolution
  // logic. Build a document-wide token index so aliases resolve to their
  // literal target at export time. The index spans every set even when
  // splitByCollection writes single-set files, because aliases often
  // cross set boundaries (semantic → primitives).
  const tokenIndex = buildTokenIndex(doc);

  if (splitByCollection) {
    for (const set of doc.sets) {
      files.push({
        path: filenameFor(opts, set),
        content: renderTsFile([set], opts, tokenIndex, warnings),
      });
    }
  } else {
    files.push({
      path: filenameFor(opts),
      content: renderTsFile(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)}.tokens.ts`;
  return "tokens.ts";
}

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

/**
 * Convert a path segment to a valid JS identifier or quoted-key form.
 * Numeric leading chars, hyphens, etc. → quoted string key.
 */
function jsKey(segment: string): string {
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment)) return segment;
  return JSON.stringify(segment);
}

interface TokenTree {
  [key: string]: TokenTree | TokenLeaf;
}

interface TokenLeaf {
  __leaf: true;
  values: Record<string, unknown>;
  type: string;
  isMultiMode: boolean;
  description?: string;
}

function renderTsFile(
  sets: TokenSet[],
  opts: FormatOptions,
  tokenIndex: Map<string, Token>,
  warnings: string[],
): string {
  const prefix = opts.target.prefix ?? "";

  const lines: string[] = [];
  lines.push("/**");
  lines.push(" * Generated by figma-console-mcp — do not edit by hand.");
  lines.push(" * Regenerate with: figma_export_tokens (or your npm tokens:export script)");
  lines.push(" */");
  lines.push("");

  const constName = prefix
    ? `${prefix.replace(/-+$/, "").replace(/-(\w)/g, (_, c) => c.toUpperCase())}Tokens`
    : "tokens";

  // Build the nested object structure by walking each set's tokens.
  const tree: TokenTree = {};

  for (const set of sets) {
    for (const token of set.tokens) {
      writeTokenIntoTree(tree, token, set, tokenIndex, warnings);
    }
  }

  lines.push(`export const ${constName} = ${renderTree(tree, 0)} as const;`);
  lines.push("");
  lines.push(`export type ${constName[0].toUpperCase()}${constName.slice(1)} = typeof ${constName};`);
  lines.push("");

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

function writeTokenIntoTree(
  tree: TokenTree,
  token: Token,
  set: TokenSet,
  tokenIndex: Map<string, Token>,
  warnings: string[],
): void {
  let cursor: TokenTree = tree;
  for (let i = 0; i < token.path.length - 1; i++) {
    const segment = token.path[i];
    let next = cursor[segment];
    if (!next || isLeaf(next)) {
      next = {} as TokenTree;
      cursor[segment] = next;
    }
    cursor = next as TokenTree;
  }

  const leafKey = token.path[token.path.length - 1];
  const isMulti = set.modes.length > 1;

  const resolvedValues: Record<string, unknown> = {};
  for (const [modeName, value] of Object.entries(token.values)) {
    let effective: TokenValue | null = value;
    if (value.reference) {
      effective = resolveAliasChain(value, modeName, tokenIndex);
      if (!effective) {
        const bare = value.reference.replace(/^\{|\}$/g, "");
        const reason = bare.startsWith("__library:") || bare === "unknown"
          ? "cross-library alias"
          : "alias target not found";
        warnings.push(
          `Skipped ${token.path.join(".")} (mode "${modeName}") in TS module — ${reason}: ${value.reference}.`,
        );
        resolvedValues[modeName] = null;
        continue;
      }
    }
    if (!effective || effective.literal === undefined || effective.literal === null) continue;
    resolvedValues[modeName] = formatLiteral(effective.literal, token.type);
  }

  cursor[leafKey] = {
    __leaf: true,
    values: resolvedValues,
    type: token.type,
    isMultiMode: isMulti,
    description: token.description,
  };
}

function isLeaf(node: TokenTree | TokenLeaf): node is TokenLeaf {
  return (node as TokenLeaf).__leaf === true;
}

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

  for (const [key, value] of Object.entries(tree)) {
    const k = jsKey(key);
    if (isLeaf(value)) {
      const leaf = value;
      if (leaf.isMultiMode) {
        // Emit as { Mode: value } object
        const modeEntries: string[] = [];
        for (const [m, v] of Object.entries(leaf.values)) {
          modeEntries.push(`${jsKey(m)}: ${renderValue(v)}`);
        }
        entries.push(`${innerInd}${k}: { ${modeEntries.join(", ")} }`);
      } else {
        // Single-mode: emit value directly
        const onlyValue = Object.values(leaf.values)[0];
        entries.push(`${innerInd}${k}: ${renderValue(onlyValue)}`);
      }
    } else {
      entries.push(`${innerInd}${k}: ${renderTree(value, indent + 1)}`);
    }
  }

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

function renderValue(v: unknown): string {
  if (v === null) return "null /* TODO: cross-library alias unresolved */";
  if (typeof v === "string") return JSON.stringify(v);
  if (typeof v === "number" || typeof v === "boolean") return String(v);
  return JSON.stringify(v);
}

function formatLiteral(value: unknown, type: string): string | number | boolean | object {
  if (typeof value === "number") {
    if (type === "dimension") return `${value}px`;
    return value;
  }
  return value as string | number | boolean | object;
}
