import { PropertyValidators } from "../../values/index.js";
import { version } from "../../index.js";
import {
  AnyFunctionReference,
  FunctionReference,
  FunctionType,
} from "../api.js";
import { performAsyncSyscall } from "../impl/syscall.js";
import { DefaultFunctionArgs } from "../registration.js";
import {
  AppDefinitionAnalysis,
  ComponentDefinitionAnalysis,
  ComponentDefinitionType,
} from "./definition.js";
import {
  getFunctionAddress,
  setReferencePath,
  toReferencePath,
} from "./paths.js";
import type {
  Validator,
  VLiteral,
  VOptional,
  VString,
  VUnion,
} from "../../values/validators.js";
import type { Infer } from "../../values/validator.js";
import { isValidator } from "../../values/validator.js";
import type { Expand } from "../../type_utils.js";
export { getFunctionAddress } from "./paths.js";

/**
 * A serializable reference to a Convex function.
 * Passing a this reference to another component allows that component to call this
 * function during the current function execution or at any later time.
 * Function handles are used like `api.folder.function` FunctionReferences,
 * e.g. `ctx.scheduler.runAfter(0, functionReference, args)`.
 *
 * A function reference is stable across code pushes but it's possible
 * the Convex function it refers to might no longer exist.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export type FunctionHandle<
  Type extends FunctionType,
  Args extends DefaultFunctionArgs = any,
  ReturnType = any,
> = string & FunctionReference<Type, "internal", Args, ReturnType>;

/**
 * Create a serializable reference to a Convex function.
 * Passing a this reference to another component allows that component to call this
 * function during the current function execution or at any later time.
 * Function handles are used like `api.folder.function` FunctionReferences,
 * e.g. `ctx.scheduler.runAfter(0, functionReference, args)`.
 *
 * A function reference is stable across code pushes but it's possible
 * the Convex function it refers to might no longer exist.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export async function createFunctionHandle<
  Type extends FunctionType,
  Args extends DefaultFunctionArgs,
  ReturnType,
>(
  functionReference: FunctionReference<
    Type,
    "public" | "internal",
    Args,
    ReturnType
  >,
): Promise<FunctionHandle<Type, Args, ReturnType>> {
  const address = getFunctionAddress(functionReference);
  return await performAsyncSyscall("1.0/createFunctionHandle", {
    ...address,
    version,
  });
}

interface ComponentExports {
  [key: string]: FunctionReference<any, any, any, any> | ComponentExports;
}

/**
 * An object of this type should be the default export of a
 * convex.config.ts file in a component definition directory.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export type ComponentDefinition<
  Exports extends ComponentExports = any,
  Env extends EnvDefinition = {},
> = {
  /**
   * Install a component with the given definition in this component definition.
   *
   * Takes a component definition and an optional name.
   *
   * For editor tooling this method expects a {@link ComponentDefinition}
   * but at runtime the object that is imported will be a {@link ImportedComponentDefinition}
   */
  use<Definition extends ComponentDefinition<any, any>>(
    definition: Definition,
    options?: UseOptions<Definition>,
  ): InstalledComponent<Definition>;

  /**
   * Internal type-only property tracking exports provided.
   *
   * @deprecated This is a type-only property, don't use it.
   */
  __exports: Exports;

  /**
   * References to this component's declared env vars. Pass one of these in
   * `app.use(child, { env: { ... } })` to bind a child's env var by
   * reference to this component's env var.
   */
  env: EnvRefFromDefinition<Env>;

  /**
   * Internal type-only property tracking env definition.
   *
   * @deprecated This is a type-only property, don't use it.
   */
  __env: Env;
};

type ComponentDefinitionExports<T extends ComponentDefinition<any, any>> =
  T["__exports"];

type ComponentDefinitionEnv<T extends ComponentDefinition<any, any>> =
  T["__env"];

/**
 * Options for installing a component via `app.use()` or `component.use()`.
 *
 * If the component declares required env vars, the `env` property is required.
 */
type UseOptions<Definition extends ComponentDefinition<any, any>> =
  keyof ComponentDefinitionEnv<Definition> extends never
    ? { name?: string; httpPrefix?: string }
    : {
        name?: string;
        httpPrefix?: string;
        env: UseOptionsEnv<ComponentDefinitionEnv<Definition>>;
      };

type UseOptionsEnv<E extends EnvDefinition> = Expand<
  {
    [K in keyof E as E[K] extends VOptional<any> ? never : K]:
      | Infer<E[K]>
      | EnvRef;
  } & {
    [K in keyof E as E[K] extends VOptional<any> ? K : never]?:
      | Infer<E[K]>
      | EnvRef
      | undefined;
  }
>;

/**
 * A string-like validator: `v.string()`, a string `v.literal("...")`, or a
 * `v.union(...)` of those (recursively). Component env vars are serialized
 * as strings on the wire, so only string-typed validators are allowed.
 *
 * @public
 */
export type StringLikeValidator =
  | VString<string, "required">
  | VLiteral<string, "required">
  | VUnion<string, Validator<any, "required", any>[], "required">;

/**
 * A definition of environment variables for the app.
 *
 * Maps environment variable names to string-like validators. Use
 * `v.string()` for a plain string, `v.literal("a")` for an enum value, or
 * `v.union(v.literal("a"), v.literal("b"))` for an enum. Wrap in
 * `v.optional(...)` for optional vars.
 *
 * @example
 * ```typescript
 * import { defineApp } from "convex/server";
 * import { v } from "convex/values";
 *
 * const app = defineApp({
 *   env: {
 *     OPENAI_API_KEY: v.string(),
 *     DEBUG_MODE: v.optional(v.string()),
 *   },
 * });
 * ```
 *
 * @public
 */
export type EnvDefinition = Record<
  string,
  StringLikeValidator | VOptional<StringLikeValidator>
>;

/**
 * Compute the typed environment object from an {@link EnvDefinition}.
 *
 * Required entries get the validator's inferred string type; optional
 * entries are `T | undefined`.
 *
 * @public
 */
export type EnvFromDefinition<E extends EnvDefinition> = Expand<
  {
    [K in keyof E as E[K] extends VOptional<any> ? never : K]: Infer<E[K]>;
  } & {
    [K in keyof E as E[K] extends VOptional<any> ? K : never]?:
      | Infer<E[K]>
      | undefined;
  }
>;

/**
 * A reference to a parent-declared env var, produced by `app.env.<NAME>` or
 * `component.env.<NAME>`. Pass this in `use(child, { env: { ... } })` to
 * bind a child's declared env var to the parent's env var by reference
 * instead of snapshotting its current value.
 *
 * @public
 */
export type EnvRef<K extends string = string> = { __envVarRef: K };

/**
 * Compute the typed `env` namespace object from an {@link EnvDefinition}.
 * Each declared name maps to an {@link EnvRef} for that name.
 *
 * @public
 */
export type EnvRefFromDefinition<E extends EnvDefinition> = {
  [K in keyof E & string]: EnvRef<K>;
};

/**
 * Extract the typed environment from an {@link AppDefinition}.
 *
 * @public
 */
export type EnvFromAppDefinition<A> =
  A extends AppDefinition<infer E>
    ? EnvFromDefinition<E>
    : Record<string, never>;

/**
 * An object of this type should be the default export of a
 * convex.config.ts file in a component-aware convex directory.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export type AppDefinition<Env extends EnvDefinition = EnvDefinition> = {
  /**
   * Install a component with the given definition in this component definition.
   *
   * Takes a component definition and an optional name.
   *
   * For editor tooling this method expects a {@link ComponentDefinition}
   * but at runtime the object that is imported will be a {@link ImportedComponentDefinition}
   */
  use<Definition extends ComponentDefinition<any, any>>(
    definition: Definition,
    options?: UseOptions<Definition>,
  ): InstalledComponent<Definition>;

  /**
   * References to this app's declared env vars. Pass one of these in
   * `app.use(child, { env: { ... } })` to bind a child's env var by
   * reference to this app's env var.
   */
  env: EnvRefFromDefinition<Env>;

  /**
   * Internal type-only property tracking env definition.
   *
   * @deprecated This is a type-only property, don't use it.
   */
  __env: Env;
};

interface ExportTree {
  // Tree with serialized `Reference`s as leaves.
  [key: string]: string | ExportTree;
}

type CommonDefinitionData = {
  _isRoot: boolean;
  _childComponents: [
    string,
    ImportedComponentDefinition,
    Record<string, any> | null,
    string | undefined,
  ][];
  _exportTree: ExportTree;
};

type ComponentDefinitionData = CommonDefinitionData & {
  _env: PropertyValidators;
  _name: string;
  _onInitCallbacks: Record<string, (argsStr: string) => string>;
};
type AppDefinitionData = CommonDefinitionData & {
  _httpPrefix?: string;
  _env?: EnvDefinition;
};

/**
 * Used to refer to an already-installed component.
 */
class InstalledComponent<Definition extends ComponentDefinition<any, any>> {
  /**
   * @internal
   */
  _definition: Definition;

  /**
   * @internal
   */
  _name: string;

  constructor(definition: Definition, name: string) {
    this._definition = definition;
    this._name = name;
    setReferencePath(this, `_reference/childComponent/${name}`);
  }

  get exports(): ComponentDefinitionExports<Definition> {
    return createExports(this._name, []);
  }
}

function createExports(name: string, pathParts: string[]): any {
  const handler: ProxyHandler<any> = {
    get(_, prop: string | symbol) {
      if (typeof prop === "string") {
        const newParts = [...pathParts, prop];
        return createExports(name, newParts);
      } else if (prop === toReferencePath) {
        let reference = `_reference/childComponent/${name}`;
        for (const part of pathParts) {
          reference += `/${part}`;
        }
        return reference;
      } else {
        return undefined;
      }
    },
  };
  return new Proxy({}, handler);
}

function createEnvRefs(
  ownerLabel: string,
  declared: Record<string, any> | undefined,
): any {
  const handler: ProxyHandler<any> = {
    get(_, prop: string | symbol) {
      if (typeof prop !== "string") {
        return undefined;
      }
      if (!declared || !Object.prototype.hasOwnProperty.call(declared, prop)) {
        throw new Error(
          `Env var "${prop}" is not declared on ${ownerLabel}. Add it to the \`env\` option of ${ownerLabel === "this app" ? "defineApp" : "defineComponent"}.`,
        );
      }
      return { __envVarRef: prop };
    },
  };
  return new Proxy({}, handler);
}

function isEnvRef(value: unknown): value is EnvRef {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as EnvRef).__envVarRef === "string"
  );
}

function use<Definition extends ComponentDefinition<any, any>>(
  this: CommonDefinitionData,
  definition: Definition,
  options?: {
    name?: string;
    httpPrefix?: string;
    env?: Record<string, any>;
  },
): InstalledComponent<Definition> {
  // At runtime an imported component will have this shape.
  const importedComponentDefinition =
    definition as unknown as ImportedComponentDefinition;
  if (typeof importedComponentDefinition.componentDefinitionPath !== "string") {
    throw new Error(
      "Component definition does not have the required componentDefinitionPath property. This code only works in Convex runtime.",
    );
  }
  const name =
    options?.name ??
    // added recently
    importedComponentDefinition.defaultName ??
    // can be removed once backend is out
    importedComponentDefinition.componentDefinitionPath.split("/").pop()!;

  if (typeof name !== "string") {
    throw new Error(
      `Component name must be a string. Received: ${typeof name}`,
    );
  }
  if (name.length === 0) {
    // "" is used internally as the name for the root component, so
    // users shouldn't try to define child components with an empty name.
    throw new Error("Component name cannot be empty.");
  }

  const httpPrefix = options?.httpPrefix;
  if (httpPrefix !== undefined) {
    if (!httpPrefix.startsWith("/")) {
      throw new Error(
        `httpPrefix must start with "/". Received: "${httpPrefix}"`,
      );
    }
  }

  const envValues: Record<string, any> = {};
  if (options?.env) {
    for (const [key, value] of Object.entries(options.env)) {
      if (value !== undefined) {
        envValues[key] = value;
      }
    }
  }

  this._childComponents.push([
    name,
    importedComponentDefinition,
    envValues,
    httpPrefix,
  ]);
  return new InstalledComponent(definition, name);
}

/**
 * The runtime type of a ComponentDefinition. TypeScript will claim
 * the default export of a module like "cool-component/convex.config.js"
 * is a `@link ComponentDefinition}, but during component definition evaluation
 * this is its type instead.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export type ImportedComponentDefinition = {
  componentDefinitionPath: string;
  defaultName: string;
};

function exportAppForAnalysis(
  this: ComponentDefinition<any, any> & AppDefinitionData,
): AppDefinitionAnalysis {
  const definitionType = { type: "app" as const };
  const childComponents = serializeChildComponents(this._childComponents);
  const httpMounts = buildHttpMounts(this._childComponents);
  const envVars = this._env
    ? Object.entries(this._env).map(
        ([name, validator]) =>
          [
            name,
            {
              type: "value" as const,
              value: JSON.stringify(validator.json),
              ...(validator.isOptional === "optional"
                ? { optional: true }
                : {}),
            },
          ] as [string, { type: "value"; value: string; optional?: boolean }],
      )
    : undefined;
  return {
    definitionType,
    ...(this._httpPrefix !== undefined
      ? { httpPrefix: normalizeHttpPrefix(this._httpPrefix) }
      : {}),
    childComponents: childComponents as any,
    httpMounts,
    exports: serializeExportTree(this._exportTree),
    ...(envVars !== undefined ? { envVars } : {}),
  };
}

function serializeExportTree(tree: ExportTree): any {
  const branch: any[] = [];
  for (const [key, child] of Object.entries(tree)) {
    let node;
    if (typeof child === "string") {
      node = { type: "leaf", leaf: child };
    } else {
      node = serializeExportTree(child);
    }
    branch.push([key, node]);
  }
  return { type: "branch", branch };
}

function normalizeHttpPrefix(prefix: string): string {
  // Ensure the prefix ends with "/" as required by HttpMountPath in Rust.
  return prefix.endsWith("/") ? prefix : prefix + "/";
}

function buildHttpMounts(
  childComponents: [
    string,
    ImportedComponentDefinition,
    Record<string, any> | null,
    string | undefined,
  ][],
): Record<string, string> {
  const httpMounts: Record<string, string> = {};
  for (const [name, , , httpPrefix] of childComponents) {
    if (httpPrefix !== undefined) {
      const normalized = normalizeHttpPrefix(httpPrefix);
      httpMounts[normalized] = `_reference/childComponent/${name}`;
    }
  }
  return httpMounts;
}

type SerializedEnvArg =
  | { type: "value"; value: string }
  | { type: "envVar"; name: string };

function serializeChildComponents(
  childComponents: [
    string,
    ImportedComponentDefinition,
    Record<string, any> | null,
    string | undefined,
  ][],
): {
  name: string;
  path: string;
  env: [string, SerializedEnvArg][] | null;
}[] {
  return childComponents.map(([name, definition, p]) => {
    // Note: httpPrefix (4th element) is used separately in buildHttpMounts()
    let env: [string, SerializedEnvArg][] | null = null;
    if (p !== null) {
      env = [];
      for (const [name, value] of Object.entries(p)) {
        if (value === undefined) {
          continue;
        }
        if (isEnvRef(value)) {
          env.push([name, { type: "envVar", name: value.__envVarRef }]);
        } else if (typeof value === "string") {
          env.push([name, { type: "value", value }]);
        } else {
          throw new Error(
            `Env var "${name}" must be a string or an env var reference. ` +
              `Received: ${typeof value}`,
          );
        }
      }
    }
    // we know that components carry this extra information
    const path = definition.componentDefinitionPath;
    if (!path)
      throw new Error(
        "no .componentPath for component definition " +
          JSON.stringify(definition, null, 2),
      );

    return {
      name: name!,
      path: path!,
      args: [],
      env,
    };
  });
}

function exportComponentForAnalysis(
  this: ComponentDefinition<any, any> & ComponentDefinitionData,
): ComponentDefinitionAnalysis {
  const envVars = Object.entries(this._env).map(
    ([name, validator]) =>
      [
        name,
        {
          type: "value" as const,
          value: JSON.stringify((validator as any).json),
          ...((validator as any).isOptional === "optional"
            ? { optional: true }
            : {}),
        },
      ] as [string, { type: "value"; value: string; optional?: boolean }],
  );
  const definitionType: ComponentDefinitionType = {
    type: "childComponent" as const,
    name: this._name,
    args: [],
  };
  const childComponents = serializeChildComponents(this._childComponents);
  const httpMounts = buildHttpMounts(this._childComponents);
  return {
    name: this._name,
    definitionType,
    childComponents: childComponents as any,
    httpMounts,
    exports: serializeExportTree(this._exportTree),
    ...(envVars.length > 0 ? { envVars } : {}),
  };
}

// This is what is actually contained in a ComponentDefinition.
type RuntimeComponentDefinition = Omit<
  ComponentDefinition<any, any>,
  "__exports" | "__env"
> &
  ComponentDefinitionData & {
    export: () => ComponentDefinitionAnalysis;
  };
type RuntimeAppDefinition = Omit<AppDefinition<any>, "__env"> &
  AppDefinitionData & {
    export: () => AppDefinitionAnalysis;
  };

/**
 * Define a component, a piece of a Convex deployment with namespaced resources.
 *
 * Optionally define typed environment variables that will be available via
 * the `env` export from `_generated/server` in all Convex functions within
 * this component. Values are passed by the parent via
 * `app.use(component, { env: { ... } })`.
 *
 * @param name Name must be alphanumeric plus underscores. Typically these are
 * lowercase with underscores like `"onboarding_flow_tracker"`.
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export function defineComponent<
  Exports extends ComponentExports = any,
  const Env extends EnvDefinition = {},
>(
  name: string,
  options?: {
    env?: Env;
  },
): ComponentDefinition<Exports, Env> {
  const envValidators: PropertyValidators = {};
  if (options?.env) {
    for (const [key, decl] of Object.entries(options.env)) {
      if (decl !== null && decl !== undefined && isValidator(decl)) {
        envValidators[key] = decl as any;
      } else {
        throw new Error(
          `Environment variable "${key}" must be defined with a validator (e.g. v.string()).`,
        );
      }
    }
  }

  const ret: RuntimeComponentDefinition = {
    _isRoot: false,
    _name: name,
    _env: envValidators,
    _childComponents: [],
    _exportTree: {},
    _onInitCallbacks: {},

    env: createEnvRefs(`component "${name}"`, options?.env),

    export: exportComponentForAnalysis,
    use,

    ...({} as { __exports: any; __env: any }),
  };
  return ret as any as ComponentDefinition<Exports, Env>;
}

/**
 * Attach components, reuseable pieces of a Convex deployment, to this Convex app.
 *
 * Optionally define typed environment variables that will be available via
 * the `env` export from `_generated/server` in all Convex functions.
 *
 * @example
 * ```typescript
 * import { defineApp } from "convex/server";
 * import { v } from "convex/values";
 *
 * const app = defineApp({
 *   env: {
 *     OPENAI_API_KEY: v.string(),
 *     DEBUG_MODE: v.optional(v.string()),
 *   },
 * });
 * export default app;
 * ```
 *
 * This is a feature of components, which are in beta.
 * This API is unstable and may change in subsequent releases.
 */
export function defineApp<Env extends EnvDefinition = EnvDefinition>(options?: {
  httpPrefix?: string;
  env?: Env;
}): AppDefinition<Env> {
  const httpPrefix = options?.httpPrefix;
  if (httpPrefix !== undefined && !httpPrefix.startsWith("/")) {
    throw new Error(
      `httpPrefix must start with "/". Received: "${httpPrefix}"`,
    );
  }
  const env = options?.env;
  if (env !== undefined) {
    for (const [name, validator] of Object.entries(env)) {
      if (!isValidator(validator)) {
        throw new Error(
          `Environment variable "${name}" must be defined with a validator (e.g. v.string()).`,
        );
      }
    }
  }
  const ret: RuntimeAppDefinition = {
    _isRoot: true,
    _childComponents: [],
    _exportTree: {},
    ...(httpPrefix !== undefined ? { _httpPrefix: httpPrefix } : {}),
    ...(env !== undefined ? { _env: env } : {}),

    env: createEnvRefs("this app", env),

    export: exportAppForAnalysis,
    use,
  };
  return ret as unknown as AppDefinition<Env>;
}

type AnyInterfaceType = {
  [key: string]: AnyInterfaceType;
} & AnyFunctionReference;
export type AnyComponentReference = Record<string, AnyInterfaceType>;

export type AnyChildComponents = Record<string, AnyComponentReference>;

/**
 * @internal
 */
export function currentSystemUdfInComponent(
  componentId: string,
): AnyComponentReference {
  return {
    [toReferencePath]: `_reference/currentSystemUdfInComponent/${componentId}`,
  };
}

function createChildComponents(
  root: string,
  pathParts: string[],
): AnyChildComponents {
  const handler: ProxyHandler<object> = {
    get(_, prop: string | symbol) {
      if (typeof prop === "string") {
        const newParts = [...pathParts, prop];
        return createChildComponents(root, newParts);
      } else if (prop === toReferencePath) {
        if (pathParts.length < 1) {
          const found = [root, ...pathParts].join(".");
          throw new Error(
            `API path is expected to be of the form \`${root}.childComponent.functionName\`. Found: \`${found}\``,
          );
        }
        return `_reference/childComponent/` + pathParts.join("/");
      } else {
        return undefined;
      }
    },
  };
  return new Proxy({}, handler);
}

export const componentsGeneric = () => createChildComponents("components", []);

export type AnyComponents = AnyChildComponents;
