/**
 * Loyaly Merchant — monochrome neutral dark theme.
 *
 * Gray IS the accent. The only color anywhere in the UI is semantic
 * (success / warning / error). Astryx's categorical hues are deliberately
 * collapsed onto the gray ramp so that Badge/Card/Banner/Token variants
 * cannot leak color into the workspace.
 *
 * The app runs `mode="dark"`, so the DARK slot of every [light, dark] tuple
 * is the real value. Light slots are still supplied — never duplicated from
 * the dark hexes — so that `light-dark()` degrades sanely if someone flips
 * mode, and so light-surfaced popovers stay legible.
 *
 * After editing this file run `npm run theme:build` (needs Node >= 22.13)
 * and commit the generated loyaly.{css,js,d.ts}.
 */

import {defineTheme} from '@astryxdesign/core/theme';
import type {TokenName, TokenValue} from '@astryxdesign/core/theme';
import {neutralTheme} from '@astryxdesign/theme-neutral';

/** The Loyaly monochrome scale — single source of truth for the whole UI. */
const G = {
  black: '#000000', // page canvas + nav rails
  surface: '#0F0F10', // elevated workspace panel
  card: '#171717', // cards, popovers
  raised: '#202124', // secondary surface, active nav item, primary button
  border: '#2F2F2F',
  white: '#FFFFFF',
  text2: '#A1A1AA',
  text3: '#71717A',
  // extra stops for the chart ramp, focus rings and disabled states
  g600: '#52525B',
  g700: '#3F3F46',
  g800: '#27272A',
  e4: '#E4E4E7',
} as const;

/**
 * Astryx ships 9 categorical hues, each with background/border/icon/text
 * sub-tokens that Badge, Card, Banner, Token and Avatar variants read from.
 * Left at their defaults they would inject color into a monochrome UI, so
 * every non-semantic hue is aliased onto the gray equivalents.
 *
 * red / green / yellow are NOT in this list, and that is deliberate: Banner's
 * success/warning/error statuses resolve THROUGH these hue tokens
 * (`.astryx-banner.success { --color-text-primary: var(--color-text-green) }`),
 * so aliasing them would strip the colour out of the semantic language the
 * user explicitly kept. The side effect is that `<Badge variant="green">`
 * reads as success-tinted — which is the correct reading, not a leak.
 */
const MUTED_HUES = ['blue', 'cyan', 'teal', 'purple', 'pink', 'orange'] as const;

function aliasHueToGray(): Partial<Record<TokenName, TokenValue>> {
  const out: Partial<Record<TokenName, TokenValue>> = {};
  for (const hue of MUTED_HUES) {
    out[`--color-background-${hue}` as TokenName] = ['#E4E4E7', G.raised];
    out[`--color-border-${hue}` as TokenName] = ['#D4D4D8', G.border];
    out[`--color-icon-${hue}` as TokenName] = ['#52525B', G.text2];
    out[`--color-text-${hue}` as TokenName] = ['#27272A', G.e4];
  }
  return out;
}

/**
 * Astryx's data-viz domain tokens. Every categorical slot points at the same
 * monochrome ramp, so any Astryx-internal visualization stays gray too.
 */
const DATA_CATEGORICAL = [
  'blue',
  'orange',
  'green',
  'purple',
  'cyan',
  'red',
  'teal',
  'pink',
  'brown',
  'indigo',
] as const;

const GRAY_RAMP: [string, string][] = [
  ['#27272A', '#E4E4E7'],
  ['#52525B', '#A1A1AA'],
  ['#71717A', '#71717A'],
  ['#A1A1AA', '#52525B'],
  ['#D4D4D8', '#3F3F46'],
];

function monochromeDataTokens(): Partial<Record<TokenName, TokenValue>> {
  const out: Partial<Record<TokenName, TokenValue>> = {};
  DATA_CATEGORICAL.forEach((hue, i) => {
    out[`--color-data-categorical-${hue}` as TokenName] =
      GRAY_RAMP[i % GRAY_RAMP.length];
  });
  return out;
}

export const loyalyTheme = defineTheme({
  name: 'loyaly',
  // REQUIRED: theme CSS is @scope-gated on the theme name, so importing
  // theme-neutral's own theme.css would match nothing once we are "loyaly".
  // Extending pulls neutral's tokens, components and icon registry in.
  extends: neutralTheme,

  typography: {
    scale: {base: 14, ratio: 1.2},
    // next/font injects the real families behind these custom properties.
    body: {
      family: 'var(--font-inter)',
      fallbacks:
        '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    },
    heading: {
      family: 'var(--font-sora)',
      fallbacks:
        '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    },
    code: {
      family: 'ui-monospace',
      fallbacks: '"SF Mono", Monaco, Consolas, monospace',
    },
  },

  // Snappy — this is a workspace people use all day, not a showcase.
  motion: {fast: 120, medium: 260, slow: 600, ratio: 0.75},

  tokens: {
    // ---- Backgrounds ------------------------------------------------------
    '--color-background-body': ['#F7F7F8', G.black],
    '--color-background-surface': ['#FFFFFF', G.surface],
    '--color-background-card': ['#FFFFFF', G.card],
    '--color-background-popover': ['#FFFFFF', G.card],
    '--color-background-muted': ['#F1F1F2', G.raised],
    '--color-background-inverted': ['#171717', G.e4],

    // ---- Accent — gray is the accent --------------------------------------
    '--color-accent': ['#262626', G.raised],
    '--color-on-accent': ['#FFFFFF', G.white],
    '--color-accent-muted': ['#F1F1F2', G.g800],
    // Accent-coloured TEXT and ICONS must stay white — a gray "accent" text
    // colour on a gray surface would be unreadable.
    '--color-text-accent': ['#171717', G.white],
    '--color-icon-accent': ['#171717', G.white],
    // Selected nav item background + secondary-button base both read this.
    '--color-neutral': ['#0000000F', G.raised],

    // ---- Text -------------------------------------------------------------
    '--color-text-primary': ['#171717', G.white],
    '--color-text-secondary': ['#71717A', G.text2],
    '--color-text-disabled': ['#A1A1AA', G.text3],
    '--color-on-dark': '#FFFFFF',
    '--color-on-light': '#171717',

    // ---- Icon (separate tokens from text) ---------------------------------
    '--color-icon-primary': ['#171717', G.white],
    '--color-icon-secondary': ['#71717A', G.text2],
    '--color-icon-disabled': ['#A1A1AA', G.text3],

    // ---- Border -----------------------------------------------------------
    '--color-border': ['#00000014', G.border],
    // Must sit ABOVE --color-border, or inputs and switches become
    // indistinguishable from card edges at #2F2F2F.
    '--color-border-emphasized': ['#D4D4D8', G.g700],

    // ---- Overlays / effects -----------------------------------------------
    '--color-overlay': ['#00000080', '#000000CC'],
    '--color-overlay-hover': ['#0000000D', '#FFFFFF0F'],
    '--color-overlay-pressed': ['#0000001A', '#FFFFFF1A'],
    '--color-skeleton': ['#EBEBEB', G.g800],
    '--color-track': ['#E4E4E7', G.g800],
    '--color-shadow': ['#0000001A', '#00000099'],
    '--color-tint-hover': ['black', 'white'],

    // ---- Semantic — the ONLY color in the UI ------------------------------
    '--color-success': ['#15803D', '#4ADE80'],
    '--color-success-muted': ['#DCFCE7', '#22C55E29'],
    '--color-on-success': ['#FFFFFF', '#0A0A0A'],
    '--color-error': ['#B91C1C', '#F87171'],
    '--color-error-muted': ['#FEE2E2', '#EF444429'],
    '--color-on-error': ['#FFFFFF', '#0A0A0A'],
    '--color-warning': ['#B45309', '#FBBF24'],
    '--color-warning-muted': ['#FEF3C7', '#F59E0B29'],
    '--color-on-warning': '#171717',

    // ---- Neutral hue family (gray is already gray, but pin the dark slots)
    '--color-background-gray': ['#E4E4E7', G.raised],
    '--color-border-gray': ['#D4D4D8', G.border],
    '--color-icon-gray': ['#52525B', G.text2],
    '--color-text-gray': ['#27272A', G.e4],

    // ---- Categorical hues collapsed to gray --------------------------------
    ...aliasHueToGray(),

    // ---- Data-viz: the monochrome chart ramp -------------------------------
    '--color-data-gray-1': ['#E4E4E7', G.g700],
    '--color-data-gray-2': ['#A1A1AA', G.g600],
    '--color-data-gray-3': ['#71717A', G.text3],
    '--color-data-gray-4': ['#52525B', G.text2],
    '--color-data-gray-5': ['#27272A', G.e4],
    '--color-data-neutral': ['#71717A', G.g600],
    ...monochromeDataTokens(),

    // ---- Shape -------------------------------------------------------------
    '--radius-inner': '0.375rem',
    '--radius-element': '0.5rem',
    '--radius-container': '0.75rem',
    '--radius-page': '1rem',

    // ---- Elevation ---------------------------------------------------------
    // On pure black a drop shadow is invisible, so every elevation step also
    // carries a 1px inset rim of light. That rim is what actually separates
    // a card from the surface behind it.
    '--shadow-low':
      '0 1px 2px light-dark(oklch(0 0 0 / 5%), oklch(0 0 0 / 60%)), ' +
      'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 6%))',
    '--shadow-med':
      '0 2px 8px light-dark(oklch(0 0 0 / 8%), oklch(0 0 0 / 70%)), ' +
      'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 9%))',
    '--shadow-high':
      '0 8px 24px light-dark(oklch(0 0 0 / 12%), oklch(0 0 0 / 80%)), ' +
      'inset 0 0 0 1px light-dark(transparent, oklch(1 0 0 / 12%))',
    // --color-accent is also the focus/selected colour. Because our accent is
    // gray, these two rings are escalated to stay visible on a #171717 card.
    '--shadow-inset-hover': `inset 0 0 0 2px ${G.g600}`,
    '--shadow-inset-selected': `inset 0 0 0 2px ${G.e4}`,
  },

  components: {
    /**
     * Button hierarchy.
     *
     * The complaint this answers is that every button looked the same: with a
     * gray accent, `primary` painted #202124 and `secondary` painted a
     * transparent chip with a #2F2F2F border, which on a #0F0F10 surface are
     * within a few percent of each other. Nothing on a screen told you which
     * action was the one to take.
     *
     * The ladder is now: white → dark chip → transparent → red. Each step is a
     * full contrast tier apart, so the eye ranks them without reading them.
     *
     * Done here rather than at the call sites: ~40 Buttons across the app use
     * the variant they always meant, and none of them change.
     */
    button: {
      // The one action per screen that should be obvious. White on near-black
      // is the highest contrast pairing available, so nothing else can
      // out-shout it — which is precisely why nothing else may use it.
      'variant:primary': {
        backgroundColor: 'var(--color-text-primary)',
        color: 'var(--color-background-body)',
        borderWidth: '1px',
        borderStyle: 'solid',
        borderColor: 'var(--color-text-primary)',
        ':hover': {backgroundColor: G.e4, borderColor: G.e4},
        ':active': {backgroundColor: G.text2, borderColor: G.text2},
      },
      // A filled dark chip, not a transparent one. It has to read as a control
      // at rest — bordered-transparent was indistinguishable from the ghost
      // variant until you hovered it.
      'variant:secondary': {
        // --color-neutral, not --color-background-*: the raised #202124 step
        // is published under the neutral token, and naming a token that does
        // not exist makes the declaration invalid and the fill transparent.
        // (Verified against the generated loyaly.css, not assumed — see
        // CLAUDE.md on the CLI's unreliable unknown-key warning.)
        backgroundColor: 'var(--color-neutral)',
        color: 'var(--color-text-primary)',
        borderWidth: '1px',
        borderStyle: 'solid',
        borderColor: 'var(--color-border-emphasized)',
        ':hover': {
          backgroundColor: G.g800,
          borderColor: 'var(--color-text-secondary)',
        },
        ':active': {backgroundColor: 'var(--color-background-card)'},
      },
      // Ghost stays chromeless at rest — it is the tertiary tier, and giving
      // it a resting box would collapse it back into secondary.
      'variant:ghost': {
        ':hover': {backgroundColor: 'var(--color-overlay-hover)'},
        ':active': {backgroundColor: 'var(--color-overlay-pressed)'},
      },
      // Destructive is one of the three colours the monochrome rule permits,
      // and the only variant that may carry hue at rest.
      'variant:destructive': {
        backgroundColor: 'var(--color-error)',
        color: 'var(--color-on-error)',
        borderWidth: '1px',
        borderStyle: 'solid',
        borderColor: 'var(--color-error)',
        ':hover': {filter: 'brightness(1.1)'},
        ':active': {filter: 'brightness(0.95)'},
      },
      base: {
        // A gray accent makes the default focus ring nearly invisible. White
        // focus is still monochrome, and it is unambiguous.
        ':focus-visible': {
          outline: '2px solid var(--color-text-primary)',
          outlineOffset: '2px',
        },
        // Disabled has to read as "not available", not as "quiet". Opacity
        // alone left a disabled primary button still the brightest thing on
        // the screen; muting the surface as well takes it out of the hierarchy
        // entirely.
        ':disabled': {
          // Darker than the secondary chip, not the same #202124: a disabled
          // control has to sit BELOW the quietest enabled one, or "off" and
          // "available but secondary" render identically.
          backgroundColor: 'var(--color-background-card)',
          color: 'var(--color-text-disabled)',
          borderColor: 'transparent',
          opacity: '1',
          cursor: 'not-allowed',
        },
      },
    },

    /**
     * Inputs: black field, visible border, white text, gray placeholder.
     *
     * The border is --color-border-emphasized (#3F3F46) rather than
     * --color-border (#2F2F2F): a field is a target, and at #2F2F2F on a
     * #171717 card it was a 4% contrast step — legible as an edge only if you
     * already knew it was there. Hover and focus each add a step, so the field
     * answers the pointer before it is clicked.
     */
    'text-input': {
      base: {
        backgroundColor: 'var(--color-background-body)',
        borderColor: 'var(--color-border-emphasized)',
        ':hover': {borderColor: 'var(--color-text-secondary)'},
        ':focus-within': {
          borderColor: 'var(--color-text-primary)',
          outline: 'none',
        },
      },
    },
    textarea: {
      base: {
        backgroundColor: 'var(--color-background-body)',
        borderColor: 'var(--color-border-emphasized)',
        ':hover': {borderColor: 'var(--color-text-secondary)'},
        ':focus-within': {
          borderColor: 'var(--color-text-primary)',
          outline: 'none',
        },
      },
    },
    'number-input': {
      base: {
        backgroundColor: 'var(--color-background-body)',
        borderColor: 'var(--color-border-emphasized)',
        ':hover': {borderColor: 'var(--color-text-secondary)'},
      },
    },
    selector: {
      base: {
        backgroundColor: 'var(--color-background-body)',
        borderColor: 'var(--color-border-emphasized)',
        ':hover': {borderColor: 'var(--color-text-secondary)'},
      },
    },

    /**
     * Menus: a hover you can see, and a selected row you can find.
     *
     * --color-overlay-hover is white at 6%, which is right for a nav rail
     * sitting on pure black but nearly invisible on a #171717 popover. Menu
     * rows get the 10% step instead, and a selected row gets a filled surface
     * rather than a tint so the current choice is identifiable at a glance
     * rather than by comparison.
     */
    'dropdown-menu-item': {
      base: {
        ':hover': {backgroundColor: G.g800},
        ':focus-visible': {backgroundColor: G.g800, outline: 'none'},
      },
    },
    'selector-option': {
      base: {
        ':hover': {backgroundColor: G.g800},
      },
      'selected:selected': {
        backgroundColor: 'var(--color-background-raised)',
        color: 'var(--color-text-primary)',
      },
    },
    item: {
      base: {
        ':hover': {backgroundColor: G.g800},
      },
    },

    // Cards: dark gray background, thin gray border, and room to breathe.
    //
    // Astryx's default card padding is 11px, which puts a card title about a
    // character's width from its own border — the single biggest reason the
    // UI read as unfinished. 24px is the smallest value where a title, a
    // subtitle and a trailing badge all sit comfortably inside the frame.
    // Set here rather than per-page so every card in every module agrees.
    card: {
      base: {
        borderColor: 'var(--color-border)',
        padding: 'var(--spacing-6)',
      },
    },

    // Table cells.
    //
    // `density` sets the vertical rhythm; this sets the horizontal inset,
    // which density does not touch. Without it a row's first value started
    // exactly where the card border ended, so the table read as a bare grid
    // rather than as content inside a panel.
    //
    // `table-cell` / `table-header-cell` are Astryx's own themeProps targets —
    // verified against dist/Table/*.js rather than guessed, because an unknown
    // key here fails silently.
    // 12px on interior cells, 20px lead-in on the first column. A flat 16px
    // everywhere added ~96px to a six-column table, which matters because
    // Settings tables live in a ~556px card once the sub-nav and Loyaly AI have
    // taken their width. The lead-in is where the eye enters the row, so that
    // is where the extra space earns its place.
    'table-cell': {
      base: {
        paddingInlineStart: 'var(--spacing-3)',
        paddingInlineEnd: 'var(--spacing-3)',
        ':first-child': {paddingInlineStart: 'var(--spacing-6)'},
        ':last-child': {paddingInlineEnd: 'var(--spacing-8)'},
      },
    },
    'table-header-cell': {
      base: {
        paddingInlineStart: 'var(--spacing-3)',
        paddingInlineEnd: 'var(--spacing-3)',
        ':first-child': {paddingInlineStart: 'var(--spacing-6)'},
        ':last-child': {paddingInlineEnd: 'var(--spacing-8)'},
      },
    },

    // Status dots: neutral goes gray, semantic keeps hue.
    statusdot: {
      'variant:accent': {backgroundColor: G.text2},
    },

    // The neutral theme hardcodes a saturated blue for the "info" badge —
    // it does not read a token, so aliasHueToGray() cannot reach it.
    // "Info" is not one of the three permitted semantic states, so it goes
    // gray. (Banner's info status DOES read --color-background-blue and is
    // therefore already handled.)
    badge: {
      'variant:info': {
        backgroundColor: 'var(--color-background-gray)',
        color: 'var(--color-text-gray)',
      },
    },

    // Same story: neutral pins `.astryx-progressbar.accent { --color-accent: #0074e2 }`,
    // which overrides our gray accent locally. A progress bar is a magnitude,
    // not a status — it should read as light gray on the track.
    progressbar: {
      'variant:accent': {'--color-accent': G.e4},
    },
  },
});

export default loyalyTheme;
