import { AxiosAdapter, AxiosError, AxiosResponse } from "axios";
import { RastackEngine, RastackEngineLoader, RastackWasmModule } from "./types";

/**
 * Default WASM loader: import the bundle `rastack wasm` generates
 * (`rastack/wasm/rastack_wasm.js`) and run its init. The indirection through
 * `new Function` keeps a genuine dynamic `import()` at runtime even when the
 * package is transpiled to CommonJS, so the consumer's bundler resolves and
 * code-splits the `.wasm`.
 */
const runtimeImport: (specifier: string) => Promise<any> =
  // eslint-disable-next-line no-new-func
  new Function("s", "return import(s)") as any;

export const defaultEngineLoader: RastackEngineLoader = async () => {
  const mod: RastackWasmModule = await runtimeImport("rastack/wasm/rastack_wasm.js");
  const init = mod.default || mod.init;
  if (typeof init === "function") {
    await init();
  }
  return mod;
};

/**
 * Instantiate the engine from a loaded module and the compiled schema. The
 * manifest/openapi are passed as JSON strings — the same `schema.rastack.json` /
 * `openapi.json` the Rust server reads.
 */
export function createEngine(
  mod: RastackWasmModule,
  manifest: object | string,
  openapi?: object | string,
): RastackEngine {
  const manifestJson =
    typeof manifest === "string" ? manifest : JSON.stringify(manifest);
  const openapiJson =
    openapi === undefined
      ? undefined
      : typeof openapi === "string"
        ? openapi
        : JSON.stringify(openapi);
  return new mod.RastackApi(manifestJson, openapiJson);
}

const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);

/**
 * An axios adapter that dispatches requests into the in-browser WASM engine
 * instead of over the network. Translates an axios request into the engine's
 * `handle(method, path, query, body)` and maps the `{ status, body }` result
 * back to an `AxiosResponse` — rejecting on non-2xx exactly as HTTP axios does,
 * so `react-query` error handling is unchanged.
 *
 * `onWrite` fires after each successful mutation so the provider can debounce a
 * persist of the warehouse.
 */
export function createWasmAdapter(
  engine: RastackEngine,
  onWrite?: () => void,
): AxiosAdapter {
  return async (config: any): Promise<AxiosResponse> => {
    const method = String(config.method || "get").toUpperCase();

    const rawUrl = config.url || "";
    const [rawPath, rawQuery] = rawUrl.split("?");
    // Strip any origin so only the `/api/...` path reaches the engine.
    const path = rawPath.replace(/^https?:\/\/[^/]+/i, "") || "/";

    const query: Record<string, string> = {};
    if (rawQuery) {
      new URLSearchParams(rawQuery).forEach((value, key) => {
        query[key] = value;
      });
    }
    if (config.params && typeof config.params === "object") {
      for (const [key, value] of Object.entries(config.params)) {
        if (value !== undefined && value !== null) query[key] = String(value);
      }
    }

    // axios has already run transformRequest, so JSON bodies arrive as strings.
    let body: unknown;
    if (typeof config.data === "string" && config.data.length > 0) {
      try {
        body = JSON.parse(config.data);
      } catch {
        body = config.data;
      }
    } else if (config.data && typeof config.data === "object") {
      body = config.data;
    }

    const result = await engine.handle(
      method,
      path,
      Object.keys(query).length ? query : undefined,
      body,
    );

    const response: AxiosResponse = {
      data: result.body,
      status: result.status,
      statusText: String(result.status),
      headers: {},
      config,
      request: null,
    };

    const validate =
      config.validateStatus || ((s: number) => s >= 200 && s < 300);
    if (!validate(result.status)) {
      throw new AxiosError(
        `Request failed with status code ${result.status}`,
        result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
        config,
        null,
        response,
      );
    }

    if (MUTATING.has(method) && onWrite) onWrite();
    return response;
  };
}
