import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
import { generateDeclarations } from "./type-codegen.ts";
import { rpcManifest } from "./server-codegen.ts";

const RESERVED_BINDINGS = new Set([
  "await",
  "break",
  "case",
  "catch",
  "class",
  "const",
  "continue",
  "debugger",
  "default",
  "delete",
  "do",
  "else",
  "enum",
  "export",
  "extends",
  "false",
  "finally",
  "for",
  "function",
  "if",
  "implements",
  "import",
  "in",
  "instanceof",
  "interface",
  "let",
  "new",
  "null",
  "package",
  "private",
  "protected",
  "public",
  "return",
  "static",
  "super",
  "switch",
  "this",
  "throw",
  "true",
  "try",
  "typeof",
  "var",
  "void",
  "while",
  "with",
  "yield",
]);

function safeBinding(name: string): boolean {
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}

function stateObject(ast: PageAst, runtime: "shared" | "client" | "server"): string {
  const entries = ast.states
    .filter((state) => state.runtime === runtime)
    .map((state) => `${JSON.stringify(state.name)}: (${state.expr})`);
  return `{ ${entries.join(", ")} }`;
}

function actionSource(fn: RuntimeFunctionDecl, stateNames: string[], eraseTypes = false): string {
  const parameterNames = new Set(fn.parameters.map((param) => param.name));
  const params = fn.parameters.map((param) => param.name).join(", ");
  const aliases = stateNames.filter((name) => safeBinding(name) && !parameterNames.has(name));
  const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
  const runtimeAliases = ["server"]
    .filter((name) => !parameterNames.has(name))
    .map((name) => `const ${name} = context.${name};`)
    .join("\n");
  const copyBack = aliases.map((name) => `context.state.${name} = ${name};`).join("\n");
  const body = eraseTypes ? eraseFunctionTypes(fn.body) : fn.body;
  return `{ runtime: ${JSON.stringify(fn.runtime)}, handler: ${fn.async ? "async " : ""}function(context${params ? `, ${params}` : ""}) { ${runtimeAliases}\n${aliasSource}\ntry { ${body} } finally { ${copyBack} } } }`;
}

function persistedCallback(
  source: string | undefined,
  functionName: "migrate" | "validate",
): string | undefined {
  if (!source?.trim()) return undefined;
  const body = eraseFunctionTypes(source);
  if (functionName === "migrate") {
    return `(value, fromVersion, toVersion) => {\n${body}\nif (typeof migrate === "function") return migrate(value, fromVersion, toVersion);\nreturn value;\n}`;
  }
  return `(value) => {\n${body}\nif (typeof validate === "function") return validate(value);\nreturn value && typeof value === "object" && !Array.isArray(value) ? value : null;\n}`;
}

function persistenceSource(ast: PageAst): string {
  if (!ast.persist) return "undefined";
  const migrate = persistedCallback(ast.persist.migrations, "migrate");
  const validate = persistedCallback(ast.persist.validation, "validate");
  return `{ storage: ${JSON.stringify(ast.persist.storage)}, include: ${JSON.stringify(ast.persist.include)}, version: ${ast.persist.version}${migrate ? `, migrate: ${migrate}` : ""}${validate ? `, validate: ${validate}` : ""} }`;
}

function lifecycleSource(ast: PageAst, stateNames: string[], browser: boolean): string {
  return Object.entries(ast.storeLifecycle)
    .filter(([name]) => !browser || name !== "serverInit")
    .map(([name, body]) => {
      const aliases = stateNames.filter(safeBinding);
      const aliasSource = aliases.length ? `let { ${aliases.join(", ")} } = context.state;` : "";
      const runtimeAliases = browser ? "const server = context.server;" : "";
      const copyBack = aliases.map((state) => `context.state.${state} = ${state};`).join("\n");
      const emittedBody = browser ? eraseFunctionTypes(body) : body;
      return `${name}: async (context) => { ${runtimeAliases} ${aliasSource} try { ${emittedBody} } finally { ${copyBack} } }`;
    })
    .join(",\n");
}

export function generateStoreModule(ast: PageAst): string {
  if (ast.kind !== "global-store" && ast.kind !== "page-store") {
    throw new Error("generateStoreModule requires a store AST");
  }
  const stateNames = ast.states.map((state) => state.name);
  const safeStateNames = stateNames.filter(safeBinding);
  const computed = ast.computed
    .map(
      (entry) =>
        `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`,
    )
    .join(",\n");
  const actionGroups = new Map<string, RuntimeFunctionDecl[]>();
  for (const fn of ast.runtimeFunctions) {
    const group = actionGroups.get(fn.name) ?? [];
    group.push(fn);
    actionGroups.set(fn.name, group);
  }
  const actions = Array.from(
    actionGroups,
    ([name, functions]) =>
      `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames)).join(", ")}]`,
  ).join(",\n");
  const persistence = persistenceSource(ast);
  const lifecycle = lifecycleSource(ast, stateNames, false);
  const manifest = rpcManifest(ast);
  const remoteFunctions = manifest.map((entry) => entry.function);
  const rpcWrappers = remoteFunctions
    .map(
      (name) => `${JSON.stringify(name)}: async (...received) => {
    const rpcContext = received.pop();
    if (!rpcContext || !rpcContext.request) throw new Error("WRN-RPC-CONTEXT: request context is required");
    let container = __wrnexusRpcContainers.get(rpcContext.request);
    if (!container) {
      const url = new URL(rpcContext.request.url);
      container = createRequestStoreContainer(rpcContext.request, url.pathname + url.search);
      __wrnexusRpcContainers.set(rpcContext.request, container);
    }
    const store = await container.use(${ast.name});
    const action = store.actions[${JSON.stringify(name)}];
    if (typeof action !== "function") throw new Error(${JSON.stringify(`WRN-RPC-FUNCTION: store action '${name}' is unavailable on the server`)});
    return action(...received);
  }`,
    )
    .join(",\n");

  return `${ast.imports.join("\n")}\nimport { defineStore } from "@wrnexus/store";\nimport { createRequestStoreContainer } from "@wrnexus/store/server";\n\n${ast.types.join("\n\n")}\n\nexport const ${ast.name} = defineStore({\n  name: ${JSON.stringify(ast.name)},\n  kind: ${JSON.stringify(ast.storeKind)},\n  createSharedState: () => (${stateObject(ast, "shared")}),\n  createClientState: () => (${stateObject(ast, "client")}),\n  createServerState: () => (${stateObject(ast, "server")}),\n  computed: { ${computed} },\n  actions: { ${actions} },\n  persist: ${persistence},\n  lifecycle: { ${lifecycle} },\n});\n\nexport default ${ast.name};\n\nconst __wrnexusRpcContainers = new WeakMap();\nexport const __wrnexusServerFunctions = {\n${rpcWrappers}\n};\nexport const __wrnexusRpcManifest = ${JSON.stringify(manifest, null, 2)};\n\n${generateDeclarations(ast)}\n`;
}

/** Standalone browser artifact for an imported `.wrn` store. */
export function generateStoreBrowserModule(ast: PageAst): string {
  if (ast.kind !== "global-store" && ast.kind !== "page-store") {
    throw new Error("generateStoreBrowserModule requires a store AST");
  }
  const browserStates = ast.states.filter((state) => state.runtime !== "server");
  const stateNames = browserStates.map((state) => state.name);
  const safeStateNames = stateNames.filter(safeBinding);
  const initialState = `{ ${browserStates.map((state) => `${JSON.stringify(state.name)}: (${state.expr})`).join(", ")} }`;
  const computed = ast.computed
    .map(
      (entry) =>
        `${JSON.stringify(entry.name)}: (state) => { ${safeStateNames.length ? `const { ${safeStateNames.join(", ")} } = state;` : ""} return (${entry.expr}); }`,
    )
    .join(",\n");
  const groups = new Map<string, RuntimeFunctionDecl[]>();
  for (const fn of ast.runtimeFunctions.filter((entry) =>
    ["client", "shared", "legacy"].includes(entry.runtime),
  )) {
    const group = groups.get(fn.name) ?? [];
    group.push(fn);
    groups.set(fn.name, group);
  }
  const actions = Array.from(
    groups,
    ([name, functions]) =>
      `${JSON.stringify(name)}: [${functions.map((fn) => actionSource(fn, stateNames, true)).join(", ")}]`,
  ).join(",\n");
  const persistence = persistenceSource(ast);
  const lifecycleEntries = lifecycleSource(ast, stateNames, true);

  return `// generated WRNexusJS browser store module for ${ast.name}
const __root = globalThis;
const __registry = __root.__wrnexusStoreRegistry || (__root.__wrnexusStoreRegistry = new Map());
const __hydrationNode = typeof document !== "undefined" ? document.querySelector("script[data-wrnexus-store-hydration]") : null;
let __hydration = {};
try { __hydration = __hydrationNode ? JSON.parse(__hydrationNode.textContent || "{}") : {}; } catch (_) {}
function __clone(value) { try { return structuredClone(value); } catch (_) { return JSON.parse(JSON.stringify(value)); } }
function __storage(kind) { if (typeof window === "undefined") return null; return kind === "local" ? window.localStorage : kind === "session" ? window.sessionStorage : null; }
function __diagnostic(code, message, details) {
  const detail = { code, message, store: ${JSON.stringify(ast.name)}, details };
  try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-diagnostic", { detail })); } catch (_) {}
  if (typeof console !== "undefined" && console.warn) console.warn("[wrnexus:store] " + code + ": " + message, details || "");
}
function __csrfToken() {
  if (typeof document === "undefined") return undefined;
  const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || "");
  return match ? decodeURIComponent(match[1]) : undefined;
}
async function __callServerFunction(storeName, functionName, args, options) {
  options = options || {};
  const csrf = options.csrfToken || __csrfToken();
  const traceId = options.traceId || (globalThis.crypto && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()));
  const response = await fetch(options.endpoint || "/__wrnexus/rpc", {
    method: "POST",
    credentials: "same-origin",
    signal: options.signal,
    headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}),
    body: JSON.stringify({ component: storeName, function: functionName, args: args }),
  });
  const payload = await response.json().catch(function () { return null; });
  if (!response.ok || !payload || !payload.ok) {
    const error = new Error(payload && payload.error && payload.error.message || "Server call failed (" + response.status + ")");
    error.code = payload && payload.error && payload.error.code || "WRN-RPC-FAILED";
    error.status = response.status;
    error.details = payload && payload.error && payload.error.details;
    error.traceId = payload && payload.error && payload.error.traceId || traceId;
    throw error;
  }
  return payload.value;
}
function __compatible(expected, value) {
  if (expected === null || value === null) return expected === value || expected === null;
  if (Array.isArray(expected)) return Array.isArray(value);
  return typeof expected === typeof value;
}
function __create(definition) {
  const routeId = typeof location !== "undefined" ? location.pathname + location.search : "default";
  const key = definition.kind === "page" ? definition.name + "@" + routeId : definition.name;
  if (__registry.has(key)) return __registry.get(key);
  let currentDefinition = definition;
  const listeners = new Set();
  const initial = currentDefinition.createState();
  let restored = null;
  if (currentDefinition.persist) {
    try {
      const storage = __storage(currentDefinition.persist.storage);
      const rawValue = storage && storage.getItem("wrnexus:store:" + currentDefinition.name);
      const parsed = rawValue ? JSON.parse(rawValue) : null;
      if (parsed) {
        let candidate = parsed.state;
        const fromVersion = Number(parsed.version || 0);
        if (fromVersion !== currentDefinition.persist.version) {
          if (typeof currentDefinition.persist.migrate === "function") candidate = currentDefinition.persist.migrate(candidate, fromVersion, currentDefinition.persist.version);
          else { __diagnostic("WRN-PERSIST-VERSION", "Persisted state version cannot be restored without a migration.", { fromVersion, toVersion: currentDefinition.persist.version }); candidate = null; }
        }
        if (candidate && typeof currentDefinition.persist.validate === "function") candidate = currentDefinition.persist.validate(candidate);
        if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
          restored = {};
          currentDefinition.persist.include.forEach(function (name) { if (Object.prototype.hasOwnProperty.call(candidate, name)) restored[name] = candidate[name]; });
        } else if (candidate != null) {
          __diagnostic("WRN-PERSIST-INVALID", "Persisted state failed validation and was reset.", candidate);
        }
      }
    } catch (error) { __diagnostic("WRN-PERSIST-RESTORE", "Persisted state could not be restored and was reset.", error); }
  }
  const raw = Object.assign({}, initial, restored || {}, __hydration[currentDefinition.name] || {});
  let mutable = false;
  let actionName = "direct";
  function persistState() {
    if (!currentDefinition.persist) return;
    try {
      const picked = {};
      currentDefinition.persist.include.forEach(function (name) { picked[name] = raw[name]; });
      const storage = __storage(currentDefinition.persist.storage);
      if (storage) storage.setItem("wrnexus:store:" + currentDefinition.name, JSON.stringify({ version: currentDefinition.persist.version, state: picked }));
    } catch (error) { __diagnostic("WRN-PERSIST-WRITE", "Persisted state could not be written.", error); }
  }
  const state = new Proxy(raw, {
    set(target, property, value) {
      if (!mutable) throw new TypeError("WRN-STORE-READONLY: " + currentDefinition.name + "." + String(property) + " must be changed by a store action.");
      if (Object.is(target[property], value)) return true;
      target[property] = value;
      persistState();
      listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: actionName, changed: [String(property)] }); });
      return true;
    },
    deleteProperty(target, property) {
      if (!mutable) throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions");
      return Reflect.deleteProperty(target, property);
    },
  });
  const actions = {};
  const server = new Proxy({}, { get: function (_target, property) { return function () { return __callServerFunction(currentDefinition.name, String(property), Array.prototype.slice.call(arguments)); }; } });
  function installActions() {
    Object.keys(actions).forEach(function (name) { delete actions[name]; });
    Object.entries(currentDefinition.actions || {}).forEach(function (pair) {
      const name = pair[0], candidates = pair[1];
      const selected = candidates.find(function (entry) { return entry.runtime === "client"; }) || candidates.find(function (entry) { return entry.runtime === "shared"; }) || candidates.find(function (entry) { return entry.runtime === "legacy"; });
      if (!selected) return;
      actions[name] = async function () {
        const args = Array.prototype.slice.call(arguments);
        const previousMutable = mutable, previousAction = actionName;
        mutable = true; actionName = name;
        try { return await selected.handler({ state, snapshot: function () { return __clone(state); }, reset: function () { return instance.reset(); }, runtime: "client", routeId, server }, ...args); }
        finally { mutable = previousMutable; actionName = previousAction; }
      };
    });
  }
  installActions();
  const core = {
    name: currentDefinition.name,
    kind: currentDefinition.kind,
    state,
    actions,
    whenReady: Promise.resolve(),
    reset() {
      mutable = true; actionName = "$reset";
      try {
        const next = currentDefinition.createState();
        Object.keys(raw).forEach(function (name) { if (!(name in next)) delete raw[name]; });
        Object.assign(raw, next); persistState();
        listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$reset", changed: Object.keys(next) }); });
      } finally { mutable = false; actionName = "direct"; }
    },
    snapshot() { return Object.freeze(__clone(raw)); },
    subscribe(listener) { listeners.add(listener); return function () { listeners.delete(listener); }; },
    async dispose() {
      mutable = true; actionName = "$dispose";
      try { await currentDefinition.lifecycle && currentDefinition.lifecycle.dispose && currentDefinition.lifecycle.dispose({ state, runtime: "client", routeId, server }); }
      finally { mutable = false; actionName = "direct"; listeners.clear(); __registry.delete(key); }
    },
    async __hotUpdate(nextDefinition) {
      const previous = __clone(raw);
      const nextShape = nextDefinition.createState();
      const preserved = [], reset = [], added = [], removed = [];
      Object.keys(previous).forEach(function (name) {
        if (!(name in nextShape)) { removed.push(name); return; }
        if (__compatible(nextShape[name], previous[name])) { nextShape[name] = previous[name]; preserved.push(name); }
        else reset.push(name);
      });
      Object.keys(nextShape).forEach(function (name) { if (!(name in previous)) added.push(name); });
      currentDefinition = nextDefinition;
      mutable = true; actionName = "$hmr";
      try {
        Object.keys(raw).forEach(function (name) { delete raw[name]; });
        Object.assign(raw, nextShape);
        installActions(); persistState();
      } finally { mutable = false; actionName = "direct"; }
      const result = { store: currentDefinition.name, preserved, reset, added, removed };
      listeners.forEach(function (listener) { listener(instance.snapshot(), { store: currentDefinition.name, action: "$hmr", changed: added.concat(reset, removed) }); });
      try { __root.dispatchEvent && __root.dispatchEvent(new CustomEvent("wrnexus:store-hmr", { detail: result })); } catch (_) {}
      return result;
    },
  };
  const instance = new Proxy(core, {
    get(target, property, receiver) {
      if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
      if (property in actions) return actions[property];
      if (property in (currentDefinition.computed || {})) return currentDefinition.computed[property](state);
      return state[property];
    },
    set() { throw new TypeError("WRN-STORE-READONLY: store state is readonly outside actions"); },
  });
  __registry.set(key, instance);
  const hydrationSource = __hydration[currentDefinition.name];
  const init = async function () {
    const run = async function (name, hook) {
      if (!hook) return;
      mutable = true; actionName = name;
      try { await hook({ state, runtime: "client", routeId, server }); }
      finally { mutable = false; actionName = "direct"; }
    };
    await run("$clientInit", currentDefinition.lifecycle && currentDefinition.lifecycle.clientInit);
    if (hydrationSource) await run("$hydrate", currentDefinition.lifecycle && currentDefinition.lifecycle.hydrate);
  };
  core.whenReady = init();
  return instance;
}
if (!__root.__wrnexusApplyStoreHotUpdate) {
  __root.__wrnexusApplyStoreHotUpdate = async function (name, definition) {
    const results = [];
    for (const item of Array.from(__registry.values())) if (item.name === name && typeof item.__hotUpdate === "function") results.push(await item.__hotUpdate(definition));
    return results;
  };
}
if (!__root.__wrnexusStoreContainer) {
  __root.__wrnexusStoreContainer = {
    async disposePageStores() { for (const item of Array.from(__registry.values())) if (item.kind === "page") await item.dispose(); },
    async hotUpdate(name, definition) { return __root.__wrnexusApplyStoreHotUpdate(name, definition); },
    inspect() { return Array.from(__registry.values()).map(function (item) { return { name: item.name, kind: item.kind, state: item.snapshot() }; }); },
  };
}
export const ${ast.name}Definition = {
  name: ${JSON.stringify(ast.name)},
  kind: ${JSON.stringify(ast.storeKind)},
  createState: () => (${initialState}),
  computed: { ${computed} },
  actions: { ${actions} },
  persist: ${persistence},
  lifecycle: { ${lifecycleEntries} },
};
export const ${ast.name} = __create(${ast.name}Definition);
export default ${ast.name};
`;
}
