/**
 * @fileoverview OrdoJS Reactivity System - Signal-based reactive state management
 * @author OrdoJS Framework Team
 */

/**
 * Cleanup function type
 */
export type EffectCleanup = () => void;

/**
 * Unsubscribe function type
 */
export type Unsubscribe = () => void;

/**
 * Signal interface for reactive values
 */
export interface Signal<T> {
  /** Current value */
  readonly value: T;
  /** Subscribe to value changes */
  subscribe(callback: (value: T) => void): Unsubscribe;
  /** Update value with new value */
  set(value: T): void;
  /** Update value with updater function */
  update(updater: (current: T) => T): void;
  /** Get current value (same as .value) */
  get(): T;
  /** Check if signal has subscribers */
  hasSubscribers(): boolean;
}

/**
 * Computed signal interface for derived values
 */
export interface ComputedSignal<T> extends Omit<Signal<T>, 'set' | 'update'> {
  /** Recompute the value */
  recompute(): void;
  /** Get dependencies */
  getDependencies(): Signal<any>[];
}

/**
 * Effect options
 */
export interface EffectOptions {
  /** Run effect immediately */
  immediate?: boolean;
  /** Effect name for debugging */
  name?: string;
  /** Cleanup function */
  onCleanup?: () => void;
}

/**
 * Batch options
 */
export interface BatchOptions {
  /** Batch name for debugging */
  name?: string;
  /** Priority level */
  priority?: 'low' | 'normal' | 'high';
}

/**
 * Global reactivity context
 */
interface ReactivityContext {
  /** Currently running effect */
  currentEffect: EffectInstance | null;
  /** Effect stack for nested effects */
  effectStack: EffectInstance[];
  /** Batch update queue */
  batchQueue: Set<Signal<any>>;
  /** Is currently batching */
  isBatching: boolean;
  /** Scheduled batch flush */
  batchFlushScheduled: boolean;
}

/**
 * Effect instance
 */
interface EffectInstance {
  /** Effect function */
  fn: () => void;
  /** Dependencies */
  dependencies: Set<Signal<any>>;
  /** Cleanup function */
  cleanup?: () => void;
  /** Effect options */
  options: EffectOptions;
  /** Is active */
  active: boolean;
}

/**
 * Global reactivity context
 */
const reactivityContext: ReactivityContext = {
  currentEffect: null,
  effectStack: [],
  batchQueue: new Set(),
  isBatching: false,
  batchFlushScheduled: false
};

/**
 * Signal implementation
 */
class SignalImpl<T> implements Signal<T> {
  private _value: T;
  private subscribers = new Set<(value: T) => void>();
  private effects = new Set<EffectInstance>();

  constructor(initialValue: T) {
    this._value = initialValue;
  }

  get value(): T {
    this.track();
    return this._value;
  }

  get(): T {
    return this.value;
  }

  set(newValue: T): void {
    if (Object.is(this._value, newValue)) {
      return;
    }

    this._value = newValue;
    this.trigger();
  }

  update(updater: (current: T) => T): void {
    this.set(updater(this._value));
  }

  subscribe(callback: (value: T) => void): Unsubscribe {
    this.subscribers.add(callback);

    return () => {
      this.subscribers.delete(callback);
    };
  }

  hasSubscribers(): boolean {
    return this.subscribers.size > 0 || this.effects.size > 0;
  }

  private track(): void {
    const currentEffect = reactivityContext.currentEffect;
    if (currentEffect) {
      this.effects.add(currentEffect);
      currentEffect.dependencies.add(this);
    }
  }

  private trigger(): void {
    if (reactivityContext.isBatching) {
      reactivityContext.batchQueue.add(this);
      this.scheduleBatchFlush();
      return;
    }

    this.flush();
  }

  private flush(): void {
    // Notify subscribers
    for (const callback of this.subscribers) {
      try {
        callback(this._value);
      } catch (error) {
        console.error('Error in signal subscriber:', error);
      }
    }

    // Run effects
    for (const effect of this.effects) {
      if (effect.active) {
        try {
          runEffect(effect);
        } catch (error) {
          console.error('Error in effect:', error);
        }
      }
    }
  }

  private scheduleBatchFlush(): void {
    if (!reactivityContext.batchFlushScheduled) {
      reactivityContext.batchFlushScheduled = true;
      queueMicrotask(() => {
        flushBatch();
      });
    }
  }
}

/**
 * Computed signal implementation
 */
class ComputedSignalImpl<T> implements ComputedSignal<T> {
  private _value: T;
  private _computed = false;
  private dependencies = new Set<Signal<any>>();
  private subscribers = new Set<(value: T) => void>();
  private effects = new Set<EffectInstance>();

  constructor(private computeFn: () => T) {
    this._value = this.compute();
  }

  get value(): T {
    if (!this._computed) {
      this.recompute();
    }
    this.track();
    return this._value;
  }

  get(): T {
    return this.value;
  }

  subscribe(callback: (value: T) => void): Unsubscribe {
    this.subscribers.add(callback);

    return () => {
      this.subscribers.delete(callback);
    };
  }

  hasSubscribers(): boolean {
    return this.subscribers.size > 0 || this.effects.size > 0;
  }

  recompute(): void {
    const newValue = this.compute();

    if (!Object.is(this._value, newValue)) {
      this._value = newValue;
      this.trigger();
    }
  }

  getDependencies(): Signal<any>[] {
    return Array.from(this.dependencies);
  }

  private compute(): T {
    // Clear old dependencies
    this.dependencies.clear();

    // Track new dependencies
    const prevEffect = reactivityContext.currentEffect;
    const computedEffect: EffectInstance = {
      fn: () => this.recompute(),
      dependencies: new Set(),
      options: { name: 'computed' },
      active: true
    };

    reactivityContext.currentEffect = computedEffect;

    try {
      const result = this.computeFn();
      this.dependencies = computedEffect.dependencies;
      this._computed = true;
      return result;
    } finally {
      reactivityContext.currentEffect = prevEffect;
    }
  }

  private track(): void {
    const currentEffect = reactivityContext.currentEffect;
    if (currentEffect) {
      this.effects.add(currentEffect);
      currentEffect.dependencies.add(this as any);
    }
  }

  private trigger(): void {
    if (reactivityContext.isBatching) {
      reactivityContext.batchQueue.add(this as any);
      this.scheduleBatchFlush();
      return;
    }

    this.flush();
  }

  private flush(): void {
    // Notify subscribers
    for (const callback of this.subscribers) {
      try {
        callback(this._value);
      } catch (error) {
        console.error('Error in computed subscriber:', error);
      }
    }

    // Run effects
    for (const effect of this.effects) {
      if (effect.active) {
        try {
          runEffect(effect);
        } catch (error) {
          console.error('Error in effect:', error);
        }
      }
    }
  }

  private scheduleBatchFlush(): void {
    if (!reactivityContext.batchFlushScheduled) {
      reactivityContext.batchFlushScheduled = true;
      queueMicrotask(() => {
        flushBatch();
      });
    }
  }
}

/**
 * Run an effect
 */
function runEffect(effect: EffectInstance): void {
  if (!effect.active) return;

  // Cleanup previous run
  if (effect.cleanup) {
    effect.cleanup();
    effect.cleanup = undefined;
  }

  // Clear dependencies
  for (const dep of effect.dependencies) {
    if ('effects' in dep) {
      (dep as any).effects.delete(effect);
    }
  }
  effect.dependencies.clear();

  // Run effect
  const prevEffect = reactivityContext.currentEffect;
  reactivityContext.currentEffect = effect;
  reactivityContext.effectStack.push(effect);

  try {
    effect.fn();
  } finally {
    reactivityContext.currentEffect = prevEffect;
    reactivityContext.effectStack.pop();
  }
}

/**
 * Flush batch updates
 */
function flushBatch(): void {
  if (!reactivityContext.isBatching) return;

  const signals = Array.from(reactivityContext.batchQueue);
  reactivityContext.batchQueue.clear();
  reactivityContext.batchFlushScheduled = false;

  for (const signal of signals) {
    (signal as any).flush();
  }
}

/**
 * Create a reactive signal
 */
export function signal<T>(initialValue: T): Signal<T> {
  return new SignalImpl(initialValue);
}

/**
 * Create a computed signal
 */
export function computed<T>(computeFn: () => T): ComputedSignal<T> {
  return new ComputedSignalImpl(computeFn);
}

/**
 * Create an effect that runs when dependencies change
 */
export function effect(fn: () => void, options: EffectOptions = {}): EffectCleanup {
  const effectInstance: EffectInstance = {
    fn,
    dependencies: new Set(),
    options: { immediate: true, ...options },
    active: true
  };

  if (options.onCleanup) {
    effectInstance.cleanup = options.onCleanup;
  }

  // Run immediately if requested
  if (effectInstance.options.immediate) {
    runEffect(effectInstance);
  }

  return () => {
    effectInstance.active = false;

    // Cleanup
    if (effectInstance.cleanup) {
      effectInstance.cleanup();
    }

    // Remove from dependencies
    for (const dep of effectInstance.dependencies) {
      if ('effects' in dep) {
        (dep as any).effects.delete(effectInstance);
      }
    }
  };
}

/**
 * Batch multiple updates together
 */
export function batch<T>(fn: () => T, options: BatchOptions = {}): T {
  if (reactivityContext.isBatching) {
    // Already batching, just run the function
    return fn();
  }

  reactivityContext.isBatching = true;

  try {
    const result = fn();

    // Flush all batched updates
    flushBatch();

    return result;
  } finally {
    reactivityContext.isBatching = false;
  }
}

/**
 * Create a derived signal that depends on other signals
 */
export function derived<T>(fn: () => T): ComputedSignal<T> {
  return computed(fn);
}

/**
 * Create a writable derived signal
 */
export function writableDerived<T>(
  getter: () => T,
  setter: (value: T) => void
): Signal<T> {
  const computedValue = computed(getter);

  return {
    get value() {
      return computedValue.value;
    },
    get: () => computedValue.value,
    set: setter,
    update: (updater) => setter(updater(computedValue.value)),
    subscribe: (callback) => computedValue.subscribe(callback),
    hasSubscribers: () => computedValue.hasSubscribers()
  };
}

/**
 * Create a signal that persists to localStorage
 */
export function persistentSignal<T>(
  key: string,
  initialValue: T,
  storage: Storage = localStorage
): Signal<T> {
  // Try to load from storage
  let storedValue = initialValue;
  try {
    const stored = storage.getItem(key);
    if (stored !== null) {
      storedValue = JSON.parse(stored);
    }
  } catch (error) {
    console.warn(`Failed to load persistent signal "${key}":`, error);
  }

  const sig = signal(storedValue);

  // Subscribe to changes and persist
  sig.subscribe((value) => {
    try {
      storage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.warn(`Failed to persist signal "${key}":`, error);
    }
  });

  return sig;
}

/**
 * Create a debounced signal
 */
export function debouncedSignal<T>(
  source: Signal<T>,
  delay: number
): Signal<T> {
  const debounced = signal(source.value);
  let timeoutId: number | undefined;

  source.subscribe((value) => {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      debounced.set(value);
      timeoutId = undefined;
    }, delay) as any;
  });

  return debounced;
}

/**
 * Create a throttled signal
 */
export function throttledSignal<T>(
  source: Signal<T>,
  delay: number
): Signal<T> {
  const throttled = signal(source.value);
  let lastUpdate = 0;
  let timeoutId: number | undefined;

  source.subscribe((value) => {
    const now = Date.now();
    const timeSinceLastUpdate = now - lastUpdate;

    if (timeSinceLastUpdate >= delay) {
      throttled.set(value);
      lastUpdate = now;
    } else {
      if (timeoutId !== undefined) {
        clearTimeout(timeoutId);
      }

      timeoutId = setTimeout(() => {
        throttled.set(value);
        lastUpdate = Date.now();
        timeoutId = undefined;
      }, delay - timeSinceLastUpdate) as any;
    }
  });

  return throttled;
}

/**
 * Combine multiple signals into one
 */
export function combineSignals<T extends readonly Signal<any>[]>(
  signals: T
): ComputedSignal<{ [K in keyof T]: T[K] extends Signal<infer U> ? U : never }> {
  return computed(() => {
    return signals.map(sig => sig.value) as any;
  });
}

/**
 * Create a signal from a promise
 */
export function fromPromise<T>(
  promise: Promise<T>,
  initialValue?: T
): Signal<{ loading: boolean; data?: T; error?: Error }> {
  const state = signal<{ loading: boolean; data?: T; error?: Error }>({
    loading: true,
    data: initialValue
  });

  promise
    .then((data) => {
      state.set({ loading: false, data });
    })
    .catch((error) => {
      state.set({ loading: false, error });
    });

  return state;
}

/**
 * Create a signal from an event target
 */
export function fromEvent<T = Event>(
  target: EventTarget,
  eventName: string,
  options?: AddEventListenerOptions
): Signal<T | null> {
  const eventSignal = signal<T | null>(null);

  const handler = (event: Event) => {
    eventSignal.set(event as T);
  };

  target.addEventListener(eventName, handler, options);

  // Return signal with cleanup
  const originalSubscribe = eventSignal.subscribe;
  eventSignal.subscribe = (callback) => {
    const unsubscribe = originalSubscribe.call(eventSignal, callback);

    return () => {
      unsubscribe();
      target.removeEventListener(eventName, handler, options);
    };
  };

  return eventSignal;
}

/**
 * Reactivity utilities
 */
export const reactivity = {
  signal,
  computed,
  effect,
  batch,
  derived,
  writableDerived,
  persistentSignal,
  debouncedSignal,
  throttledSignal,
  combineSignals,
  fromPromise,
  fromEvent
};

/**
 * Get current reactivity context (for debugging)
 */
export function getReactivityContext(): Readonly<ReactivityContext> {
  return { ...reactivityContext };
}

/**
 * Reset reactivity context (for testing)
 */
export function resetReactivityContext(): void {
  reactivityContext.currentEffect = null;
  reactivityContext.effectStack = [];
  reactivityContext.batchQueue.clear();
  reactivityContext.isBatching = false;
  reactivityContext.batchFlushScheduled = false;
}
