/**
 * Fine-grained reactive primitives shared by server utilities and client code.
 * Updates are synchronous by default and coalesced inside `batch()`.
 */

export type Subscriber<T> = (value: T, previous?: T) => void;
export type Unsubscribe = () => void;
export type Cleanup = () => void;

export interface Signal<T> {
  get(): T;
  set(next: T): void;
  update(fn: (current: T) => T): void;
  subscribe(fn: Subscriber<T>): Unsubscribe;
}

export interface ReadonlySignal<T> {
  get(): T;
  subscribe(fn: Subscriber<T>): Unsubscribe;
}

type DependencyCollector = (subscribe: (subscriber: Subscriber<unknown>) => Unsubscribe) => void;

let activeCollector: DependencyCollector | null = null;
let batchDepth = 0;
const pending = new Set<() => void>();

function enqueue(job: () => void): void {
  if (batchDepth > 0) pending.add(job);
  else job();
}

function flush(): void {
  while (pending.size > 0) {
    const jobs = [...pending];
    pending.clear();
    for (const job of jobs) job();
  }
}

/** Coalesce every signal notification made by `fn` into one flush. */
export function batch<T>(fn: () => T): T {
  batchDepth++;
  try {
    return fn();
  } finally {
    batchDepth--;
    if (batchDepth === 0) flush();
  }
}

/** Read reactive values without recording dependencies. */
export function untrack<T>(fn: () => T): T {
  const previous = activeCollector;
  activeCollector = null;
  try {
    return fn();
  } finally {
    activeCollector = previous;
  }
}

export function signal<T>(initial: T): Signal<T> {
  let value = initial;
  let pendingPrevious: T | undefined;
  let queued = false;
  const subscribers = new Set<Subscriber<T>>();

  const notify = (): void => {
    queued = false;
    const previous = pendingPrevious;
    pendingPrevious = undefined;
    for (const fn of [...subscribers]) fn(value, previous);
  };

  const api: Signal<T> = {
    get(): T {
      if (activeCollector) {
        activeCollector((subscriber) => api.subscribe(subscriber as Subscriber<T>));
      }
      return value;
    },
    set(next: T): void {
      if (Object.is(next, value)) return;
      const previous = value;
      value = next;
      if (!queued) {
        queued = true;
        pendingPrevious = previous;
        enqueue(notify);
      }
    },
    update(fn: (current: T) => T): void {
      api.set(fn(value));
    },
    subscribe(fn: Subscriber<T>): Unsubscribe {
      subscribers.add(fn);
      return () => subscribers.delete(fn);
    },
  };

  return api;
}

/**
 * Run a dependency-tracked side effect. Dependencies are rebuilt after every
 * execution, preventing stale subscriptions when conditional reads change.
 */
export function effect(run: () => void | Cleanup): Cleanup {
  let disposed = false;
  let cleanup: void | Cleanup;
  let subscriptions: Cleanup[] = [];
  let scheduled = false;

  const execute = (): void => {
    scheduled = false;
    if (disposed) return;
    if (typeof cleanup === "function") cleanup();
    for (const unsubscribe of subscriptions) unsubscribe();
    subscriptions = [];

    const previous = activeCollector;
    activeCollector = (subscribe) => {
      subscriptions.push(
        subscribe(() => {
          if (scheduled || disposed) return;
          scheduled = true;
          enqueue(execute);
        }),
      );
    };
    try {
      const nextCleanup = run();
      cleanup = typeof nextCleanup === "function" ? nextCleanup : undefined;
    } finally {
      activeCollector = previous;
    }
  };

  execute();
  return () => {
    if (disposed) return;
    disposed = true;
    if (typeof cleanup === "function") cleanup();
    for (const unsubscribe of subscriptions) unsubscribe();
    subscriptions = [];
  };
}

/** Create a lazily readable derived signal with automatic dependency tracking. */
export function computed<T>(read: () => T): ReadonlySignal<T> {
  const output = signal<T>(undefined as T);
  effect(() => output.set(read()));
  return {
    get: output.get,
    subscribe: output.subscribe,
  };
}
