import { mount, unmount } from 'svelte';
import type { Component } from 'svelte';
import FormFieldContextSelector from './FormFieldContextSelector.svelte';
import FormFieldScopeSubscriptions from './FormFieldScopeSubscriptions.svelte';
import FormFieldAgentContext from './FormFieldAgentContext.svelte';

/**
 * Module-level value store that survives bridge re-creation.
 *
 * FlowDrop's $.component() may destroy and re-create our bridge function
 * whenever props change. We persist the user's current value here so the
 * new bridge instance picks it up instead of reading the stale $$props.value.
 */
const valueStore = new Map<string, string>();

/**
 * Mount a string-valued field component with the given value.
 */
function mountStringField(
  Comp: Component<any>,
  container: HTMLDivElement,
  fieldId: string,
  value: string,
  $$props: Record<string, any>,
  onChange: (val: unknown) => void,
) {
  return mount(Comp, {
    target: container,
    props: {
      id: fieldId,
      value,
      required: $$props.required ?? false,
      ariaDescribedBy: $$props.ariaDescribedBy,
      onChange,
    },
  });
}

/**
 * Builds a bridge for a string-valued field component.
 *
 * Persistence strategy:
 *  - A hidden <input> is created directly in the DOM (outside Svelte)
 *    so ConfigForm.handleSave() can find it via querySelectorAll().
 *  - We call $$props.onChange to update ConfigForm.configValues so that
 *    handleFormBlur → workflow store sync includes our value.
 */
function createStringFieldBridge(
  Comp: Component<any>,
  dataAttrs: Record<string, string> = {},
) {
  return function StringFieldBridge(
    $$anchor: Comment,
    $$props: Record<string, any>,
  ) {
    const fieldId = $$props.id ?? '';

    let currentValue = valueStore.get(fieldId) ?? String($$props.value ?? '');

    const hiddenInput = document.createElement('input');
    hiddenInput.type = 'hidden';
    hiddenInput.id = fieldId;
    hiddenInput.value = currentValue;
    for (const [k, v] of Object.entries(dataAttrs)) {
      hiddenInput.dataset[k] = v;
    }
    $$anchor.parentNode!.insertBefore(hiddenInput, $$anchor);

    const container = document.createElement('div');
    container.style.display = 'contents';
    $$anchor.parentNode!.insertBefore(container, $$anchor);

    const onChange = (val: unknown) => {
      const strVal = String(val ?? '');
      valueStore.set(fieldId, strVal);
      hiddenInput.value = strVal;
      $$props.onChange(strVal);
      container.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
    };

    let app = mountStringField(
      Comp,
      container,
      fieldId,
      currentValue,
      $$props,
      onChange,
    );

    // Re-read deferred value after ConfigForm's effects flush.
    if (!currentValue) {
      requestAnimationFrame(() => {
        if (!$$anchor.isConnected) return;
        const deferredValue = String($$props.value ?? '');
        if (deferredValue && deferredValue !== currentValue) {
          unmount(app);
          container.innerHTML = '';
          currentValue = deferredValue;
          valueStore.set(fieldId, deferredValue);
          hiddenInput.value = deferredValue;
          app = mountStringField(
            Comp,
            container,
            fieldId,
            deferredValue,
            $$props,
            onChange,
          );
        }
      });
    }

    // Cleanup when FlowDrop removes the anchor from the DOM.
    const observer = new MutationObserver(() => {
      if (!$$anchor.isConnected) {
        unmount(app);
        hiddenInput.remove();
        valueStore.delete(fieldId);
        observer.disconnect();
      }
    });
    if ($$anchor.parentNode) {
      observer.observe($$anchor.parentNode, { childList: true });
    }
  };
}

const ContextSelectorBridge = createStringFieldBridge(FormFieldContextSelector);
// Tag the agent field so the scope widget can detect when inheritance is on.
const AgentContextBridge = createStringFieldBridge(FormFieldAgentContext, {
  fdAicRole: 'agent_id',
});

/**
 * Per-field store for object-valued widgets (scope subscriptions).
 *
 * Mirrors valueStore but holds the structured value so it survives bridge
 * re-creation without being flattened to a string.
 */
const objectValueStore = new Map<string, Record<string, string[]>>();

/**
 * Parses an incoming widget value (object or JSON string) into a clean map.
 */
function parseScopeValue(raw: unknown): Record<string, string[]> {
  if (typeof raw === 'string') {
    if (!raw.trim()) return {};
    try {
      raw = JSON.parse(raw);
    } catch {
      return {};
    }
  }
  if (!raw || typeof raw !== 'object') return {};
  const out: Record<string, string[]> = {};
  for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {
    if (Array.isArray(val) && val.length) {
      out[key] = val.map((v) => String(v));
    }
  }
  return out;
}

/**
 * Bridge for the scope-subscriptions widget.
 *
 * Unlike the context selector, the value is an object. The hidden input holds
 * a JSON string for ConfigForm.handleSave()'s querySelectorAll fallback, while
 * onChange receives the structured object so configValues stays typed.
 */
function ScopeSubscriptionsBridge(
  $$anchor: Comment,
  $$props: Record<string, any>,
) {
  const fieldId = $$props.id ?? '';

  let currentValue =
    objectValueStore.get(fieldId) ?? parseScopeValue($$props.value);

  const hiddenInput = document.createElement('input');
  hiddenInput.type = 'hidden';
  hiddenInput.id = fieldId;
  hiddenInput.value = JSON.stringify(currentValue);
  $$anchor.parentNode!.insertBefore(hiddenInput, $$anchor);

  const container = document.createElement('div');
  container.style.display = 'contents';
  $$anchor.parentNode!.insertBefore(container, $$anchor);

  // Note shown instead of the widget when an agent is inherited (its scopes
  // take over). Keeps manual scopes and agent inheritance mutually exclusive.
  const note = document.createElement('p');
  note.textContent =
    'Scopes are inherited from the selected agent. Clear the agent below to set scopes manually.';
  note.style.cssText =
    'margin:0;color:var(--fd-text-muted,#667085);font-size:0.75rem;';
  note.hidden = true;
  $$anchor.parentNode!.insertBefore(note, $$anchor);

  // Mirror the backend rule: hide the manual scope widget only when the
  // selected agent actually carries CCC config (config agents). Plugin agents
  // contribute nothing, so the manual scopes must stay visible. Best-effort
  // DOM coordination; if the agent field is absent the widget stays visible.
  let lastAgentId: string | null = null;
  let lastInherited = false;

  const applyVisibility = (inherited: boolean): void => {
    container.style.display = inherited ? 'none' : 'contents';
    note.hidden = !inherited;
  };

  const currentAgentId = (): string => {
    const agentInput = document.querySelector<HTMLInputElement>(
      'input[data-fd-aic-role="agent_id"]',
    );
    return agentInput?.isConnected ? agentInput.value.trim() : '';
  };

  const syncVisibility = (): void => {
    const agentId = currentAgentId();
    if (agentId === lastAgentId) {
      applyVisibility(lastInherited);
      return;
    }
    lastAgentId = agentId;
    if (!agentId) {
      lastInherited = false;
      applyVisibility(false);
      return;
    }
    fetch(
      `/api/flowdrop/ai-context-agent/${encodeURIComponent(agentId)}/context-url`,
    )
      .then((r) => (r.ok ? r.json() : {}))
      .then((j) => {
        // Ignore stale responses if the agent changed meanwhile.
        if (currentAgentId() !== agentId) return;
        lastInherited = !!j.has_context_config;
        applyVisibility(lastInherited);
      })
      .catch(() => {
        lastInherited = false;
        applyVisibility(false);
      });
  };
  // Re-check whenever a field in the panel changes (the agent bridge dispatches
  // focusout on select/clear) and once after initial paint.
  document.addEventListener('focusout', syncVisibility);
  requestAnimationFrame(syncVisibility);

  const onChange = (val: unknown) => {
    const obj = parseScopeValue(val);
    objectValueStore.set(fieldId, obj);
    hiddenInput.value = JSON.stringify(obj);
    $$props.onChange(obj);
    container.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
  };

  const mountWidget = (val: Record<string, string[]>) =>
    mount(FormFieldScopeSubscriptions, {
      target: container,
      props: {
        id: fieldId,
        value: val,
        ariaDescribedBy: $$props.ariaDescribedBy,
        onChange,
      },
    });

  let app = mountWidget(currentValue);

  // Re-read deferred value after ConfigForm's effects flush. The saved config
  // value often arrives after the initial mount, so re-mount with it once it
  // appears — otherwise a saved selection renders as "0 selected".
  if (Object.keys(currentValue).length === 0) {
    requestAnimationFrame(() => {
      if (!$$anchor.isConnected) return;
      const deferred = parseScopeValue($$props.value);
      if (Object.keys(deferred).length > 0) {
        unmount(app);
        container.innerHTML = '';
        currentValue = deferred;
        objectValueStore.set(fieldId, deferred);
        hiddenInput.value = JSON.stringify(deferred);
        app = mountWidget(deferred);
      }
    });
  }

  const observer = new MutationObserver(() => {
    if (!$$anchor.isConnected) {
      unmount(app);
      hiddenInput.remove();
      note.remove();
      document.removeEventListener('focusout', syncVisibility);
      objectValueStore.delete(fieldId);
      observer.disconnect();
    }
  });
  if ($$anchor.parentNode) {
    observer.observe($$anchor.parentNode, { childList: true });
  }
}

/**
 * Field bridges, as [registry name, schema `format` matched, bridge].
 */
const FIELD_COMPONENTS: Array<[string, string, unknown]> = [
  [
    'flowdrop-ai-context:context-selector',
    'ai_context_item_selector',
    ContextSelectorBridge,
  ],
  [
    'flowdrop-ai-context:scope-subscriptions',
    'ai_context_scope_subscriptions',
    ScopeSubscriptionsBridge,
  ],
  [
    'flowdrop-ai-context:agent-context',
    'ai_context_agent_selector',
    AgentContextBridge,
  ],
];

/**
 * Register the custom field components with the FlowDrop field registry.
 *
 * FlowDrop 2.0 made the field registry instance-scoped — `fd.fields` on the
 * page-default instance — and dropped the `window.FlowDrop
 * .fieldComponentRegistry` singleton that 1.x exposed (the registry API itself
 * is unchanged). Reach it through the editor module, which is already loaded by
 * the time the editor dispatches its init event.
 */
async function registerFieldComponents(): Promise<boolean> {
  const FlowDrop = (window as any).FlowDrop;
  if (typeof FlowDrop?.getEditorModule !== 'function') {
    return false;
  }

  const editor = await FlowDrop.getEditorModule();
  const registry = editor?.getDefaultInstance?.()?.fields;
  if (!registry) {
    return false;
  }

  for (const [name, format, component] of FIELD_COMPONENTS) {
    registry.register(name, {
      matcher: (schema: Record<string, unknown>) => schema.format === format,
      component,
      priority: 100,
    });
  }

  return true;
}

/**
 * Events that announce a FlowDrop surface is ready to accept registrations.
 *
 * Each surface dispatches its own: the editor, the playground
 * (flowdrop_ui_components/components/playground/playground.js) and standalone
 * schema forms (components/schema-form/schema-form.js). Listening to only one
 * leaves the others rendering built-in widgets.
 */
const READY_EVENTS = [
  'flowdrop:editor:init',
  'flowdrop:playground:init',
  'flowdrop:schemaform:init',
];

/**
 * Registers on the first surface that comes up, retrying on later events.
 *
 * The latch is set only once registration actually succeeded. Latching on the
 * attempt instead would mean a first event that arrives before the editor
 * module is resolvable permanently disables the widgets — the same silent
 * failure this file exists to avoid. Re-registering is harmless: the registry
 * is keyed by name.
 */
let registered = false;

async function attemptRegistration(): Promise<void> {
  if (registered) {
    return;
  }
  registered = await registerFieldComponents();
}

for (const event of READY_EVENTS) {
  document.addEventListener(event, () => {
    void attemptRegistration();
  });
}
