import type { ReadState, Selector, SetState } from './medama.types';

export type SelectorTrigger = () => void;

type UnregisterTriggerFromKeyHandle = () => void;

/**
 * Function to register a selector's trigger with a state property. When
 * property value changes, registered trigger is called. Returns cleanup
 * function to unregister trigger.
 */
export type KeyHandle = (trigger: SelectorTrigger) => UnregisterTriggerFromKeyHandle;

export type KeyHandleCollector = (keyHandle: KeyHandle) => void;

export type RegisterSelectorTrigger<State extends object> = (
  selectorTrigger: SelectorTrigger
) => ReadState<State>;

type KeyHandleRecord = {
  keyHandle: KeyHandle;
  fireKey: () => void;
};

type GetFromTarget<State> = <K extends string | symbol>(
  target: State,
  p: K
) => State[K & keyof State];

type SetToTarget<State> = <K extends string | symbol>(
  target: State,
  p: K,
  newValue: State[K & keyof State]
) => true;

type ProxyHandler<State> = {
  get: GetFromTarget<State>;
  set: SetToTarget<State>;
};

export type RunOverState<State extends object, V> = (
  selector: Selector<State, V>,
  keyHandleCollector?: KeyHandleCollector
) => V;

/**
 * Creates core state management system with controlled access and subscription
 * handling. Provides:
 * - Proxy-wrapped state object with access restrictions
 * - Dependency tracking during selector execution
 * - Subscription management for state changes
 * - Methods for reading and updating state
 *
 * @param initState Optional initial state values
 * @returns Methods for running selectors over state and updating state
 */
export const createStateImage = <State extends object>(
  initState?: Partial<State>
): {
  runOverState: RunOverState<State, unknown>;
  setState: SetState<State>;
} => {
  let calculationAllowed = false;

  /**
   * Enforces authorized access to state properties. Throws error if attempting
   * to access state outside of:
   * - Selector execution
   * - State update operations
   */
  const restrictCalculation = (): void => {
    if (!calculationAllowed)
      throw new Error('Medama Error: The object has no access to its properties');
  };

  /**
   * Temporarily allows state property access during execution of provided
   * function. Sets calculationAllowed flag to true before execution and resets
   * it after. Used during selector execution and state updates.
   *
   * @param toRun Function to execute with state access allowed
   * @returns Result of executed function
   */
  const runWithRestrictionLifted = <V>(toRun: () => V): V => {
    calculationAllowed = true;

    return ([toRun(), (calculationAllowed = false)] as const)[0];
  };

  /**
   * Tracks current key handle collector during selector execution. Set when
   * running selector to collect state property dependencies. Used by proxy
   * handler to register key handles for accessed properties. Reset to undefined
   * after selector execution completes.
   */
  let activeKeyHandleCollector: KeyHandleCollector | undefined;

  /**
   * Executes selector over state with dependency tracking.
   * - Sets active key handle collector for dependency tracking
   * - Runs selector with authorized access to state properties
   * - Resets collector after execution
   *
   * @param selector Function to run over state
   * @param keyHandleCollector Optional collector for registering dependencies
   * @returns Result of selector execution
   */
  const runOverState: RunOverState<State, unknown> = (selector, keyHandleCollector?) => {
    activeKeyHandleCollector = keyHandleCollector;

    return (
      [
        runWithRestrictionLifted(() => selector(state)),
        (activeKeyHandleCollector = undefined),
      ] as const
    )[0];
  };

  const triggerJobStore: Partial<Record<keyof State, KeyHandleRecord>> = {};

  const { addToQueue, runQueue } = createJobQueue();

  /**
   * Creates a record to manage triggers for a state property. Contains:
   * - keyHandle: Registers selector triggers and returns cleanup function
   * - fireKey: Queues all registered triggers when property value changes Uses
   *   Set to maintain unique triggers per property.
   */
  const createKeyHandleRecord = (): KeyHandleRecord => {
    const triggerSet = new Set<SelectorTrigger>();

    const keyHandle: KeyHandle = (trigger) => {
      triggerSet.add(trigger);

      return (): void => {
        triggerSet.delete(trigger);
      };
    };

    const fireKey = (): void => {
      addToQueue(triggerSet);
    };

    return { keyHandle, fireKey };
  };

  /**
   * Updates state with new values and triggers subscriptions.
   * - Accepts either partial state object or state updater function
   * - Runs update with authorized access to state properties
   * - Executes all queued subscription triggers after update
   *
   * @param stateChange Partial state object or updater function
   * @returns Applied state changes
   */
  const setState: SetState<State> = (stateChange) =>
    (
      [
        runWithRestrictionLifted(() => {
          const mergeToState = typeof stateChange === 'function' ? stateChange(state) : stateChange;
          Object.assign(state, mergeToState);

          return mergeToState;
        }),

        runQueue(),
      ] as const
    )[0];

  /**
   * Creates a Proxy handler for the state object with following
   * responsibilities:
   * - Tracks dependencies by registering key handles when
   *   activeKeyHandleCollector is present (set during selector execution to
   *   collect accessed state properties)
   * - Triggers subscriptions when state properties change by comparing old/new
   *   values
   * - Enforces authorized access through calculationAllowed flag
   */
  const proxyHandler: ProxyHandler<State> = {
    get: <K extends string | symbol>(target: State, p: K): State[K & keyof State] => {
      restrictCalculation();

      if (activeKeyHandleCollector) {
        const { keyHandle } = (triggerJobStore[p as K & keyof State] ??= createKeyHandleRecord());

        activeKeyHandleCollector(keyHandle);
      }

      return target[p as K & keyof State];
    },

    set: <K extends string | symbol>(
      target: State,
      p: K,
      newValue: State[K & keyof State]
    ): true => {
      restrictCalculation();
      const oldValue = target[p as K & keyof State];
      target[p as K & keyof State] = newValue;
      const { fireKey } = triggerJobStore[p as K & keyof State] ?? {};

      if (fireKey && !Object.is(oldValue, newValue)) fireKey();

      return true;
    },
  };

  /**
   * The state object wrapped in a Proxy to:
   * - Track property access for dependency collection
   * - Trigger subscriptions on property changes
   * - Enforce authorized access through restriction mechanism
   */
  const state = new Proxy({ ...initState } as State, proxyHandler);

  return { runOverState, setState };
};

type AddToQueue = (triggerSet: Set<SelectorTrigger>) => void;

type RunQueue = () => void;

/**
 * Creates a queue system to manage selector trigger execution.
 * - Queue is populated when state properties change via Proxy's set handler
 * - Maintains unique set of triggers to avoid duplicate executions
 * - Provides methods to add triggers and process entire queue
 * - Clears queue after processing all triggers
 */
export const createJobQueue = (): {
  addToQueue: AddToQueue;
  runQueue: RunQueue;
} => {
  const queue = new Set<SelectorTrigger>();

  const addToQueue: AddToQueue = (triggerSet) => {
    triggerSet.forEach((trigger) => {
      queue.add(trigger);
    });
  };

  const runQueue: RunQueue = () => {
    queue.forEach((trigger) => {
      trigger();
    });

    queue.clear();
  };

  return { addToQueue, runQueue };
};
