/**
 * Design systems — the stylesheet held as records (milestone #254).
 *
 * A design system is a named set of tokens with an optional parent. A system
 * with no parent is a "family"; one with a parent holds ONLY what it changes,
 * so "what does this app alter?" is a plain list rather than a diff.
 */
import { apiDelete, apiGet, apiPatch, apiPost, apiPut } from "@/api/client";

export interface DesignSystem {
  id: number;
  owner_user_id: number;
  title: string;
  description: string;
  /** Narrative a token table cannot hold: aesthetic, voice, what's out of scope. */
  guidance: string;
  parent_id: number | null;
  created_at: string | null;
  updated_at: string | null;
}

/** A token as STORED — one system's own row for it. */
export interface DesignToken {
  id: number;
  design_system_id: number;
  name: string;
  /** Values keyed by mode. `base` applies when no mode is more specific. */
  value_by_mode: Record<string, string>;
  group_name: string | null;
  purpose: string | null;
  /** WHY it is this value — distinct from `purpose`, which is what it is FOR. */
  rationale: string | null;
  /** Literal values this token should be used INSTEAD OF, e.g. ["#fff"].
   *
   *  How a design system records what a prohibition was trying to say: not
   *  "white is banned" but "write this token instead". Declared rather than
   *  inferred, because a superseded literal and the token's own value are
   *  usually different values and nothing could connect them by matching. */
  supersedes: string[];
  order_index: number;
}

export interface Contribution {
  system_id: number;
  value: string;
}

/**
 * A token after the cascade.
 *
 * `contributions` is every system that offered a value, per mode, DEEPEST
 * FIRST — entry 0 won and the rest were shadowed. `value_by_mode` and
 * `origin_by_mode` are the winners, provided so the client never has to derive
 * them (and so it cannot derive them differently).
 *
 * Provenance is per MODE because overriding is: a system can own `base` and
 * inherit `dark` at the same time.
 */
export interface ResolvedToken {
  name: string;
  group_name: string | null;
  purpose: string | null;
  rationale: string | null;
  supersedes: string[];
  order_index: number;
  value_by_mode: Record<string, string>;
  origin_by_mode: Record<string, number>;
  contributions: Record<string, Contribution[]>;
}

export const fetchDesignSystems = () =>
  apiGet<{ design_systems: DesignSystem[] }>("/api/design-systems");

export const fetchDesignSystem = (id: number) =>
  apiGet<DesignSystem>(`/api/design-systems/${id}`);

export interface StarterRoleGroup {
  group: string;
  description: string;
  token_count: number;
  names: string[];
}

/** The starter token ROLES offered at creation — names and purposes, never
 *  values. A default palette would be one install's taste shipped as product
 *  (rule #115), so the values are always the operator's to fill. */
export const listStarterRoleGroups = () =>
  apiGet<{ groups: StarterRoleGroup[]; default_prefix: string }>(
    "/api/design-systems/starter-roles",
  );

export const createDesignSystem = (body: {
  title: string;
  description?: string;
  guidance?: string;
  parent_id?: number | null;
  starter_role_groups?: string[];
  token_prefix?: string;
}) => apiPost<DesignSystem>("/api/design-systems", body);

/** Omit `parent_id` to leave it alone; send `null` to make the system a family. */
export const updateDesignSystem = (
  id: number,
  body: {
    title?: string;
    description?: string;
    guidance?: string;
    parent_id?: number | null;
  },
) => apiPatch<DesignSystem>(`/api/design-systems/${id}`, body);

export const deleteDesignSystem = (id: number) =>
  apiDelete(`/api/design-systems/${id}`);

/** The EFFECTIVE set: everything inherited, with this system's on top. */
export const fetchResolvedTokens = (id: number) =>
  apiGet<{ design_system_id: number; tokens: ResolvedToken[] }>(
    `/api/design-systems/${id}/resolved`,
  );

/** This system's OWN tokens — its override set. */
export const fetchDesignTokens = (id: number) =>
  apiGet<{ tokens: DesignToken[] }>(`/api/design-systems/${id}/tokens`);

export const createDesignToken = (
  designSystemId: number,
  body: {
    name: string;
    value_by_mode?: Record<string, string>;
    group_name?: string | null;
    purpose?: string | null;
    rationale?: string | null;
    supersedes?: string[];
    order_index?: number;
  },
) => apiPost<DesignToken>(`/api/design-systems/${designSystemId}/tokens`, body);

export const updateDesignToken = (
  tokenId: number,
  body: Partial<Omit<DesignToken, "id" | "design_system_id">>,
) => apiPatch<DesignToken>(`/api/design-tokens/${tokenId}`, body);

export const deleteDesignToken = (tokenId: number) =>
  apiDelete(`/api/design-tokens/${tokenId}`);

/** Point a project at a design system. `null` clears it. */
export const setProjectDesignSystem = (
  projectId: number,
  designSystemId: number | null,
) =>
  apiPut<{ project_id: number; design_system_id: number | null }>(
    `/api/projects/${projectId}/design-system`,
    { design_system_id: designSystemId },
  );

export interface StylesheetResult {
  design_system_id: number;
  /** The master sheet: purpose tokens only, no element or class rules. */
  css: string;
  token_count: number;
  /** Tokens the system names but has no value for yet. */
  valueless: string[];
  /** Values declared under more than one name — alias, or one idea twice. */
  duplicates: Record<string, string[]>;
  derivation: {
    /** Tokens computed from others, mapped to what they're computed from. */
    derived: Record<string, string[]>;
    /** Formulas pointing at tokens that don't exist — the browser drops these. */
    unknown_refs: Record<string, string[]>;
    /** Derivation loops, which resolve to nothing for the same reason. */
    cycles: string[][];
  };
}

/** The master CSS sheet a design system generates.
 *
 *  Purpose tokens only. Components (buttons, tables, input schemes) are
 *  snippets that reference these names, so a value is stated once and reused
 *  rather than restated per element. */
export const fetchStylesheet = (id: number) =>
  apiGet<StylesheetResult>(`/api/design-systems/${id}/stylesheet`);

export interface SnippetFinding {
  snippet_id: number;
  title: string;
  /** References that resolve against the sheet. */
  used: string[];
  /** `var(--x)` where the system has no `--x` — renders as nothing at all. */
  unknown: string[];
  /** Literals the sheet says to stop writing, paired with what to write. */
  superseded_literals: { literal: string; use_instead: string }[];
  /** Custom properties the snippet mints for itself instead of reusing. */
  local_definitions: string[];
}

export interface SnippetCheck {
  design_system_id: number;
  checked: number;
  /** Only snippets with something to act on; clean ones are omitted. */
  findings: SnippetFinding[];
}

/** Which recorded snippets disagree with this design system's sheet.
 *
 *  `projectId` narrows to the snippets one project owns — which is how a
 *  project asks about its OWN code. Omit it to check every project, which is
 *  the right default from the system's side: a component recorded elsewhere
 *  still has to use the same tags. */
export const checkSnippets = (id: number, projectId?: number) =>
  apiGet<SnippetCheck>(
    `/api/design-systems/${id}/snippet-check`
    + (projectId ? `?project_id=${projectId}` : ""),
  );
