import type {
  StoreActionContext,
  StoreDefinition,
  StoreInstance,
  StoreInstanceCore,
  StoreMutation,
  StorePersistenceConfig,
  StoreCombinedState,
  StoreFunction,
} from "./types.ts";
export type * from "./types.ts";

const memoryPersistence = new Map<string, string>();

function clone<T>(value: T): T {
  if (typeof structuredClone === "function") {
    try {
      return structuredClone(value);
    } catch {
      /* Proxies are cloned through their JSON-visible state below. */
    }
  }
  return JSON.parse(JSON.stringify(value)) as T;
}

function readonlySnapshot<S extends object>(state: S): Readonly<S> {
  return Object.freeze(clone(state));
}

function storageFor(kind: StorePersistenceConfig<object>["storage"]): Storage | null {
  if (typeof window === "undefined") return null;
  if (kind === "local") return window.localStorage;
  if (kind === "session") return window.sessionStorage;
  return null;
}

function readPersisted<S extends object>(
  key: string,
  config: StorePersistenceConfig<S>,
): Partial<S> | null {
  try {
    const raw =
      config.storage === "memory"
        ? memoryPersistence.get(key)
        : storageFor(config.storage)?.getItem(key);
    if (!raw) return null;
    const parsed = JSON.parse(raw) as { version?: number; state?: unknown };
    const from = Number(parsed.version ?? 0);
    let state = parsed.state;
    if (from !== config.version && config.migrate)
      state = config.migrate(state, from, config.version);
    if (config.validate) return config.validate(state);
    if (!state || typeof state !== "object" || Array.isArray(state)) return null;
    return state as Partial<S>;
  } catch {
    return null;
  }
}

function writePersisted<S extends object>(
  key: string,
  state: S,
  config: StorePersistenceConfig<S>,
): void {
  const picked: Record<string, unknown> = {};
  for (const name of config.include) picked[name] = state[name];
  const raw = JSON.stringify({ version: config.version, state: picked });
  if (config.storage === "memory") memoryPersistence.set(key, raw);
  else storageFor(config.storage)?.setItem(key, raw);
}

export function defineStore<
  S extends object,
  C extends object = Record<string, never>,
  A extends Record<string, StoreFunction> = Record<string, StoreFunction>,
  CS extends object = Record<string, never>,
  SS extends object = Record<string, never>,
>(definition: StoreDefinition<S, C, A, CS, SS>): StoreDefinition<S, C, A, CS, SS> {
  return definition;
}

export interface StoreContainerOptions {
  runtime: "server" | "client";
  request?: unknown;
  routeId?: string;
  hydration?: Record<string, unknown>;
  onMutation?: (mutation: StoreMutation) => void;
}

export class StoreContainer {
  readonly runtime: "server" | "client";
  readonly request?: unknown;
  readonly routeId?: string;
  private readonly instances = new Map<
    string,
    StoreInstance<Record<string, unknown>, Record<string, unknown>, Record<string, StoreFunction>>
  >();
  private readonly hydration: Record<string, unknown>;
  private readonly onMutation?: (mutation: StoreMutation) => void;
  private readonly lastMutations = new Map<string, StoreMutation>();

  constructor(options: StoreContainerOptions) {
    this.runtime = options.runtime;
    this.request = options.request;
    this.routeId = options.routeId;
    this.hydration = options.hydration ?? {};
    this.onMutation = options.onMutation;
  }

  async use<
    S extends object,
    C extends object,
    A extends Record<string, StoreFunction>,
    CS extends object,
    SS extends object,
  >(
    definition: StoreDefinition<S, C, A, CS, SS>,
  ): Promise<StoreInstance<StoreCombinedState<S, CS, SS>, C, A>> {
    const key =
      definition.kind === "page"
        ? `${definition.name}@${this.routeId ?? "default"}`
        : definition.name;
    const existing = this.instances.get(key);
    if (existing) return existing as StoreInstance<StoreCombinedState<S, CS, SS>, C, A>;
    const instance = createStoreInstance(definition, {
      runtime: this.runtime,
      request: this.request,
      routeId: this.routeId,
      hydration: this.hydration[definition.name],
      onMutation: (mutation) => {
        this.lastMutations.set(definition.name, mutation);
        this.onMutation?.(mutation);
      },
    });
    this.instances.set(key, instance);
    await instance.whenReady;
    return instance;
  }

  serialize(): Record<string, unknown> {
    return Object.fromEntries(
      Array.from(this.instances.values(), (instance) => [instance.name, instance.serialize()]),
    );
  }

  inspect(): Array<{
    name: string;
    kind: "global" | "page";
    state: Readonly<Record<string, unknown>>;
    computed: Readonly<Record<string, unknown>>;
    lastAction?: string;
    changed: string[];
    hydrationSource: "server" | "persistence" | "initial";
  }> {
    return Array.from(this.instances.values(), (instance) => {
      const mutation = this.lastMutations.get(instance.name);
      return {
        name: instance.name,
        kind: instance.kind,
        state: instance.snapshot() as Readonly<Record<string, unknown>>,
        computed: Object.freeze(
          Object.fromEntries(
            Reflect.ownKeys(instance.computed).map((key) => [
              String(key),
              Reflect.get(instance.computed, key),
            ]),
          ),
        ),
        ...(mutation ? { lastAction: mutation.action } : {}),
        changed: mutation?.changed ?? [],
        hydrationSource: this.hydration[instance.name] ? "server" : "initial",
      };
    });
  }

  async hotUpdate<
    S extends object,
    C extends object,
    A extends Record<string, StoreFunction>,
    CS extends object,
    SS extends object,
  >(
    definition: StoreDefinition<S, C, A, CS, SS>,
  ): Promise<{ preserved: string[]; reset: string[] }> {
    const entries = [...this.instances.entries()].filter(
      ([, instance]) => instance.name === definition.name,
    );
    const preserved = new Set<string>();
    const reset = new Set<string>();
    for (const [key, previous] of entries) {
      const snapshot = previous.snapshot() as Record<string, unknown>;
      await previous.dispose();
      const freshShared = definition.createSharedState();
      const freshRuntime =
        this.runtime === "client"
          ? (definition.createClientState?.() ?? {})
          : (definition.createServerState?.() ?? {});
      const nextShape = { ...freshShared, ...freshRuntime } as Record<string, unknown>;
      const compatible: Record<string, unknown> = {};
      for (const [name, value] of Object.entries(snapshot)) {
        if (!(name in nextShape)) continue;
        const expected = nextShape[name];
        const same =
          expected === null || value === null
            ? expected === value || expected === null
            : Array.isArray(expected)
              ? Array.isArray(value)
              : typeof expected === typeof value;
        if (same) {
          compatible[name] = value;
          preserved.add(name);
        } else reset.add(name);
      }
      for (const name of Object.keys(nextShape))
        if (!(name in compatible) && name in snapshot) reset.add(name);
      const instance = createStoreInstance(definition, {
        runtime: this.runtime,
        request: this.request,
        routeId: this.routeId,
        hydration: compatible,
        onMutation: (mutation) => {
          this.lastMutations.set(definition.name, mutation);
          this.onMutation?.(mutation);
        },
      });
      this.instances.set(key, instance);
      await instance.whenReady;
    }
    return { preserved: [...preserved], reset: [...reset] };
  }

  async disposePageStores(): Promise<void> {
    for (const [key, instance] of [...this.instances]) {
      if (instance.kind !== "page") continue;
      await instance.dispose();
      this.instances.delete(key);
    }
  }

  async dispose(): Promise<void> {
    for (const instance of this.instances.values()) await instance.dispose();
    this.instances.clear();
  }
}

export function createStoreContainer(options: StoreContainerOptions): StoreContainer {
  return new StoreContainer(options);
}

export function createStoreInstance<
  S extends object,
  C extends object,
  A extends Record<string, StoreFunction>,
  CS extends object,
  SS extends object,
>(
  definition: StoreDefinition<S, C, A, CS, SS>,
  options: Omit<StoreContainerOptions, "hydration"> & { hydration?: unknown },
): StoreInstance<StoreCombinedState<S, CS, SS>, C, A> {
  type State = StoreCombinedState<S, CS, SS>;
  const createInitialState = (): State =>
    ({
      ...definition.createSharedState(),
      ...(options.runtime === "client"
        ? definition.createClientState?.()
        : definition.createServerState?.()),
    }) as State;
  const initial = createInitialState();
  const persisted =
    options.runtime === "client" && definition.persist
      ? readPersisted(`wrnexus:store:${definition.name}`, definition.persist)
      : null;
  const hydrated =
    options.hydration && typeof options.hydration === "object"
      ? (options.hydration as Partial<State>)
      : null;
  const raw = Object.assign(initial, persisted ?? {}, hydrated ?? {});
  const listeners = new Set<(snapshot: Readonly<State>, mutation?: StoreMutation) => void>();
  let currentAction = "direct";
  let internalMutation = false;

  const mutableState = new Proxy(raw, {
    set(target, property, value) {
      if (!internalMutation) {
        throw new TypeError(
          `WRN-STORE-READONLY: ${definition.name}.${String(property)} must be changed by a store action.`,
        );
      }
      if (Object.is(Reflect.get(target, property), value)) return true;
      const before = readonlySnapshot(target);
      Reflect.set(target, property, value);
      const after = readonlySnapshot(target);
      const mutation: StoreMutation = {
        store: definition.name,
        action: currentAction,
        changed: [String(property)],
        before: before as Readonly<Record<string, unknown>>,
        after: after as Readonly<Record<string, unknown>>,
        timestamp: Date.now(),
      };
      if (definition.persist && options.runtime === "client") {
        writePersisted(`wrnexus:store:${definition.name}`, target, definition.persist);
      }
      options.onMutation?.(mutation);
      for (const listener of listeners) listener(after, mutation);
      return true;
    },
    deleteProperty(target, property) {
      if (!internalMutation) {
        throw new TypeError(
          `WRN-STORE-READONLY: ${definition.name}.${String(property)} must be changed by a store action.`,
        );
      }
      return Reflect.deleteProperty(target, property);
    },
  });

  const publicState = new Proxy({} as State, {
    get(_target, property) {
      return Reflect.get(mutableState, property);
    },
    set(_target, property) {
      throw new TypeError(
        `WRN-STORE-READONLY: ${definition.name}.${String(property)} must be changed by a store action.`,
      );
    },
    deleteProperty(_target, property) {
      throw new TypeError(
        `WRN-STORE-READONLY: ${definition.name}.${String(property)} must be changed by a store action.`,
      );
    },
    ownKeys() {
      return Reflect.ownKeys(mutableState);
    },
    getOwnPropertyDescriptor(_target, property) {
      if (!(property in mutableState)) return undefined;
      return {
        enumerable: true,
        configurable: true,
        value: Reflect.get(mutableState, property),
        writable: false,
      };
    },
    has(_target, property) {
      return property in mutableState;
    },
  });

  const mutate = <T>(action: string, callback: () => T): T => {
    const previousAction = currentAction;
    const previousMutation = internalMutation;
    currentAction = action;
    internalMutation = true;
    try {
      return callback();
    } finally {
      currentAction = previousAction;
      internalMutation = previousMutation;
    }
  };

  const reset = () =>
    mutate("$reset", () => {
      const next = createInitialState();
      for (const key of Object.keys(mutableState)) {
        if (!(key in next)) Reflect.deleteProperty(mutableState, key);
      }
      Object.assign(mutableState, next);
    });

  const context: StoreActionContext<State> = {
    state: mutableState,
    snapshot: () => readonlySnapshot(mutableState),
    reset,
    runtime: options.runtime,
    request: options.request,
    routeId: options.routeId,
  };

  const actions = {} as A;
  for (const [name, rawDefinitions] of Object.entries(definition.actions ?? {})) {
    const definitions = Array.isArray(rawDefinitions) ? rawDefinitions : [rawDefinitions];
    const selected =
      definitions.find((entry) => entry.runtime === options.runtime) ??
      definitions.find((entry) => entry.runtime === "shared") ??
      definitions.find((entry) => entry.runtime === "legacy");
    if (!selected) continue;
    Reflect.set(actions, name, async (...args: unknown[]) => {
      currentAction = name;
      internalMutation = true;
      try {
        const handler = selected.handler as unknown as (
          context: StoreActionContext<State>,
          ...args: unknown[]
        ) => unknown;
        return await handler(context, ...args);
      } finally {
        currentAction = "direct";
        internalMutation = false;
      }
    });
  }

  const computed = new Proxy({} as C, {
    get(_target, property) {
      const fn = definition.computed?.[property as keyof C];
      return fn ? fn(publicState) : undefined;
    },
    set() {
      throw new TypeError("Computed store values are readonly");
    },
    ownKeys() {
      return Reflect.ownKeys(definition.computed ?? {});
    },
    getOwnPropertyDescriptor() {
      return { enumerable: true, configurable: true };
    },
  });

  const lifecycleContext = {
    state: mutableState,
    runtime: options.runtime,
    request: options.request,
    routeId: options.routeId,
  };
  const runLifecycle = async (
    name: string,
    hook?: (context: typeof lifecycleContext) => void | Promise<void>,
  ): Promise<void> => {
    if (!hook) return;
    const previousAction = currentAction;
    const previousMutation = internalMutation;
    currentAction = name;
    internalMutation = true;
    try {
      await hook(lifecycleContext);
    } finally {
      currentAction = previousAction;
      internalMutation = previousMutation;
    }
  };
  const initHook =
    options.runtime === "server"
      ? definition.lifecycle?.serverInit
      : definition.lifecycle?.clientInit;
  const ready = runLifecycle(
    options.runtime === "server" ? "$serverInit" : "$clientInit",
    initHook,
  ).then(async () => {
    if (options.runtime === "client" && options.hydration && definition.lifecycle?.hydrate) {
      await runLifecycle("$hydrate", definition.lifecycle.hydrate);
    }
  });

  const core: StoreInstanceCore<State, C, A> = {
    name: definition.name,
    kind: definition.kind,
    state: publicState,
    computed,
    actions,
    whenReady: ready,
    reset,
    snapshot: () => readonlySnapshot(mutableState),
    hydrate(value) {
      mutate("$hydrate", () => Object.assign(mutableState, value));
    },
    serialize() {
      const result: Partial<State> = {};
      const serverKeys = new Set(Object.keys(definition.createServerState?.() ?? {}));
      for (const [key, value] of Object.entries(mutableState)) {
        if (!serverKeys.has(key)) Reflect.set(result, key, clone(value));
      }
      return result;
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    async dispose() {
      await ready;
      await runLifecycle("$dispose", definition.lifecycle?.dispose);
      listeners.clear();
    },
  };

  return new Proxy(core as StoreInstance<State, C, A>, {
    get(target, property, receiver) {
      if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
      if (property in actions) return Reflect.get(actions, property);
      if (property in (definition.computed ?? {})) return Reflect.get(computed, property);
      if (property in mutableState) return Reflect.get(publicState, property);
      return undefined;
    },
    set(_target, property) {
      throw new TypeError(
        `WRN-STORE-READONLY: ${definition.name}.${String(property)} must be changed by a store action.`,
      );
    },
    ownKeys(target) {
      return [
        ...new Set([
          ...Reflect.ownKeys(target),
          ...Reflect.ownKeys(mutableState),
          ...Reflect.ownKeys(definition.computed ?? {}),
          ...Reflect.ownKeys(actions),
        ]),
      ];
    },
    getOwnPropertyDescriptor(target, property) {
      return (
        Reflect.getOwnPropertyDescriptor(target, property) ?? {
          enumerable: true,
          configurable: true,
        }
      );
    },
  });
}
