import { onCleanup, untrack } from "solid-js";
import { DEFAULT_VALIDITY_STATE, getCombinedFieldValidityData } from "../field-constants.jsx";
import { useFormContext } from "../form-context.jsx";
import { mergePropsN } from "../merge-props-n.jsx";
import { ariaInvalid } from "../seed-aria-attr.jsx";
import { createStableCallback } from "../stable-callback.jsx";
import { useTimeout } from "../use-timeout.jsx";
const validityKeys = Object.keys(DEFAULT_VALIDITY_STATE);

/**
 * Native ValidityState may only report `valueMissing` (i.e. the control is
 * empty) — we don't want to "scold" the user for empty fields if other
 * native errors aren't present. This helper returns `true` when the only
 * thing wrong is `valueMissing`.
 */
function isOnlyValueMissing(state) {
  if (!state || state.valid || !state.valueMissing) {
    return false;
  }
  let onlyValueMissing = false;
  for (const key of validityKeys) {
    if (key === "valid") continue;
    if (key === "valueMissing") {
      onlyValueMissing = state[key];
      continue;
    }
    if (state[key]) {
      onlyValueMissing = false;
    }
  }
  return onlyValueMissing;
}
/**
 * Port of Base UI React's `useFieldValidation`
 * (`packages/react/src/field/root/useFieldValidation.ts`). Produces the
 * props-factories and the imperative `commit` function that the Field root
 * exposes to the rest of the field parts.
 *
 * Solid 2 notes:
 *
 * - Every parameter except the plain refs is an `Accessor<T>`. Callers read
 *   them through the stable-callback trampolines below, so invocation-time
 *   values always win.
 * - `commit` wraps a setValidityData write; the parent root is expected to
 *   route that through its own `createStableCallback`-wrapped setter so
 *   writes inside reactive scope are legal.
 * - React used `ReactDOM.flushSync` for synchronous submit validation; the
 *   Solid port calls `flush()` is NOT needed here because consumers do it
 *   themselves after invoking `validate`. The `flushSync` parameter is
 *   accepted for API parity with the React entry handled.
 */
export function useFieldValidation(params) {
  const {
    setValidityData,
    validate,
    validityData,
    validationDebounceTime,
    invalid,
    markedDirtyRef,
    state,
    name,
    controlId,
    shouldValidateOnChange
  } = params;
  const {
    formRef,
    clearErrors
  } = useFormContext();
  const timeout = useTimeout();
  const inputRef = {
    current: null
  };

  // Monotonic generation counter — bumped on each commit entry so any
  // later-started commit can invalidate the writes of an in-flight
  // predecessor. Last-start-wins: an await in commit#1 cannot land its
  // validity write if commit#2 has been started in the meantime. This
  // matches React's pattern (used in combination with React's mount-guard)
  // and prevents intermediate states from leaking between two racing
  // commits.
  let commitGeneration = 0;

  // Owner-dispose guard. `commit` is async and can outlive the owning
  // scope: if the consumer awaits a slow validate and the root disposes
  // before it resolves, we must not land a late `setValidityData` write.
  // `useFieldValidation` itself lives in the component body, so
  // `onCleanup` fires when the owner disposes.
  let disposed = false;
  onCleanup(() => {
    disposed = true;
  });

  // Core commit routine — wrapped below via `createStableCallback` so the
  // returned reference is referentially stable for consumers that capture
  // it in refs / closures.
  const commitImpl = async (value, revalidate = false) => {
    const myGeneration = ++commitGeneration;
    const isStale = () => disposed || myGeneration !== commitGeneration;
    if (disposed) return;
    const element = inputRef.current;
    if (!element) {
      return;
    }

    // `untrack` the reactive-state snapshot reads — `commitImpl` is an
    // imperative side-effect path called from event handlers and other
    // callers that may sit in unowned/effect-apply scope. The reads
    // here are point-in-time snapshots, not subscriptions.
    const currentValidity = untrack(validityData);
    const currentState = untrack(state);
    if (revalidate) {
      if (currentState.valid !== false) {
        return;
      }
      const currentNativeValidity = element.validity;
      if (!currentNativeValidity.valueMissing) {
        // The 'valueMissing' (required) condition has been resolved
        // by the user typing. Temporarily mark the field as valid
        // for this onChange event. Other native errors (e.g.
        // `typeMismatch`) will be caught by full validation on blur
        // or submit.
        const nextValidityData = {
          value,
          state: {
            ...DEFAULT_VALIDITY_STATE,
            valid: true
          },
          error: "",
          errors: [],
          initialValue: currentValidity.initialValue
        };
        element.setCustomValidity("");
        const id = controlId();
        if (id) {
          const currentFieldData = formRef.current.fields.get(id);
          if (currentFieldData) {
            // `getCombinedFieldValidityData` returns a `FieldValidityData`
            // (top-level: state/errors/error/value/initialValue). It must
            // land under the entry's `validityData` key, NOT spread onto
            // the entry — otherwise Form's submit path, which reads
            // `entry.validityData.state.valid`, sees the stale snapshot
            // captured at registration time.
            formRef.current.fields.set(id, {
              ...currentFieldData,
              validityData: getCombinedFieldValidityData(nextValidityData, false)
            });
          }
        }
        setValidityData(nextValidityData);
        return;
      }

      // Value is still missing, or other conditions apply. Build a
      // plain object representation of the native validity so
      // `isOnlyValueMissing` can inspect it without the live
      // `ValidityState` getter overhead.
      const currentNativeValidityObject = validityKeys.reduce((acc, key) => {
        acc[key] = currentNativeValidity[key];
        return acc;
      }, {});

      // If it's (still) natively invalid due to something other than
      // just valueMissing, bail from this revalidation-on-change to
      // avoid "scolding" the user for unrelated errors mid-typing.
      if (!currentNativeValidityObject.valid && !isOnlyValueMissing(currentNativeValidityObject)) {
        return;
      }

      // Otherwise fall through to the full validation path below.
    }
    function getState(el) {
      const computedState = validityKeys.reduce((acc, key) => {
        acc[key] = el.validity[key];
        return acc;
      }, {});
      let hasOnlyValueMissingError = false;
      for (const key of validityKeys) {
        if (key === "valid") continue;
        if (key === "valueMissing" && computedState[key]) {
          hasOnlyValueMissingError = true;
        } else if (computedState[key]) {
          return computedState;
        }
      }

      // Only let `valueMissing` mark the field invalid once the user
      // has touched it; otherwise every pristine required field
      // would render as "invalid" on mount.
      if (hasOnlyValueMissingError && !markedDirtyRef.current) {
        computedState.valid = true;
        computedState.valueMissing = false;
      }
      return computedState;
    }
    timeout.clear();
    let result = null;
    let validationErrors = [];
    const nextState = getState(element);
    let defaultValidationMessage;
    const validateOnChange = shouldValidateOnChange();
    if (element.validationMessage && !validateOnChange) {
      // Not validating on change: if there's a native
      // `validationMessage`, surface it directly and skip the
      // consumer-supplied validate fn.
      defaultValidationMessage = element.validationMessage;
      validationErrors = [element.validationMessage];
    } else {
      // Either validating on change, or native constraint validations
      // passed — call the custom validate fn next.
      const formValues = Array.from(formRef.current.fields.values()).reduce((acc, field) => {
        if (field.name) {
          acc[field.name] = field.getValue();
        }
        return acc;
      }, {});
      const validateFn = validate();
      if (validateFn) {
        const resultOrPromise = validateFn(value, formValues);
        if (typeof resultOrPromise === "object" && resultOrPromise !== null && "then" in resultOrPromise) {
          result = await resultOrPromise;
          // A newer commit started while we were awaiting — bail so
          // the later call's result (or lack thereof) wins. No
          // intermediate state gets published.
          if (isStale()) return;
        } else {
          result = resultOrPromise;
        }
      }
      if (result !== null) {
        nextState.valid = false;
        nextState.customError = true;
        if (Array.isArray(result)) {
          validationErrors = result;
          element.setCustomValidity(result.join("\n"));
        } else if (result) {
          validationErrors = [result];
          element.setCustomValidity(result);
        }
      } else if (validateOnChange) {
        // Custom validate returned no errors; if validating on
        // change we need to clear the previously-set custom
        // validity so the next native check reads clean.
        element.setCustomValidity("");
        nextState.customError = false;
        if (element.validationMessage) {
          defaultValidationMessage = element.validationMessage;
          validationErrors = [element.validationMessage];
        } else if (element.validity.valid && !nextState.valid) {
          nextState.valid = true;
        }
      }
    }
    const nextValidityData = {
      value,
      state: nextState,
      error: defaultValidationMessage ?? (Array.isArray(result) ? result[0] ?? "" : result ?? ""),
      errors: validationErrors,
      initialValue: currentValidity.initialValue
    };
    const id = controlId();
    if (id) {
      const currentFieldData = formRef.current.fields.get(id);
      if (currentFieldData) {
        // Land the combined snapshot under `validityData` (NOT spread
        // onto the entry's top-level keys) so Form's `finishSubmit`,
        // which reads `entry.validityData.state.valid`, sees the
        // fresh result. See sibling fix in the revalidate branch.
        formRef.current.fields.set(id, {
          ...currentFieldData,
          validityData: getCombinedFieldValidityData(nextValidityData, invalid())
        });
      }
    }
    setValidityData(nextValidityData);
  };
  const commit = createStableCallback(() => commitImpl);
  const getValidationProps = (externalProps = {}) => {
    // `mergePropsN` is the reactive analog to React's `mergeProps` — it
    // layers getters on top of each other so `aria-invalid` updates with
    // `state()`.
    return mergePropsN([ariaInvalid(() => state().valid === false), externalProps]);
  };
  const getInputValidationProps = (externalProps = {}) => {
    const onInput = event => {
      // The native event handler for React's `onChange` is `onInput` in
      // Solid — match React's behavior (which explicitly bails on
      // `defaultPrevented` due to a React bug; we preserve the guard).
      if (event.defaultPrevented) {
        return;
      }
      clearErrors(name());
      if (!shouldValidateOnChange()) {
        void commit(event.currentTarget.value, true);
        return;
      }
      const element = event.currentTarget;
      if (element.value === "") {
        // Empty values bypass the debounce so "required" errors clear
        // immediately when the user clears the field.
        void commit(element.value);
        return;
      }
      timeout.clear();
      const debounce = validationDebounceTime();
      if (debounce) {
        timeout.start(debounce, () => {
          void commit(element.value);
        });
      } else {
        void commit(element.value);
      }
    };
    return mergePropsN([{
      onInput
    }, getValidationProps(externalProps)]);
  };
  return {
    getValidationProps,
    getInputValidationProps,
    inputRef,
    commit
  };
}
//# sourceMappingURL=use-field-validation.jsx.map
