import type { Breakpoint, BreakpointKey, ControlDef, ElementState, Settings } from './types';
import { fallbackChain } from './breakpoints';

const KEY_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)(?:::([a-z_]+))?(?:@([a-z_]+))?$/;

export interface KeyParts {
  id: string;
  state: ElementState;
  bp: BreakpointKey;
}

export interface KeyOptions {
  bp?: BreakpointKey;
  state?: ElementState;
}

/** Builds a settings key: `id`, `id::hover`, `id@tablet`, `id::hover@tablet`. */
export function makeKey(id: string, opts: KeyOptions = {}): string {
  const state = opts.state ?? 'normal';
  const bp = opts.bp ?? 'desktop';
  return id + (state !== 'normal' ? `::${state}` : '') + (bp !== 'desktop' ? `@${bp}` : '');
}

export function parseKey(key: string): KeyParts | null {
  const m = KEY_RE.exec(key);
  if (!m) return null;
  return {
    id: m[1],
    state: (m[2] as ElementState) || 'normal',
    bp: (m[3] as BreakpointKey) || 'desktop',
  };
}

/** True for values that should be treated as "not set". */
export function isEmptyValue(value: unknown): boolean {
  if (value === undefined || value === null || value === '') return true;
  if (typeof value === 'number') return Number.isNaN(value);
  if (typeof value === 'boolean') return false;
  if (Array.isArray(value)) return value.length === 0;
  if (typeof value === 'object') {
    const obj = value as Record<string, unknown>;
    const keys = Object.keys(obj).filter((k) => k !== 'unit' && k !== 'linked' && k !== 'type');
    if (keys.length === 0) return true;
    return keys.every((k) => isEmptyValue(obj[k]));
  }
  return false;
}

/** Raw value stored exactly at this breakpoint/state (no fallback). */
export function getRaw(settings: Settings, id: string, opts: KeyOptions = {}): unknown {
  return settings[makeKey(id, opts)];
}

/**
 * Effective value at a breakpoint/state, following the desktop-first cascade.
 * For non-normal states there is no fallback to the normal state (CSS inherits it naturally).
 */
export function getEffective(
  settings: Settings,
  id: string,
  opts: KeyOptions,
  breakpoints: Breakpoint[],
): unknown {
  const state = opts.state ?? 'normal';
  const chain = fallbackChain(opts.bp ?? 'desktop', breakpoints);
  for (const bp of chain) {
    const v = settings[makeKey(id, { bp, state })];
    if (!isEmptyValue(v)) return v;
  }
  return undefined;
}

/** Which breakpoint in the chain actually provides the effective value (for UI hints). */
export function getEffectiveSource(
  settings: Settings,
  id: string,
  opts: KeyOptions,
  breakpoints: Breakpoint[],
): BreakpointKey | null {
  const state = opts.state ?? 'normal';
  for (const bp of fallbackChain(opts.bp ?? 'desktop', breakpoints)) {
    if (!isEmptyValue(settings[makeKey(id, { bp, state })])) return bp;
  }
  return null;
}

/** Value with manifest default applied (desktop/normal only). */
export function getWithDefault(settings: Settings, control: ControlDef, opts: KeyOptions, breakpoints: Breakpoint[]): unknown {
  const v = getEffective(settings, control.id, opts, breakpoints);
  if (v !== undefined) return v;
  if ((opts.state ?? 'normal') === 'normal') return control.default;
  return undefined;
}

/** Returns a new settings object with the value set (or the key removed when empty). Does not mutate. */
export function withValue(settings: Settings, id: string, value: unknown, opts: KeyOptions = {}): Settings {
  const key = makeKey(id, opts);
  const next = { ...settings };
  if (value === undefined) delete next[key];
  else next[key] = value;
  return next;
}

/** Mutating variant used by the store (reactive objects). Returns previous value. */
export function setValue(settings: Settings, id: string, value: unknown, opts: KeyOptions = {}): unknown {
  const key = makeKey(id, opts);
  const prev = settings[key];
  if (value === undefined) delete settings[key];
  else settings[key] = value;
  return prev;
}

/** All keys in settings that belong to a control id (every bp/state). */
export function keysFor(settings: Settings, id: string): string[] {
  return Object.keys(settings).filter((k) => parseKey(k)?.id === id);
}

/** Merge manifest defaults into base keys that are missing. Returns a new object. */
export function applyDefaults(settings: Settings, controls: ControlDef[]): Settings {
  const out: Settings = { ...settings };
  for (const c of controls) {
    if (c.default !== undefined && out[c.id] === undefined) out[c.id] = structuredClone(c.default);
  }
  return out;
}

/**
 * Evaluates a control condition against effective values.
 * { "tag": "a" } equals, { "tag!": "" } not-equals, arrays mean "one of".
 * When the stored value is an array (checklist), `{ "actions": "email" }` means "contains".
 */
export function matchesCondition(
  condition: Record<string, unknown> | undefined,
  settings: Settings,
  controls: Record<string, ControlDef>,
  opts: KeyOptions,
  breakpoints: Breakpoint[],
): boolean {
  if (!condition) return true;
  for (const [rawKey, expected] of Object.entries(condition)) {
    const negate = rawKey.endsWith('!');
    const id = negate ? rawKey.slice(0, -1) : rawKey;
    const control = controls[id];
    let actual = getEffective(settings, id, { bp: opts.bp, state: 'normal' }, breakpoints);
    if (actual === undefined && control) actual = control.default;
    if (actual === undefined) actual = '';
    const list = Array.isArray(expected) ? expected : [expected];
    // Multi-value controls (checklist) match when the stored list contains one of the expected values.
    const matches = Array.isArray(actual) ? list.some((e) => (actual as unknown[]).some((a) => looseEqual(a, e))) : list.some((e) => looseEqual(actual, e));
    if (negate ? matches : !matches) return false;
  }
  return true;
}

function looseEqual(a: unknown, b: unknown): boolean {
  if (typeof a === 'boolean' || typeof b === 'boolean') return Boolean(a) === Boolean(b);
  return String(a) === String(b);
}

/** Split settings into content vs. style/advanced keys using control tabs (for copy/paste style). */
export function partitionByTab(settings: Settings, controls: Record<string, ControlDef>): { content: Settings; style: Settings } {
  const content: Settings = {};
  const style: Settings = {};
  for (const [key, value] of Object.entries(settings)) {
    const parts = parseKey(key);
    const control = parts ? controls[parts.id] : undefined;
    const tab = control?.tab ?? 'content';
    (tab === 'content' ? content : style)[key] = value;
  }
  return { content, style };
}
