import { createContext, createEffect, createMemo, createSignal, createUniqueId, onCleanup, runWithOwner, untrack, useContext } from "solid-js";
import { createDefaultValidityData } from "../internals/field-context.jsx";
import { useFormContext } from "../internals/form-context.jsx";
import { makeSlotRefDirective } from "../internals/make-slot-ref-directive.jsx";
import { readOption } from "../internals/maybe-accessor.jsx";
import { mergeRootProps } from "../internals/merge-root-props.jsx";
import { SNAPSHOT_KEYS, snapshotKeys } from "../internals/observability/snapshot.jsx";
import { useFieldValidation } from "../internals/use-field-validation/use-field-validation.jsx";

/**
 * Why a value/touched/lifecycle change happened. Emitted as the third
 * argument of `onValueChange(next, event, reason)` and accepted by
 * imperative setters.
 *
 * - `"input"` — change originated from the control's `onInput` handler.
 * - `"reset"` — change originated from {@link FieldApi.reset}.
 * - `"imperative"` — programmatic change with no originating DOM event,
 *   or caller-supplied reason not covered above.
 */

/**
 * Options for {@link createField}.
 *
 * Everything reactive accepts `MaybeAccessor<T>` so a consumer can flip
 * `disabled`/`invalid`/`name` etc. at runtime without re-creating the
 * primitive.
 */

/**
 * Public API returned by {@link createField}. Flat accessors at the root —
 * no `state` blob.
 *
 * The `[SNAPSHOT_KEYS]` symbol is attached post-construction (NOT declared
 * on this interface; see `primitive-template.md` §2.5) so `snapshot(c)`
 * returns a debug-friendly plain-object view of the field's logical state.
 */

/**
 * Bridge surface exposed on `api._internal`. Consumed by `<Field.Root>` to
 * compose the `FieldContextValue` without duplicating state. **Not part of
 * the public API** — the leading underscore signals intent; downstream code
 * should use `FieldApi` members directly.
 */

/**
 * Shape of attributes emitted by `c.control()`. Intersected with any
 * caller-supplied extras at the call site.
 */

const FieldApiContext = createContext(null);

/**
 * Read the nearest {@link FieldApi} provided by `c.Provider`. Throws if
 * called outside one — match the
 * `useCollapsible()` / `useToggle()` pattern.
 */
export function useField() {
  const ctx = useContext(FieldApiContext);
  if (ctx == null) {
    throw new Error("Base UI: useField() must be called inside a <c.Provider>.");
  }
  return ctx;
}

/**
 * Solid 2.0-native primitive for a single form field. Owns value
 * (controlled/uncontrolled), interaction lifecycle (touched/dirty/
 * filled/focused), and the validation pipeline. Composes
 * `useFieldValidation` under the hood — async validators (`async fn`,
 * `zod.parseAsync`, server-side checks) are first-class and gate the
 * surrounding `<Form>`'s submit when used inside one.
 *
 * Consumers can drive markup either via `c.control()` (slot getter-bag
 * spread onto an `<input>`) or via `c.controlRef()` (directive form, for
 * cva-style variants and any consumer-owned JSX). Both surfaces wire the
 * same internal handlers.
 *
 * When called inside a `<Form>`, the field auto-registers into the form's
 * fields map under the generated control id so submit-time validation
 * can find it. Outside a Form, the field works standalone.
 */
export function createField(options = {}) {
  const form = useFormContext();

  // ---------- value: consumer-owned signal ----------
  // Solid-native shape (2026-04-30, §4.1): no internal signal, no
  // defaultValue. Consumer brings the accessor; `setValue` is a pure
  // event-emitter that fires `onValueChange`. The consumer's signal
  // write is the only writer.
  //
  // We capture the initial value once at setup so `reset()` has a
  // target to fire `onValueChange(initial, …, "reset")` against; the
  // consumer is then expected to update their signal in response.
  const initialValueAtSetup = untrack(() => readOption(options.value));
  const value = createMemo(() => readOption(options.value), {
    name: "field.value"
  });
  const setValue = (next, event, reason = "imperative") => {
    const ev = event ?? new Event("valuechange", {
      cancelable: true
    });
    // Use runWithOwner(null, ...) to escape the current reactive owner so
    // that consumer signal writes inside onValueChange don't trigger
    // SIGNAL_WRITE_IN_OWNED_SCOPE. Event-emitter pattern — exits the
    // reactive graph intentionally.
    runWithOwner(null, () => options.onValueChange?.(next, ev, reason));
  };

  // ---------- lifecycle flip-flops ----------
  const [internalDirty, setInternalDirty] = createSignal(false, {
    ownedWrite: true,
    name: "field.dirty"
  });
  const [internalTouched, setInternalTouched] = createSignal(false, {
    ownedWrite: true,
    name: "field.touched"
  });
  const [filled, setFilledInner] = createSignal(false, {
    ownedWrite: true,
    name: "field.filled"
  });
  const [focused, setFocusedInner] = createSignal(false, {
    ownedWrite: true,
    name: "field.focused"
  });
  const dirty = createMemo(() => readOption(options.dirty) ?? internalDirty(), {
    name: "field.dirtyResolved"
  });
  const touched = createMemo(() => readOption(options.touched) ?? internalTouched(), {
    name: "field.touchedResolved"
  });

  // `markedDirtyRef` gates valueMissing: native `required` errors only
  // surface once the user has touched the field, so a fresh form
  // doesn't render every empty required field as invalid on mount.
  // Plain mutable ref — NOT a signal. Mirrors React's pattern.
  const markedDirtyRef = {
    current: false
  };
  const setDirty = next => {
    if (readOption(options.dirty) !== undefined) return;
    if (next) markedDirtyRef.current = true;
    setInternalDirty(next);
  };
  const setTouched = next => {
    if (readOption(options.touched) !== undefined) return;
    setInternalTouched(next);
  };
  const setFilled = next => {
    setFilledInner(next);
  };
  const setFocused = next => {
    setFocusedInner(next);
  };

  // ---------- disabled / invalid ----------
  const disabled = createMemo(() => Boolean(readOption(options.disabled)), {
    name: "field.disabled"
  });
  const name = () => readOption(options.name);

  // `invalid` combines the consumer override with any Form-level error
  // for this field's name. Mirrors `<Field.Root>`'s logic.
  const invalid = createMemo(() => {
    if (readOption(options.invalid) === true) return true;
    const n = name();
    if (!n) return false;
    const errs = form.errors();
    return Object.hasOwn(errs, n) && errs[n] !== undefined;
  }, {
    name: "field.invalid"
  });

  // ---------- validation mode ----------
  const validationMode = createMemo(() => readOption(options.validationMode) ?? form.validationMode(), {
    name: "field.validationMode"
  });
  const validationDebounceTime = () => readOption(options.validationDebounceTime) ?? 0;
  const shouldValidateOnChange = createMemo(() => {
    const mode = validationMode();
    return mode === "onChange" || mode === "onSubmit" && form.submitAttemptedRef.current;
  }, {
    name: "field.shouldValidateOnChange"
  });

  // ---------- validity ----------
  const [validityData, setValidityDataInner] = createSignal(createDefaultValidityData(), {
    ownedWrite: true,
    name: "field.validityData"
  });
  const setValidityData = next => {
    setValidityDataInner(next);
  };
  const valid = createMemo(() => {
    if (invalid()) return false;
    return validityData().state.valid;
  }, {
    name: "field.valid"
  });
  const errors = createMemo(() => validityData().errors, {
    name: "field.errors"
  });
  const error = createMemo(() => validityData().error, {
    name: "field.error"
  });

  // `validating`: in-flight counter for async commits. Increments at the
  // start of every commit, decrements after the await chain settles.
  // Counter (not boolean) so concurrent calls don't false-resolve to
  // `false` when an inner one settles before the outer.
  const [inFlightCount, setInFlightCount] = createSignal(0, {
    ownedWrite: true,
    name: "field.inFlightCount"
  });
  const validating = createMemo(() => inFlightCount() > 0, {
    name: "field.validating"
  });

  // ---------- composed validation pipeline ----------
  // `useFieldValidation` is the engine. Wrap its `commit` so we can
  // track in-flight count for the `validating` accessor.
  const validateAccessor = () => options.validate;

  // Stable controlId. `useFieldValidation` keys back into Form's fields
  // map under this; it's also stamped on the `<input>` we render via
  // `c.control()` so server-error lookup-by-name and DOM-id-by-control
  // stay aligned.
  //
  // Signal (not constant) so the structural FieldRoot bridge can swap
  // in the actual DOM input id when `<Field.Control>` registers via
  // `registerFieldControl` (the labelable-provider id pool gives the
  // input its own id; the form-fields-map key must match it so
  // `formRef.fields.has(input.id)` finds the entry).
  const fallbackControlId = createUniqueId();
  const [controlIdSignal, setControlIdSignal] = createSignal(fallbackControlId, {
    ownedWrite: true,
    name: "field.controlId"
  });
  const controlIdAccessor = () => controlIdSignal();

  // Named state memo — shared with useFieldValidation and exposed via
  // `_internal.state` so FieldRoot can build FieldContextValue.state.
  const state = createMemo(() => ({
    disabled: disabled(),
    touched: touched(),
    dirty: dirty(),
    valid: valid(),
    filled: filled(),
    focused: focused()
  }), {
    name: "field.state"
  });
  const validation = useFieldValidation({
    setValidityData,
    validate: validateAccessor,
    validityData,
    validationDebounceTime,
    invalid,
    markedDirtyRef,
    state,
    name,
    controlId: controlIdAccessor,
    shouldValidateOnChange
  });
  const trackedCommit = (v, revalidate) => {
    setInFlightCount(c => c + 1);
    const p = validation.commit(v, revalidate);
    // `validation.commit` always returns a Promise (the underlying
    // commit body is async); decrement once it settles, regardless of
    // outcome. We do NOT await here — caller decides whether to.
    p.finally(() => setInFlightCount(c => c - 1));
    return p;
  };

  // ---------- imperative validate ----------
  const validate = event => {
    // Synthesize an event purely for parity with `setValue` / `reset`'s
    // shape — we don't currently expose a pre-validate hook, but we
    // honor `defaultPrevented` if a consumer wires one.
    const ev = event ?? new Event("validate", {
      cancelable: true
    });
    if (ev.defaultPrevented) return;
    markedDirtyRef.current = true;
    const userValidate = options.validate;
    const isAsync = userValidate?.constructor.name === "AsyncFunction";
    const result = trackedCommit(untrack(value));
    if (isAsync) return result;
    return undefined;
  };

  // ---------- reset ----------
  // Fire `onValueChange(initialValue, event, "reset")` against the value
  // captured at setup; the consumer is expected to update their signal
  // in response. createField does NOT write the value itself — same
  // rule as setValue. Lifecycle flags + validity ARE cleared internally
  // since they're owned here.
  const reset = event => {
    const ev = event ?? new Event("reset", {
      cancelable: true
    });
    // runWithOwner(null) escapes the reactive owner so consumer signal
    // writes inside onValueChange don't trigger SIGNAL_WRITE_IN_OWNED_SCOPE.
    runWithOwner(null, () => options.onValueChange?.(initialValueAtSetup, ev, "reset"));
    setInternalDirty(false);
    setInternalTouched(false);
    setFilledInner(false);
    setFocusedInner(false);
    markedDirtyRef.current = false;
    setValidityDataInner(createDefaultValidityData(initialValueAtSetup ?? null));
  };

  // ---------- form integration ----------
  // Whenever this primitive lives inside a `<Form>`, register a
  // `FormFieldEntry` so submit-time validation can find us. The entry
  // is keyed by `controlId`. We re-write on every value change so the
  // snapshot the form captures stays current.
  // Always register — even when `form.present === false` (i.e. no
  // `<Form>` ancestor), the consumer-supplied noop context has its own
  // `formRef.current.fields` Map that test fixtures and external consumers
  // inspect via `useFieldContext` lookups. Skipping the write would break
  // the "test registers the control into Form's fields map under its id"
  // contract that field-root previously enforced unconditionally.
  {
    const fallbackControlRef = {
      current: null
    };
    const entry = () => ({
      name: name(),
      getValue: () => untrack(value),
      controlRef: validation.inputRef ?? fallbackControlRef,
      validityData: validityData(),
      validate: () => {
        // Match `validateControl()` in field-root: detect async via
        // the user's validator shape and only return the Promise to
        // Form when one is in-flight, so all-sync forms keep their
        // React-parity sync submit.
        markedDirtyRef.current = true;
        const userValidate = options.validate;
        const isAsync = userValidate?.constructor.name === "AsyncFunction";
        const r = trackedCommit(untrack(value));
        if (isAsync) return r;
        return undefined;
      }
    });

    // Track the previous key so a controlId swap (when FieldRoot's
    // bridge calls `_internal.setControlId(input.id)`) can delete the
    // old entry before writing under the new key.
    let lastKey;
    const writeEntry = () => {
      const key = controlIdSignal();
      if (lastKey !== undefined && lastKey !== key) {
        form.formRef.current.fields.delete(lastKey);
      }
      form.formRef.current.fields.set(key, entry());
      lastKey = key;
    };

    // Initial registration.
    untrack(writeEntry);
    // Refresh on controlId / value / name / validity change so the
    // snapshot is current and the key tracks the registered control id.
    createEffect(() => {
      controlIdSignal();
      value();
      name();
      validityData();
    }, () => {
      untrack(writeEntry);
    });
    onCleanup(() => {
      if (lastKey !== undefined) {
        form.formRef.current.fields.delete(lastKey);
      }
    });
  }

  // ---------- bridge: initial-value sync for FieldRoot ----------
  // When `<Field.Root>` wraps this primitive, `<Field.Control>` registers
  // itself via `registerFieldControl`. FieldRoot calls `pushInitialValue`
  // so the first-known value is seeded into `validityData.initialValue`
  // (mirrors the old `syncInitialValue` in FieldRoot).
  const pushInitialValue = registration => {
    if (!registration) return;
    untrack(() => {
      let initialValue = registration.value;
      if (initialValue === undefined && registration.getValue) {
        initialValue = registration.getValue();
      }
      const cur = validityData();
      if (cur.initialValue === null && initialValue !== null) {
        setValidityData({
          ...cur,
          initialValue
        });
      }
    });
  };

  // ---------- control slot ----------
  const controlInternal = {
    get id() {
      return controlIdSignal();
    },
    get name() {
      return name();
    },
    get disabled() {
      return disabled();
    },
    get "aria-invalid"() {
      return valid() === false ? "true" : "false";
    },
    onFocus(_event) {
      setFocused(true);
    },
    onBlur(event) {
      setTouched(true);
      setFocused(false);
      if (validationMode() === "onBlur") {
        void trackedCommit(event.currentTarget.value);
      }
    },
    onInput(event) {
      const next = event.currentTarget.value;
      setDirty(next !== validityData().initialValue);
      setFilled(next !== "");
      // Mirror the value through the controlled-or-internal channel.
      // `setValue` honors the `event.preventDefault()` veto and
      // dispatches `onValueChange` with the live `InputEvent`.
      setValue(next, event, "input");
      // Validation-on-change lives in `useFieldValidation`'s
      // `getInputValidationProps().onInput` — but since we're not
      // splatting that handler (we own the control bag here), do
      // the equivalent dispatch ourselves: clear form-level errors
      // for our name, then commit either with revalidate=true (when
      // not validating on change) or directly (when on-change).
      form.clearErrors(name());
      if (!shouldValidateOnChange()) {
        void trackedCommit(next, true);
        return;
      }
      if (next === "") {
        // Empty bypasses debounce so `required` clears immediately.
        void trackedCommit(next);
        return;
      }
      const debounce = validationDebounceTime();
      if (debounce > 0) {
        // We don't ship a timer instance here — keep the simple
        // path: trigger the commit. Consumers wanting debounced
        // validation should use the structural <Field.Control>
        // which composes `useFieldValidation`'s debounced
        // handler. createField's slot is the no-debounce path.
        void trackedCommit(next);
      } else {
        void trackedCommit(next);
      }
    }
  };
  const control = userProps => mergeRootProps(controlInternal, userProps);
  const controlRef = () => {
    const baseDirective = makeSlotRefDirective(() => control());
    // The validation pipeline needs access to the live element for
    // `element.validity` reads + `setCustomValidity()` writes. Layer
    // the inputRef.current assignment on top of the slot-ref
    // directive's own ref-application step.
    return el => {
      validation.inputRef.current = el;
      baseDirective(el);
    };
  };

  // ---------- assemble api ----------
  const api = {
    value,
    setValue,
    disabled,
    invalid,
    dirty,
    touched,
    filled,
    focused,
    setDirty,
    setTouched,
    setFilled,
    setFocused,
    validity: validityData,
    valid,
    validating,
    errors,
    error,
    validate,
    reset,
    control,
    controlRef,
    Provider: props => <FieldApiContext value={api}>
				{props.children}
			</FieldApiContext>,
    _internal: {
      validateFn: validateAccessor,
      validation,
      markedDirtyRef,
      validationMode,
      validationDebounceTime,
      shouldValidateOnChange,
      setValidityData,
      state,
      pushInitialValue,
      setControlId: id => {
        setControlIdSignal(id);
      }
    }
  };
  api[SNAPSHOT_KEYS] = FIELD_SNAPSHOT_KEYS;
  return api;
}
const FIELD_SNAPSHOT_KEYS = snapshotKeys("value", "disabled", "invalid", "dirty", "touched", "filled", "focused", "valid", "validating", "errors", "error");
//# sourceMappingURL=create-field.jsx.map
