import type { ControlDef, DynamicTagDef, Settings } from './types';

/**
 * Dynamic content value model (T114). Mirrors `Zadex\Dynamic\Value` in PHP.
 *
 * A setting bound to a dynamic tag is stored as
 *   { __dynamic: { tag: 'post-title', settings: {...}, before: '', after: '', fallback: '' } }
 * The frontend resolves it in `Renderer::settings()`; the canvas resolves it through POST /dynamic/resolve
 * (src/editor/canvas/useDynamicPreview.ts). CSS generation skips dynamic values.
 */
export const DYNAMIC_KEY = '__dynamic';

export type DynamicCategory = 'text' | 'url' | 'image' | 'number' | 'color';

export interface DynamicData {
  tag: string;
  settings: Settings;
  before: string;
  after: string;
  fallback: string;
}

export interface DynamicValue {
  [DYNAMIC_KEY]: DynamicData;
}

/** Control types that accept a dynamic value, and the tag category each reads. */
export const DYNAMIC_CONTROL_CATEGORIES: Record<string, DynamicCategory> = {
  text: 'text',
  textarea: 'text',
  richtext: 'text',
  url: 'url',
  media: 'image',
  number: 'number',
  color: 'color',
};

export function isDynamic(value: unknown): value is DynamicValue {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  const d = (value as Record<string, unknown>)[DYNAMIC_KEY];
  return Boolean(d) && typeof d === 'object' && typeof (d as { tag?: unknown }).tag === 'string' && (d as { tag: string }).tag !== '';
}

/** Normalised dynamic data (all fields present) or null. */
export function dynamicOf(value: unknown): DynamicData | null {
  if (!isDynamic(value)) return null;
  const d = value[DYNAMIC_KEY] as Partial<DynamicData>;
  const str = (v: unknown): string => (typeof v === 'string' || typeof v === 'number' ? String(v) : '');
  return {
    tag: String(d.tag),
    settings: d.settings && typeof d.settings === 'object' && !Array.isArray(d.settings) ? (d.settings as Settings) : {},
    before: str(d.before),
    after: str(d.after),
    fallback: str(d.fallback),
  };
}

export function makeDynamic(tag: string, settings: Settings = {}, extra: Partial<Omit<DynamicData, 'tag' | 'settings'>> = {}): DynamicValue {
  return { [DYNAMIC_KEY]: { tag, settings, before: extra.before ?? '', after: extra.after ?? '', fallback: extra.fallback ?? '' } };
}

/** True when the value is, or contains (background image, repeater items), a dynamic value. */
export function containsDynamic(value: unknown, depth = 0): boolean {
  if (!value || typeof value !== 'object' || depth > 8) return false;
  if (isDynamic(value)) return true;
  for (const v of Array.isArray(value) ? value : Object.values(value as Record<string, unknown>)) {
    if (v && typeof v === 'object' && containsDynamic(v, depth + 1)) return true;
  }
  return false;
}

/** Deep copy with every nested dynamic value removed (CSS generation must never see them). */
export function stripDynamic(value: unknown, depth = 0): unknown {
  if (!value || typeof value !== 'object' || depth > 8) return value;
  if (isDynamic(value)) return undefined;
  if (Array.isArray(value)) return value.filter((v) => !isDynamic(v)).map((v) => stripDynamic(v, depth + 1));
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
    if (!isDynamic(v)) out[k] = stripDynamic(v, depth + 1);
  }
  return out;
}

export function categoryForControl(type: string): DynamicCategory | null {
  return DYNAMIC_CONTROL_CATEGORIES[type] ?? null;
}

/** Registered tags (window.ZADEX.dynamicTags). */
export function dynamicTags(): DynamicTagDef[] {
  if (typeof window === 'undefined') return [];
  return window.ZADEX?.dynamicTags ?? [];
}

export function findTag(name: string, tags: DynamicTagDef[] = dynamicTags()): DynamicTagDef | undefined {
  return tags.find((t) => t.name === name);
}

/** Tags usable by a control of this type. */
export function tagsForType(type: string, tags: DynamicTagDef[] = dynamicTags()): DynamicTagDef[] {
  const cat = categoryForControl(type);
  return cat ? tags.filter((t) => t.categories.includes(cat)) : [];
}

/**
 * Whether the settings panel offers the dynamic button for a control: a supported type, and a control whose
 * value is rendered by the widget (not a CSS-mapped style control, not the Advanced tab), unless the manifest
 * says `dynamic: true` / `false` explicitly.
 */
export function supportsDynamic(control: ControlDef, tags: DynamicTagDef[] = dynamicTags()): boolean {
  if (control.dynamic === false || !categoryForControl(control.type)) return false;
  if (control.dynamic !== true && (control.css?.length || control.tab === 'advanced')) return false;
  return tagsForType(control.type, tags).length > 0;
}

/** Defaults of a tag's own controls (settings of a freshly picked tag). */
export function tagDefaults(tag: DynamicTagDef): Settings {
  const out: Settings = {};
  for (const c of tag.controls ?? []) if (c.default !== undefined) out[c.id] = structuredClone(c.default);
  return out;
}

/** Empty static value that replaces a dynamic one when the binding is removed (×). */
export function emptyStaticValue(type: string): unknown {
  switch (type) {
    case 'url':
      return { url: '', target: '', nofollow: false };
    case 'media':
      return { id: 0, url: '', alt: '' };
    default:
      return '';
  }
}

/** Value shown while a dynamic value is not resolved yet (or could not be): the fallback, in the control's shape. */
export function placeholderValue(data: DynamicData, type: string): unknown {
  switch (type) {
    case 'url':
      return { url: data.fallback, target: '', nofollow: false };
    case 'media':
      return { id: 0, url: data.fallback, alt: '' };
    case 'number':
      return data.fallback !== '' && !Number.isNaN(Number(data.fallback)) ? Number(data.fallback) : '';
    default:
      return data.fallback;
  }
}

/** Stable cache key of one dynamic value in a context. */
export function dynamicCacheKey(postId: number, type: string, data: DynamicData): string {
  const sorted = (v: unknown): unknown => {
    if (!v || typeof v !== 'object') return v;
    if (Array.isArray(v)) return v.map(sorted);
    return Object.fromEntries(Object.keys(v as object).sort().map((k) => [k, sorted((v as Record<string, unknown>)[k])]));
  };
  return JSON.stringify([postId, type, sorted(data)]);
}

export interface DynamicItem {
  /** Control type the value is coerced to (text, url, media ...). */
  type: string;
  data: DynamicData;
  value: DynamicValue;
}

/**
 * Walks settings and calls `visit` for every dynamic value, returning a copy where each is replaced by what
 * `visit` returns. Composite values are walked like the PHP Resolver: `image` / `poster` / `images[]` are
 * media, repeater item fields use their field type, anything else is text.
 */
export function mapDynamic(settings: Settings, controls: Record<string, ControlDef>, visit: (item: DynamicItem) => unknown): Settings {
  let out: Settings | null = null;
  for (const [key, value] of Object.entries(settings)) {
    if (!value || typeof value !== 'object' || !containsDynamic(value)) continue;
    const base = key.replace(/(::[a-z_]+)?(@[a-z_]+)?$/, '');
    const control = controls[base];
    const type = control?.type ?? 'text';
    let next: unknown;
    if (isDynamic(value)) next = visit({ type, data: dynamicOf(value)!, value });
    else if (type === 'repeater' && Array.isArray(value)) {
      const fields: Record<string, ControlDef> = {};
      for (const f of control?.fields ?? []) fields[f.id] = f;
      next = value.map((item) => (item && typeof item === 'object' ? mapDynamic(item as Settings, fields, visit) : item));
    } else next = mapNested(value, '', visit, 0);
    out ??= { ...settings };
    out[key] = next;
  }
  return out ?? settings;
}

function mapNested(value: unknown, parent: string, visit: (item: DynamicItem) => unknown, depth: number): unknown {
  if (!value || typeof value !== 'object' || depth > 6) return value;
  const entries = Array.isArray(value) ? value.map((v, i) => [String(i), v] as const) : Object.entries(value as Record<string, unknown>);
  const out: Record<string, unknown> | unknown[] = Array.isArray(value) ? [] : {};
  for (const [k, v] of entries) {
    const media = k === 'image' || k === 'poster' || parent === 'images';
    let next = v;
    if (isDynamic(v)) next = visit({ type: media ? 'media' : 'text', data: dynamicOf(v)!, value: v });
    else if (v && typeof v === 'object' && containsDynamic(v)) next = mapNested(v, k, visit, depth + 1);
    (out as Record<string, unknown>)[k] = next;
  }
  return out;
}

/** Every dynamic value of a settings object (for batching resolve requests). */
export function collectDynamic(settings: Settings, controls: Record<string, ControlDef>): DynamicItem[] {
  const items: DynamicItem[] = [];
  mapDynamic(settings, controls, (item) => {
    items.push(item);
    return item.value;
  });
  return items;
}
