import type { InjectionKey } from 'vue';
import type { Breakpoint, ControlDef, Settings, ZElement } from './types';
import { generateElementCss, generateSettingsCss, generateTreeCss } from './css';
import { reassignIds, walk } from './document';

/**
 * Components and global widgets (T168 / T210).
 *
 * A component is a saved subtree (post type `zadex_component`). A document places it with an element of type
 * `component` whose settings are `{ component_id, overrides: { [propId]: value } }` and that has no children.
 * A "global widget" is a component whose root is a single widget (kind `widget`).
 *
 * Rendering an instance (editor: widgets/component/Preview.vue, PHP: Zadex\Components\InstanceRenderer):
 *  - the component tree is cloned, the exposed properties (`props`) take the instance's override values,
 *  - every inner element id `x` becomes `<instanceId>-x` (stable per instance, so HTML ids / aria links of
 *    widgets never collide between two instances),
 *  - the instance's own attributes (classes `zx-component zx-<instanceId>`, custom classes, id, motion …) are
 *    merged onto the first root element; there is no wrapper.
 * CSS: the component's CSS is generated once with its own element ids (`.zx-x`, PHP also prints the class
 * `zx-x` on the rendered element); the page carries per-instance CSS only: the instance's own Advanced
 * settings (`.zx-component.zx-<I>`) and the style overrides (`.zx-<I> .zx-<I>-x`, root: `.zx-<I>.zx-<I>-x`).
 * Both use two classes so they beat the component's one-class rules wherever the component CSS is printed.
 */
export type ComponentKind = 'component' | 'widget';

/** A control of an inner element exposed as an instance property ("Expose as property"). */
export interface ComponentProp {
  /** Stable property id (key in the instance's `overrides`). */
  id: string;
  label: string;
  /** Inner element id (the component's own id, not an instance-prefixed one). */
  elementId: string;
  /** Control id of that element's widget (base key: desktop / normal state). */
  control: string;
}

export interface ComponentSummary {
  id: number;
  title: string;
  kind: ComponentKind;
  /** Number of posts that place the component (usage index, WordPress only). */
  usage?: number;
  modified?: string;
}

export interface ComponentData extends ComponentSummary {
  elements: ZElement[];
  css: string;
  props: ComponentProp[];
}

/** `window.ZADEX.component` when the editor opens a component on its own (`?zadex=edit` on the CPT post). */
export interface ComponentDocConfig {
  id: number;
  kind: ComponentKind;
  title: string;
}

export const COMPONENT_TYPE = 'component';
/** Page-settings key that holds the exposed properties while a component is the edited document. */
export const PROPS_KEY = 'component_props';

export interface InstanceSettings {
  component_id: number;
  overrides: Record<string, unknown>;
}

export function isInstance(el: ZElement | null | undefined): el is ZElement {
  return Boolean(el) && el!.type === COMPONENT_TYPE;
}

export function instanceSettings(el: ZElement): InstanceSettings {
  const s = el.settings ?? {};
  const id = Number(s.component_id);
  const overrides = s.overrides && typeof s.overrides === 'object' && !Array.isArray(s.overrides) ? (s.overrides as Record<string, unknown>) : {};
  return { component_id: Number.isFinite(id) && id > 0 ? Math.floor(id) : 0, overrides };
}

/** Kind of a subtree saved as a component: a single non-container widget is a global widget. */
export function kindOf(elements: ZElement[], isContainer: (type: string) => boolean): ComponentKind {
  return elements.length === 1 && !elements[0].children.length && !isContainer(elements[0].type) ? 'widget' : 'component';
}

/** Id of an inner element inside instance `instanceId`. */
export function prefixedId(instanceId: string, innerId: string): string {
  return `${instanceId}-${innerId}`;
}

function clone<T>(value: T): T {
  try {
    return structuredClone(value);
  } catch {
    return JSON.parse(JSON.stringify(value)) as T;
  }
}

/** Valid, unique props (drops broken entries, keeps the first of duplicate ids). */
export function sanitizeProps(input: unknown): ComponentProp[] {
  if (!Array.isArray(input)) return [];
  const seen = new Set<string>();
  const out: ComponentProp[] = [];
  for (const raw of input) {
    if (!raw || typeof raw !== 'object') continue;
    const p = raw as Record<string, unknown>;
    const id = typeof p.id === 'string' ? p.id.replace(/[^a-zA-Z0-9_-]/g, '') : '';
    const elementId = typeof p.elementId === 'string' ? p.elementId : '';
    const control = typeof p.control === 'string' ? p.control : '';
    if (!id || !elementId || !control || seen.has(id)) continue;
    seen.add(id);
    out.push({ id, label: typeof p.label === 'string' && p.label.trim() ? p.label.trim().slice(0, 80) : control, elementId, control });
  }
  return out;
}

/** Property id for a newly exposed control: `<control>_<n>` unique among `props`. */
export function newPropId(control: string, props: ComponentProp[]): string {
  const base = control.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^_+/, '') || 'prop';
  const taken = new Set(props.map((p) => p.id));
  if (!taken.has(base)) return base;
  let n = 2;
  while (taken.has(`${base}_${n}`)) n++;
  return `${base}_${n}`;
}

export function findProp(props: ComponentProp[], elementId: string, control: string): ComponentProp | undefined {
  return props.find((p) => p.elementId === elementId && p.control === control);
}

/** Finds an element of a (component) tree by id. */
export function findIn(elements: ZElement[], id: string): ZElement | null {
  let hit: ZElement | null = null;
  walk(elements, (el) => {
    if (el.id === id) {
      hit = el;
      return false;
    }
  });
  return hit;
}

/**
 * Deep copy of the component tree with the override values written into the exposed controls (base key).
 * Overrides of props that no longer exist, or that point at a removed element, are ignored.
 */
export function applyOverrides(elements: ZElement[], props: ComponentProp[], overrides: Record<string, unknown>): ZElement[] {
  const copy = clone(elements);
  for (const prop of props) {
    if (!Object.prototype.hasOwnProperty.call(overrides, prop.id)) continue;
    const value = overrides[prop.id];
    if (value === undefined) continue;
    const el = findIn(copy, prop.elementId);
    if (el) el.settings = { ...el.settings, [prop.control]: clone(value) };
  }
  return copy;
}

/**
 * The tree an instance renders in the editor canvas: overrides applied and every id prefixed with the instance
 * id (`<I>-<id>`), so each instance generates its own per-element CSS and never shares ids with another one.
 */
export function deriveInstanceTree(instanceId: string, data: Pick<ComponentData, 'elements' | 'props'>, overrides: Record<string, unknown> = {}): ZElement[] {
  const tree = applyOverrides(data.elements, data.props, overrides);
  walk(tree, (el) => {
    el.id = prefixedId(instanceId, el.id);
    if (!Array.isArray(el.children)) el.children = [];
  });
  return tree;
}

export type ControlsFor = (type: string) => Record<string, ControlDef>;
export type ComponentLookup = (id: number) => Pick<ComponentData, 'elements' | 'props'> | null | undefined;

/**
 * Per-instance CSS saved with the document that places the instance: the instance's own settings with a
 * two-class wrapper and the style overrides (only overridden controls that have a `css` map / CSS code).
 */
export function instanceCss(instance: ZElement, data: Pick<ComponentData, 'elements' | 'props'> | null | undefined, controlsFor: ControlsFor, breakpoints: Breakpoint[]): string {
  const out: string[] = [];
  const own = generateElementCss(instance, { controls: controlsFor(instance.type), breakpoints, wrapper: `.zx-component.zx-${instance.id}` });
  if (own) out.push(own);
  if (!data) return out.join('\n');
  const { overrides } = instanceSettings(instance);
  const roots = new Set(data.elements.map((el) => el.id));
  for (const prop of data.props) {
    if (!Object.prototype.hasOwnProperty.call(overrides, prop.id) || overrides[prop.id] === undefined) continue;
    const el = findIn(data.elements, prop.elementId);
    if (!el) continue;
    const controls = controlsFor(el.type);
    const control = controls[prop.control];
    if (!control || !control.css?.length) continue;
    const target = prefixedId(instance.id, el.id);
    const wrapper = roots.has(el.id) ? `.zx-${instance.id}.zx-${target}` : `.zx-${instance.id} .zx-${target}`;
    // Only the overridden control, with the element's other settings for `condition` checks.
    const settings: Settings = { ...el.settings, [prop.control]: overrides[prop.id] };
    const onlyThis: Record<string, ControlDef> = { ...controls };
    for (const key of Object.keys(onlyThis)) if (key !== prop.control) delete onlyThis[key];
    const css = generateSettingsCss(settings, { controls: onlyThis, breakpoints, wrapper });
    if (css) out.push(css);
  }
  return out.join('\n');
}

/** Instance elements of a tree (not descending into the component trees they place). */
export function collectInstances(elements: ZElement[]): ZElement[] {
  const out: ZElement[] = [];
  walk(elements, (el) => {
    if (isInstance(el)) out.push(el);
  });
  return out;
}

/** Component ids placed by a tree (usage index, recursion checks). */
export function usedComponentIds(elements: ZElement[]): number[] {
  const ids = new Set<number>();
  for (const el of collectInstances(elements)) {
    const id = instanceSettings(el).component_id;
    if (id) ids.add(id);
  }
  return [...ids];
}

/** CSS of every instance placed in a tree (added to the document's generated CSS). */
export function instancesCss(elements: ZElement[], lookup: ComponentLookup, controlsFor: ControlsFor, breakpoints: Breakpoint[]): string {
  return collectInstances(elements)
    .map((el) => instanceCss(el, lookup(instanceSettings(el).component_id), controlsFor, breakpoints))
    .filter(Boolean)
    .join('\n');
}

/** The CSS stored with a component (`_zadex_css` of the CPT): its own ids, plus its nested instances. */
export function componentCss(elements: ZElement[], lookup: ComponentLookup, controlsFor: ControlsFor, breakpoints: Breakpoint[]): string {
  return [generateTreeCss(elements, controlsFor, breakpoints), instancesCss(elements, lookup, controlsFor, breakpoints)].filter(Boolean).join('\n');
}

/** Settings of the instance element that are its own (Advanced tab), not the component link. */
export function ownSettings(instance: ZElement): Settings {
  const out: Settings = {};
  for (const [k, v] of Object.entries(instance.settings ?? {})) {
    if (k === 'component_id' || k === 'overrides') continue;
    out[k] = v;
  }
  return out;
}

/**
 * "Detach": the component tree as normal elements for this document: overrides applied, fresh ids that do not
 * collide with `taken`, the instance's own settings (spacing, classes …) merged onto the first root and its
 * label kept.
 */
export function detachElements(instance: ZElement, data: Pick<ComponentData, 'elements' | 'props'>, taken: Iterable<string> = []): ZElement[] {
  const { overrides } = instanceSettings(instance);
  const tree = reassignIds(applyOverrides(data.elements, data.props, overrides), taken);
  if (tree[0]) {
    tree[0].settings = { ...tree[0].settings, ...clone(ownSettings(instance)) };
    if (instance.classes?.length) tree[0].classes = [...new Set([...(tree[0].classes ?? []), ...instance.classes])];
    if (instance.label && !tree[0].label) tree[0].label = instance.label;
  }
  return tree;
}

/** True when placing component `id` inside the tree of component `hostId` would make it contain itself. */
export function wouldRecurse(hostId: number, id: number, lookup: ComponentLookup, seen = new Set<number>()): boolean {
  if (id === hostId) return true;
  if (seen.has(id)) return false;
  seen.add(id);
  const data = lookup(id);
  if (!data) return false;
  return usedComponentIds(data.elements).some((child) => wouldRecurse(hostId, child, lookup, seen));
}

/**
 * Canvas scope provided by widgets/component/Preview.vue to the elements it renders. `readonly`: the derived tree
 * of an instance (not selectable, no inline editing, no empty-container drop zones); `false` while the component
 * is edited in place. `chain`: component ids being rendered (recursion guard).
 */
export interface ComponentScope {
  readonly: boolean;
  chain: number[];
}

export const COMPONENT_SCOPE: InjectionKey<ComponentScope | null> = Symbol('zxComponentScope');

/* ---- lookup registry: the components store registers the cache so the document store can read it ---- */
let registeredLookup: ComponentLookup = () => null;

export function setComponentLookup(fn: ComponentLookup): void {
  registeredLookup = fn;
}

export function lookupComponent(id: number): Pick<ComponentData, 'elements' | 'props'> | null | undefined {
  return registeredLookup(id);
}
