import {
	validateCallback,
	validateReadValue,
	validateSignalValue,
} from '../errors'
import {
	activeSink,
	type Cleanup,
	DEFAULT_EQUALITY,
	link,
	type SignalOptions,
	type StateNode,
	setState,
	TYPE_SENSOR,
} from '../graph'
import { isSignalOfType, isSyncFunction } from '../util'

/* === Types === */

/**
 * A read-only signal that tracks external input and updates a state value as long as it is active.
 *
 * @template T - The type of value produced by the sensor
 */
type Sensor<T extends {}> = {
	readonly [Symbol.toStringTag]: 'Sensor'

	/**
	 * Gets the current value of the sensor.
	 * When called inside another reactive context, creates a dependency.
	 * @returns The sensor value
	 * @throws UnsetSignalValueError If the sensor value is still unset when read.
	 */
	get(): T
}

/**
 * Configuration options for `createSensor`.
 *
 * @template T - The type of value produced by the sensor
 */
type SensorOptions<T extends {}> = SignalOptions<T> & {
	/**
	 * Optional initial value. Avoids `UnsetSignalValueError` on first read
	 * before the watched callback fires.
	 */
	value?: T
}

/**
 * Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
 * subscriber; receives a `set` function to push new values into the graph.
 *
 * @template T - The type of value produced by the sensor
 * @param set - Updates the sensor value and propagates the change to subscribers
 * @returns A cleanup function invoked when the sensor loses all subscribers
 */
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup

/* === Exported Functions === */

/**
 * Creates a sensor that tracks external input and updates a state value as long as it is active.
 * Sensors get activated when they are first accessed by an effect and deactivated when they are
 * no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
 *
 * @since 0.18.0
 * @template T - The type of value produced by the sensor
 * @param watched - The callback invoked when the sensor starts being watched, receives a `set` function and returns a cleanup function.
 * @param options - Optional configuration for the sensor.
 * @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
 *   before the watched callback fires. Essential for the mutable-object observation pattern.
 * @param options.equals - Optional equality function. Defaults to strict equality (`===`). Use `SKIP_EQUALITY`
 *   for mutable objects where the reference stays the same but internal state changes.
 * @param options.guard - Optional type guard to validate values.
 * @returns A read-only sensor signal.
 *
 * @example Tracking external values
 * ```ts
 * const mousePos = createSensor<{ x: number; y: number }>((set) => {
 *   const handler = (e: MouseEvent) => {
 *     set({ x: e.clientX, y: e.clientY });
 *   };
 *   window.addEventListener('mousemove', handler);
 *   return () => window.removeEventListener('mousemove', handler);
 * });
 * ```
 *
 * @example Observing a mutable object
 * ```ts
 * import { createSensor, SKIP_EQUALITY } from 'cause-effect';
 *
 * const el = createSensor<HTMLElement>((set) => {
 *   const node = document.getElementById('box')!;
 *   set(node);
 *   const obs = new MutationObserver(() => set(node));
 *   obs.observe(node, { attributes: true });
 *   return () => obs.disconnect();
 * }, { value: node, equals: SKIP_EQUALITY });
 * ```
 */
function createSensor<T extends {}>(
	watched: SensorCallback<T>,
	options?: SensorOptions<T>,
): Sensor<T> {
	validateCallback(TYPE_SENSOR, watched, isSyncFunction)
	if (options?.value !== undefined)
		validateSignalValue(TYPE_SENSOR, options.value, options?.guard)

	const node: StateNode<T> = {
		value: options?.value as T,
		sinks: null,
		sinksTail: null,
		equals: options?.equals ?? DEFAULT_EQUALITY,
		guard: options?.guard,
		stop: undefined,
	}

	return {
		[Symbol.toStringTag]: TYPE_SENSOR,
		get(): T {
			if (activeSink) {
				if (!node.sinks)
					node.stop = watched((next: T): void => {
						validateSignalValue(TYPE_SENSOR, next, node.guard)
						setState(node, next)
					})
				link(node, activeSink)
			}
			validateReadValue(TYPE_SENSOR, node.value)
			return node.value
		},
	}
}

/**
 * Checks if a value is a Sensor signal.
 *
 * @since 0.18.0
 * @param value - The value to check
 * @returns True if the value is a Sensor
 */
function isSensor<T extends {} = unknown & {}>(
	value: unknown,
): value is Sensor<T> {
	return isSignalOfType(value, TYPE_SENSOR)
}

export {
	createSensor,
	isSensor,
	type Sensor,
	type SensorCallback,
	type SensorOptions,
}
