import { appOrigin } from "@wrnexus/helpers";
import { RPC_ERROR_CODES, failure, isRetryableStatus } from "./errors.ts";
import { RPC_IDENTITY_HEADER } from "./identity.ts";
import type { CallOptions, RpcTarget, Transport } from "./transport.ts";
import type { ServiceResult } from "./types.ts";

export const RPC_PATH_PREFIX = "/__wrnexus/rpc";
export const RPC_INTERNAL_HEADER = "x-wrnexus-internal";

export function rpcPath(service: string, procedure: string): string {
  return `${RPC_PATH_PREFIX}/${service}/${procedure}`;
}

function parseOriginMap(value: string | undefined): Record<string, string> {
  if (!value) return {};
  try {
    const parsed: unknown = JSON.parse(value);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
    return parsed as Record<string, string>;
  } catch {
    return {};
  }
}

/**
 * Resolve the origin an RPC call to `app` should target.
 *
 * Prefer `WRNEXUS_INTERNAL_ORIGINS` (loopback origins the gateway hands each
 * child before spawning it) over `appOrigin`, which resolves the app's
 * PUBLIC origin. The public origin is the wrong target for RPC: the gateway
 * unconditionally 404s the reserved `/__wrnexus/rpc` prefix on anything that
 * arrives at a public origin — that block is the whole point, it is what
 * keeps inter-app calls off the public internet. Falling back to `appOrigin`
 * when no internal-origin map is present keeps single-app and test setups
 * (which only set `WRNEXUS_WORKSPACE_ORIGINS`) working.
 */
export function resolveAppOrigin(app: string): string {
  const internalOrigin = parseOriginMap(process.env.WRNEXUS_INTERNAL_ORIGINS)[app];
  if (internalOrigin) {
    try {
      return new URL(internalOrigin).origin;
    } catch {
      // Malformed internal-origin entry — fall through to the public origin.
    }
  }
  return appOrigin(app);
}

export interface HttpTransportOptions {
  resolveOrigin?: (app: string) => string;
  fetch?: typeof fetch;
}

function isServiceResult(value: unknown): value is ServiceResult {
  if (!value || typeof value !== "object" || !("ok" in value)) return false;
  const result = value as Record<string, unknown>;
  return (
    result.ok === true ||
    (result.ok === false &&
      typeof result.code === "string" &&
      typeof result.message === "string" &&
      typeof result.retryable === "boolean")
  );
}

export function httpTransport(options: HttpTransportOptions = {}): Transport {
  const resolveOrigin = options.resolveOrigin ?? resolveAppOrigin;
  const doFetch = options.fetch ?? fetch;
  return {
    async call(target: RpcTarget, payload: unknown, callOptions: CallOptions) {
      let response: Response;
      try {
        const headers: Record<string, string> = {
          "content-type": "application/json",
          [RPC_INTERNAL_HEADER]: "1",
        };
        if (callOptions.identity) headers[RPC_IDENTITY_HEADER] = callOptions.identity;
        response = await doFetch(
          `${resolveOrigin(target.app)}${rpcPath(target.service, target.procedure)}`,
          {
            method: "POST",
            headers,
            body: JSON.stringify(payload ?? {}),
            signal: callOptions.signal,
          },
        );
      } catch {
        return failure(RPC_ERROR_CODES.transport, "Service unreachable");
      }
      if (!response.ok) {
        return {
          ok: false,
          code: RPC_ERROR_CODES.transport,
          message: `Service returned ${response.status}`,
          retryable: isRetryableStatus(response.status),
        };
      }
      try {
        const result: unknown = await response.json();
        return isServiceResult(result)
          ? result
          : failure(RPC_ERROR_CODES.malformed, "Malformed service response");
      } catch {
        return failure(RPC_ERROR_CODES.malformed, "Malformed service response");
      }
    },
  };
}
