import type { Context } from "./context.ts";

export interface SchemaLike<T> {
  parse(input: unknown): T;
}

export interface OutputSchemaLike<T> {
  readonly __output: T;
  parse(input: unknown): unknown;
}
export type InferEndpointSchema<TSchema> =
  TSchema extends OutputSchemaLike<infer TValue> ? TValue : never;

export interface EndpointErrorBody {
  code: string;
  message: string;
  details?: unknown;
}

export class EndpointError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string,
    readonly details?: unknown,
  ) {
    super(message);
    this.name = "EndpointError";
  }
}

export interface EndpointDefinition<I, O> {
  input?: SchemaLike<I> | OutputSchemaLike<I>;
  output?: SchemaLike<O> | OutputSchemaLike<O>;
  auth?: "optional" | "required";
  description?: string;
  tags?: string[];
  handler(input: I, ctx: Context): O | Promise<O>;
}

export interface DefinedEndpoint<I, O> {
  readonly definition: EndpointDefinition<I, O>;
  (ctx: Context, input?: unknown): Promise<Response>;
}

function json(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json; charset=utf-8" },
  });
}

function schemaValue<T>(schema: SchemaLike<T> | OutputSchemaLike<T>, input: unknown): T {
  const parsed = schema.parse(input);
  if (
    parsed &&
    typeof parsed === "object" &&
    "ok" in parsed &&
    "value" in parsed &&
    typeof (parsed as { ok?: unknown }).ok === "boolean"
  ) {
    const result = parsed as { ok: boolean; value: T; errors?: unknown };
    if (!result.ok)
      throw new EndpointError(
        400,
        "VALIDATION_ERROR",
        "Endpoint validation failed.",
        result.errors,
      );
    return result.value;
  }
  return parsed as T;
}

/** Define a validated, typed endpoint that can also drive SDK/OpenAPI generation. */
export function defineEndpoint<
  InputSchema extends OutputSchemaLike<unknown>,
  OutputSchema extends OutputSchemaLike<unknown>,
>(
  definition: Omit<
    EndpointDefinition<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>,
    "input" | "output"
  > & {
    input: InputSchema;
    output: OutputSchema;
  },
): DefinedEndpoint<InferEndpointSchema<InputSchema>, InferEndpointSchema<OutputSchema>>;
export function defineEndpoint<I = unknown, O = unknown>(
  definition: EndpointDefinition<I, O>,
): DefinedEndpoint<I, O>;
export function defineEndpoint(
  definition: EndpointDefinition<unknown, unknown>,
): DefinedEndpoint<unknown, unknown> {
  const endpoint = async (ctx: Context, rawInput?: unknown): Promise<Response> => {
    try {
      if (definition.auth === "required" && !ctx.user) {
        throw new EndpointError(401, "UNAUTHENTICATED", "Authentication is required.");
      }
      const input = definition.input ? schemaValue(definition.input, rawInput) : rawInput;
      const rawOutput = await definition.handler(input, ctx);
      const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput;
      return output instanceof Response ? output : json({ data: output });
    } catch (error) {
      if (error instanceof EndpointError) {
        return json(
          { error: { code: error.code, message: error.message, details: error.details } },
          error.status,
        );
      }
      return json(
        {
          error: {
            code: "INTERNAL_ERROR",
            message: "The endpoint failed unexpectedly.",
          } satisfies EndpointErrorBody,
        },
        500,
      );
    }
  };
  return Object.assign(endpoint, { definition });
}

export interface RpcClientOptions {
  baseUrl?: string;
  fetch?: typeof globalThis.fetch;
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
}

/** Create a tiny typed RPC caller for endpoints exposed by a WrNexus app. */
export function createRpcClient(options: RpcClientOptions = {}) {
  const request = options.fetch ?? globalThis.fetch;
  return async function call<I, O>(path: string, input: I): Promise<O> {
    const headers =
      typeof options.headers === "function" ? await options.headers() : (options.headers ?? {});
    const response = await request(new URL(path, options.baseUrl ?? globalThis.location?.origin), {
      method: "POST",
      headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) },
      body: JSON.stringify(input),
    });
    const body = (await response.json()) as { data?: O; error?: EndpointErrorBody };
    if (!response.ok || body.error) {
      throw new EndpointError(
        response.status,
        body.error?.code ?? "RPC_ERROR",
        body.error?.message ?? `RPC request failed with ${response.status}`,
        body.error?.details,
      );
    }
    return body.data as O;
  };
}
