/**
 * Decantr CSS Runtime - CSS injection and extraction.
 */

/** Set of injected class names (prevents duplicates) */
const injected = new Set<string>();

/** Responsive breakpoints (mobile-first, min-width) */
export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;
const BP_ORDER = ['sm', 'md', 'lg', 'xl'] as const;
/**
 * Max-width variants render at (breakpoint - 0.02px). They occupy their own
 * style buckets so the DOM ordering matches the cascade: min-width rules
 * (sm → xl) come first, then max-width rules (xlmax → smmax) so the tighter
 * max-width queries specifically target smaller viewports without being
 * overridden by larger-viewport min-width rules that happen to be injected
 * first.
 */
const BP_MAX_ORDER = ['xlmax', 'lgmax', 'mdmax', 'smmax'] as const;

/** Container query breakpoints */
export const CQ_WIDTHS = [320, 480, 640, 768, 1024] as const;

/** CSS cascade layer order declaration */
const LAYER_ORDER = '@layer d.base,d.theme,d.atoms,d.user;';

// Buffered injection - rules are buffered and flushed to DOM in batches via microtask
let atomBuffer: string[] = [];
let bpBuffers: Record<string, string[]> = {};
let cqBuffer: string[] = [];
let flushScheduled = false;

// DOM elements (browser only)
let styleEl: HTMLStyleElement | null = null;
let layerEl: HTMLStyleElement | null = null;
let bpEls: Record<string, HTMLStyleElement> | null = null;
let cqEl: HTMLStyleElement | null = null;

function scheduleFlush(): void {
  if (flushScheduled) return;
  flushScheduled = true;
  if (typeof queueMicrotask === 'function') {
    queueMicrotask(flushBuffers);
  } else {
    Promise.resolve().then(flushBuffers);
  }
}

function flushBuffers(): void {
  flushScheduled = false;
  if (atomBuffer.length) {
    const el = getStyleElement();
    if (el) el.textContent = (el.textContent || '') + atomBuffer.join('');
    atomBuffer = [];
  }
  if (Object.keys(bpBuffers).length) {
    const els = ensureBpElements();
    if (els) {
      // Flush min-width (mobile-first) buckets first, then max-width
      // (desktop-first) buckets in reverse order so tighter queries
      // win the cascade when both are set on the same element.
      for (const bp of BP_ORDER) {
        if (bpBuffers[bp]?.length) {
          els[bp].textContent = (els[bp].textContent || '') + bpBuffers[bp].join('');
          bpBuffers[bp] = [];
        }
      }
      for (const bp of BP_MAX_ORDER) {
        if (bpBuffers[bp]?.length) {
          els[bp].textContent = (els[bp].textContent || '') + bpBuffers[bp].join('');
          bpBuffers[bp] = [];
        }
      }
    }
    bpBuffers = {};
  }
  if (cqBuffer.length) {
    const el = ensureCqElement();
    if (el) el.textContent = (el.textContent || '') + cqBuffer.join('');
    cqBuffer = [];
  }
}

function ensureLayerOrder(): void {
  if (layerEl) return;
  if (typeof document === 'undefined') return;
  layerEl = document.createElement('style');
  layerEl.setAttribute('data-decantr-layers', '');
  layerEl.textContent = LAYER_ORDER;
  document.head.prepend(layerEl);
}

function getStyleElement(): HTMLStyleElement | null {
  if (!styleEl) {
    if (typeof document === 'undefined') return null;
    ensureLayerOrder();
    styleEl = document.createElement('style');
    styleEl.setAttribute('data-decantr', '');
    document.head.appendChild(styleEl);
  }
  return styleEl;
}

function ensureBpElements(): Record<string, HTMLStyleElement> | null {
  if (bpEls) return bpEls;
  if (typeof document === 'undefined') return null;
  bpEls = {};
  getStyleElement();
  for (const bp of BP_ORDER) {
    const el = document.createElement('style');
    el.setAttribute(`data-decantr-${bp}`, '');
    document.head.appendChild(el);
    bpEls[bp] = el;
  }
  for (const bp of BP_MAX_ORDER) {
    const el = document.createElement('style');
    el.setAttribute(`data-decantr-${bp}`, '');
    document.head.appendChild(el);
    bpEls[bp] = el;
  }
  return bpEls;
}

function ensureCqElement(): HTMLStyleElement | null {
  if (cqEl) return cqEl;
  if (typeof document === 'undefined') return null;
  getStyleElement();
  cqEl = document.createElement('style');
  cqEl.setAttribute('data-decantr-cq', '');
  document.head.appendChild(cqEl);
  return cqEl;
}

/**
 * Inject a CSS rule for a class.
 * @param className - The class name (e.g., '_flex')
 * @param declaration - CSS declaration(s) (e.g., 'display:flex')
 * @param escapedName - Pre-escaped class name for special chars
 */
export function inject(className: string, declaration: string, escapedName?: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const sel = escapedName || className;
  atomBuffer.push(`@layer d.atoms{.${sel}{${declaration}}}`);
  scheduleFlush();
}

/**
 * Inject a responsive (breakpoint-wrapped) atom.
 * @param className - e.g., '_sm:gc3'
 * @param declaration - CSS declaration(s)
 * @param bp - breakpoint key (sm|md|lg|xl)
 */
export function injectResponsive(className: string, declaration: string, bp: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  if (!bpBuffers[bp]) bpBuffers[bp] = [];
  bpBuffers[bp].push(
    `@layer d.atoms{@media(min-width:${BREAKPOINTS[bp as keyof typeof BREAKPOINTS]}px){.${escaped}{${declaration}}}}`,
  );
  scheduleFlush();
}

/**
 * Inject a max-width responsive atom.
 * @param className - e.g., '_mdmax:none' (hide below md)
 * @param declaration - CSS declaration(s)
 * @param bp - breakpoint key (sm|md|lg|xl). The max-width query is (breakpoint - 0.02)px
 *             so it doesn't overlap with the matching min-width variant.
 */
export function injectResponsiveMax(className: string, declaration: string, bp: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  const key = `${bp}max`;
  if (!bpBuffers[key]) bpBuffers[key] = [];
  const maxPx = BREAKPOINTS[bp as keyof typeof BREAKPOINTS] - 0.02;
  bpBuffers[key].push(`@layer d.atoms{@media(max-width:${maxPx}px){.${escaped}{${declaration}}}}`);
  scheduleFlush();
}

/**
 * Inject a pseudo-class atom.
 * @param className - e.g., '_h:bgprimary'
 * @param declaration - CSS declaration(s)
 * @param prefix - 'h'|'f'|'fv'|'a'|'fw'
 */
export function injectPseudo(className: string, declaration: string, prefix: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  const PSEUDO_MAP: Record<string, string> = {
    h: 'hover',
    f: 'focus',
    fv: 'focus-visible',
    a: 'active',
    fw: 'focus-within',
  };
  const pseudo = PSEUDO_MAP[prefix];
  atomBuffer.push(`@layer d.atoms{.${escaped}:${pseudo}{${declaration}}}`);
  scheduleFlush();
}

/**
 * Inject a responsive + pseudo-class atom.
 * @param className - e.g., '_sm:h:bgprimary'
 * @param declaration - CSS declaration(s)
 * @param bp - breakpoint key
 * @param pseudo - pseudo-class name
 */
export function injectResponsivePseudo(
  className: string,
  declaration: string,
  bp: string,
  pseudo: string,
): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  if (!bpBuffers[bp]) bpBuffers[bp] = [];
  bpBuffers[bp].push(
    `@layer d.atoms{@media(min-width:${BREAKPOINTS[bp as keyof typeof BREAKPOINTS]}px){.${escaped}:${pseudo}{${declaration}}}}`,
  );
  scheduleFlush();
}

/**
 * Inject a max-width responsive + pseudo-class atom.
 * @param className - e.g., '_mdmax:h:fgmuted'
 * @param declaration - CSS declaration(s)
 * @param bp - breakpoint key (sm|md|lg|xl)
 * @param pseudo - pseudo-class name
 */
export function injectResponsiveMaxPseudo(
  className: string,
  declaration: string,
  bp: string,
  pseudo: string,
): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  const key = `${bp}max`;
  if (!bpBuffers[key]) bpBuffers[key] = [];
  const maxPx = BREAKPOINTS[bp as keyof typeof BREAKPOINTS] - 0.02;
  bpBuffers[key].push(
    `@layer d.atoms{@media(max-width:${maxPx}px){.${escaped}:${pseudo}{${declaration}}}}`,
  );
  scheduleFlush();
}

/**
 * Inject a container query-wrapped atom.
 * @param className - e.g., '_cq640:gc3'
 * @param declaration - CSS declaration(s)
 * @param width - container min-width in px
 */
export function injectContainer(className: string, declaration: string, width: number): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  cqBuffer.push(`@layer d.atoms{@container(min-width:${width}px){.${escaped}{${declaration}}}}`);
  scheduleFlush();
}

/**
 * Inject a group/peer state atom.
 * @param className - e.g., '_gh:fgprimary'
 * @param declaration - CSS declaration(s)
 * @param prefix - 'gh'|'gf'|'ga'|'ph'|'pf'|'pa'
 */
export function injectGroupPeer(className: string, declaration: string, prefix: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  const GP_STATE: Record<string, [string, string]> = {
    gh: ['group', 'hover'],
    gf: ['group', 'focus-within'],
    ga: ['group', 'active'],
    ph: ['peer', 'hover'],
    pf: ['peer', 'focus'],
    pa: ['peer', 'active'],
  };
  const [kind, state] = GP_STATE[prefix];
  const combinator = kind === 'group' ? ' ' : ' ~ ';
  atomBuffer.push(`@layer d.atoms{.d-${kind}:${state}${combinator}.${escaped}{${declaration}}}`);
  scheduleFlush();
}

/**
 * Inject a media query-wrapped atom.
 * @param className - e.g., '_motionSafe:trans'
 * @param declaration - CSS declaration(s)
 * @param query - media query string
 */
export function injectMediaQuery(className: string, declaration: string, query: string): void {
  if (injected.has(className)) return;
  injected.add(className);
  if (typeof document === 'undefined') return;
  const escaped = escapeSelector(className);
  atomBuffer.push(`@layer d.atoms{@media${query}{.${escaped}{${declaration}}}}`);
  scheduleFlush();
}

/** Escape special characters in a CSS selector */
function escapeSelector(className: string): string {
  const chars = Array.from(className);
  let escaped = '';

  for (let index = 0; index < chars.length; index += 1) {
    const char = chars[index]!;
    const codePoint = char.codePointAt(0) ?? 0;
    const firstChar = chars[0];
    const isControl = (codePoint >= 1 && codePoint <= 31) || codePoint === 127;
    const needsNumericEscape =
      isControl ||
      (index === 0 && isAsciiDigit(codePoint)) ||
      (index === 1 && firstChar === '-' && isAsciiDigit(codePoint));

    if (codePoint === 0) {
      escaped += '\uFFFD';
    } else if (needsNumericEscape) {
      escaped += `\\${codePoint.toString(16)} `;
    } else if (index === 0 && char === '-' && chars.length === 1) {
      escaped += '\\-';
    } else if (isCssNameCodePoint(codePoint)) {
      escaped += char;
    } else {
      escaped += `\\${char}`;
    }
  }

  return escaped;
}

function isAsciiDigit(codePoint: number): boolean {
  return codePoint >= 48 && codePoint <= 57;
}

function isCssNameCodePoint(codePoint: number): boolean {
  return (
    codePoint >= 128 ||
    codePoint === 45 ||
    codePoint === 95 ||
    isAsciiDigit(codePoint) ||
    (codePoint >= 65 && codePoint <= 90) ||
    (codePoint >= 97 && codePoint <= 122)
  );
}

/**
 * Extract all injected CSS as a string (for SSR).
 */
export function extractCSS(): string {
  flushBuffers();
  let css = layerEl ? layerEl.textContent || '' : '';
  css += styleEl ? styleEl.textContent || '' : '';
  if (bpEls) {
    for (const bp of BP_ORDER) {
      if (bpEls[bp]) css += bpEls[bp].textContent || '';
    }
    for (const bp of BP_MAX_ORDER) {
      if (bpEls[bp]) css += bpEls[bp].textContent || '';
    }
  }
  if (cqEl) css += cqEl.textContent || '';
  return css;
}

/**
 * Get array of injected class names.
 */
export function getInjectedClasses(): string[] {
  return [...injected];
}

/**
 * Reset all injected styles (for testing).
 */
export function reset(): void {
  injected.clear();
  atomBuffer = [];
  bpBuffers = {};
  cqBuffer = [];
  flushScheduled = false;
  if (layerEl) layerEl.textContent = LAYER_ORDER;
  if (styleEl) styleEl.textContent = '';
  if (bpEls) {
    for (const bp of BP_ORDER) {
      if (bpEls[bp]) bpEls[bp].textContent = '';
    }
    for (const bp of BP_MAX_ORDER) {
      if (bpEls[bp]) bpEls[bp].textContent = '';
    }
  }
  if (cqEl) cqEl.textContent = '';
}
