import { createEffect, createMemo, createSignal, untrack } from "solid-js";
import { readOption } from "../maybe-accessor.jsx";
import { SNAPSHOT_KEYS, snapshotKeys } from "../observability/snapshot.jsx";

// Stage 02 — error-contract spike.
//
// Validates RFC 05's claim that a compound's API surface should not carry a
// parallel `error` accessor: validation *messages* (returned strings) live in
// the data graph, validation *exceptions* (sync throws / async rejections)
// propagate through the reactive graph to the nearest `<Errored>` boundary.
// Two different concepts, two different channels — neither one is `c.error`.
//
// Companion doc: work-left/primitive-template.md §2.3.

/**
 * What a validator function may return. A returned string (or array of
 * strings) is a validation **message** — surfaced to the consumer through
 * `validity().errors`. Distinct from a validator that throws or rejects: a
 * thrown error is treated as a programmer/system fault and propagates to
 * `<Errored>`.
 */

const NOT_VALIDATED = Object.freeze({
  valid: undefined,
  errors: Object.freeze([]),
  message: ""
});
function toValidity(result) {
  if (result == null) {
    return {
      valid: true,
      errors: [],
      message: ""
    };
  }
  const errors = Array.isArray(result) ? result.filter(s => s.length > 0) : typeof result === "string" && result.length > 0 ? [result] : [];
  if (errors.length === 0) {
    return {
      valid: true,
      errors: [],
      message: ""
    };
  }
  return {
    valid: false,
    errors,
    message: errors[0] ?? ""
  };
}
function isPromiseLike(v) {
  return typeof v === "object" && v !== null && typeof v.then === "function";
}
export function createValidator(options) {
  const value = () => readOption(options.value);

  // Internal validity-store backed by a signal that rejects can poison.
  // On rejection we stash the error in `pendingThrow`; the public
  // `validity` memo re-reads it and throws into the graph so a `<Errored>`
  // boundary above the consumer's read site catches it.
  const [committed, setCommitted] = createSignal(NOT_VALIDATED, {
    ownedWrite: true,
    name: "validator.committed"
  });
  const [pendingThrow, setPendingThrow] = createSignal(undefined, {
    ownedWrite: true,
    name: "validator.pendingThrow"
  });
  let generation = 0;
  const runValidate = async (v, event, reason) => {
    const myGen = ++generation;
    const isStale = () => myGen !== generation;
    const validateFn = options.validate;
    // No validator → trivially valid. Useful for hooking pre-change
    // cancellation without writing a real `validate`.
    if (!validateFn) {
      const next = {
        valid: true,
        errors: [],
        message: ""
      };
      options.onValidityChange?.(next, event, reason);
      if (event.defaultPrevented) return committed();
      setPendingThrow(undefined);
      setCommitted(next);
      return next;
    }

    // Sync throws are intentionally not caught here — they re-throw at
    // the call site (consumer's `await c.validate()` rejects) AND
    // poison `validity()` so a downstream read inside `<Errored>` also
    // catches. This mirrors RFC 05's "one error path" promise.
    let resultOrPromise;
    try {
      resultOrPromise = validateFn(v);
    } catch (err) {
      if (!isStale()) setPendingThrow(err);
      throw err;
    }
    let result;
    if (isPromiseLike(resultOrPromise)) {
      try {
        result = await resultOrPromise;
      } catch (err) {
        if (!isStale()) setPendingThrow(err);
        throw err;
      }
      if (isStale()) return committed();
    } else {
      result = resultOrPromise;
    }
    const next = toValidity(result);
    options.onValidityChange?.(next, event, reason);
    if (event.defaultPrevented) return committed();
    setPendingThrow(undefined);
    setCommitted(next);
    return next;
  };
  const validate = (event, reason = "imperative") => {
    const ev = event ?? new Event("validate", {
      cancelable: true
    });
    return runValidate(untrack(value), ev, reason);
  };

  // Public validity accessor. The memo re-reads `pendingThrow` so a
  // poisoned validator surfaces to `<Errored>` on every read until the
  // next successful `validate()` clears it.
  const validity = createMemo(() => {
    const err = pendingThrow();
    if (err !== undefined) throw err;
    return committed();
  }, {
    name: "validator.validity"
  });

  // `onValueSettled` runs through the {effect, error} bundle. Errors
  // here are explicitly **internal** — they don't surface to the
  // consumer, by design. The bundle's `error` handler is the primitive's
  // recovery path: log, run cleanup, move on.
  //
  // **Observed contract — important.** RFC 01's bundle catches exceptions
  // thrown from the **compute** phase, not the effect phase. So the
  // throwing work has to ride in the compute body. The empty `effect`
  // callback is the price of using the bundle as a try/catch around
  // per-tick side-effect work.
  if (options.onValueSettled) {
    const settled = options.onValueSettled;
    createEffect(prev => {
      const v = value();
      if (prev !== undefined) {
        // Run cleanup from the prior tick before the new
        // side-effect; mirrors React-effect lifecycle.
      }
      settled(v);
      return v;
    }, {
      effect: () => undefined,
      error: (err, cleanup) => {
        // In a real component this would route through
        // `DEV.diagnostics` so attribution names the compound
        // + slot (stage 04). For the spike, console.warn is
        // enough to demonstrate the contract.
        if (typeof console !== "undefined") {
          console.warn("[createValidator] onValueSettled threw", err);
        }
        cleanup();
      }
    });
  }
  const api = {
    value,
    validity,
    validate
  };
  api[SNAPSHOT_KEYS] = VALIDATOR_SNAPSHOT_KEYS;
  return api;
}
const VALIDATOR_SNAPSHOT_KEYS = snapshotKeys("value", "validity");
//# sourceMappingURL=create-validator.jsx.map
