import {
  convexToJson,
  jsonToConvex,
  version,
  STATUS_CODE_UDF_FAILED,
} from "@convex-dev/common";
import { ClientConfiguration } from "./client_config.js";
import { createError, logToConsole } from "./logging.js";

/** Isomorphic `fetch` for Node.js and browser usage. */
const hasFetch =
  typeof window !== "undefined" && typeof window.fetch !== "undefined";
type WindowFetch = typeof window.fetch;
const fetch: WindowFetch = hasFetch
  ? window.fetch
  : (...args) =>
      import("node-fetch").then(({ default: fetch }) =>
        (fetch as unknown as WindowFetch)(...args)
      );

// TODO Typedoc doesn't generate documentation for the comment below perhaps
// because it's a callable interface.
/**
 * An interface to execute a Convex query function on the server.
 *
 * @public
 */
export interface Query<F extends (...args: any[]) => Promise<any>> {
  /**
   * Execute the query on the server, returning a `Promise` of the return value.
   *
   * @param args - Arguments for the query.
   * @returns The result of the query.
   */
  (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>;
}

// TODO Typedoc doesn't generate documentation for the comment below perhaps
// because it's a callable interface.
/**
 * An interface to execute a Convex mutation function on the server.
 *
 * @public
 */
export interface Mutation<F extends (...args: any[]) => Promise<any>> {
  /**
   * Execute the mutation on the server, returning a `Promise` of its return value.
   *
   * @param args - Arguments for the mutation.
   * @returns The return value of the server-side function call.
   */
  (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>;
}

/**
 * A Convex client that runs queries and mutations over HTTP.
 *
 * This is appropriate for server-side code (like Netlify Lambdas) or non-reactive
 * webapps.
 *
 * If you're building a React app, consider using
 * {@link react.ConvexReactClient} instead.
 *
 *
 * @public
 */
export class ConvexHttpClient {
  private readonly address: string;
  private auth?: string;
  constructor(clientConfig: ClientConfiguration) {
    this.address = `${clientConfig.address}/api/${version}`;
  }

  /**
   * Obtain the {@link ConvexHttpClient}'s URL to its backend.
   *
   * @returns The URL to the Convex backend, including the client's API version.
   */
  backendUrl(): string {
    return this.address;
  }

  /**
   * Set the authentication token to be used for subsequent queries and mutations.
   *
   * Should be called whenever the token changes (i.e. due to expiration and refresh).
   *
   * @param value - JWT-encoded OpenID Connect identity token.
   */
  setAuth(value: string) {
    this.auth = value;
  }

  /**
   * Clear the current authentication token if set.
   */
  clearAuth() {
    this.auth = undefined;
  }

  /**
   * Construct a new {@link Query}.
   *
   * @param name - The name of the query function.
   * @returns The {@link Query} object with that name.
   */
  query<F extends (...args: any[]) => Promise<any>>(name: string): Query<F> {
    return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
      // Interpret the arguments as a Convex array, and then serialize
      // it to JSON.
      const argsJSON = JSON.stringify(convexToJson(args));
      const argsComponent = encodeURIComponent(argsJSON);
      const url = `${this.address}/udf?path=${name}&args=${argsComponent}`;
      const headers: Record<string, string> = this.auth
        ? { Authorization: `Bearer ${this.auth}` }
        : {};

      const response = await fetch(url, {
        credentials: "include",
        headers: headers,
      });
      if (!response.ok && response.status != STATUS_CODE_UDF_FAILED) {
        throw new Error(await response.text());
      }
      const respJSON = await response.json();

      const value = jsonToConvex(respJSON.value);
      for (const line of respJSON.logs) {
        logToConsole("info", "query", name, line);
      }
      if (!respJSON.success) {
        throw createError("query", name, value as string);
      }
      return value as ReturnType<F>;
    };
  }

  /**
   * Construct a new {@link Mutation}.
   *
   * @param name - The name of the mutation function.
   * @returns The {@link Mutation} object with that name.
   */
  mutation<F extends (...args: any[]) => Promise<any>>(
    name: string
  ): Mutation<F> {
    return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
      // Interpret the arguments as a Convex array and then serialize to JSON.
      const body = JSON.stringify({
        path: name,
        args: convexToJson(args),
        tokens: [],
      });
      const headers: Record<string, string> = {
        "Content-Type": "application/json",
      };
      if (this.auth) {
        headers["Authorization"] = `Bearer ${this.auth}`;
      }
      const response = await fetch(`${this.address}/udf`, {
        body,
        method: "POST",
        headers: headers,
        credentials: "include",
      });
      if (!response.ok && response.status != STATUS_CODE_UDF_FAILED) {
        throw new Error(await response.text());
      }
      const respJSON = await response.json();
      const value = jsonToConvex(respJSON.value);
      for (const line of respJSON.logs) {
        logToConsole("info", "mutation", name, line);
      }
      if (!respJSON.success) {
        throw createError("mutation", name, value as string);
      }
      return value as ReturnType<F>;
    };
  }
}
