/**
 * Shared RPC-request construction.
 *
 * Both the channel-based `FileClient` (`createFileClient`) and the
 * HTTP-based `FilesHttpClient` build identical `RpcRequest` envelopes
 * — they differ only in *transport* (channel.send → ratchet via
 * Shade.send/onMessage vs HTTP POST → ratchet via single
 * request-response). This module is the single source of truth for
 * the wire shape so the two clients can never drift.
 */
import type { ZodTypeAny } from 'zod';
import {
  KIND_DELETE_V1,
  KIND_GET_THUMBNAIL_V1,
  KIND_LIST_V1,
  KIND_MKDIR_V1,
  KIND_MOVE_V1,
  KIND_READ_V1,
  KIND_STAT_V1,
  KIND_WRITE_V1,
  MUTATION_OPS,
  type StandardOp,
} from './kinds.js';
import { generateIdempotencyKey, generateRequestId } from './correlate.js';
import { canonicalRpcBytes, hashArgs } from './canonical.js';
import type { RpcRequest } from '../schemas/envelope.js';

export const KIND_BY_OP: Record<StandardOp, string> = {
  list: KIND_LIST_V1,
  stat: KIND_STAT_V1,
  mkdir: KIND_MKDIR_V1,
  delete: KIND_DELETE_V1,
  move: KIND_MOVE_V1,
  read: KIND_READ_V1,
  write: KIND_WRITE_V1,
  getThumbnail: KIND_GET_THUMBNAIL_V1,
};

export type SignRequest = (canonicalBytes: Uint8Array) => Promise<string> | string;

export interface BuildRpcRequestOptions {
  /** Address that this RPC call originates from. */
  senderAddress: string;
  /** RPC kind — `KIND_*_V1` constants for standard ops, `KIND_CUSTOM_V1` otherwise. */
  kind: string;
  /** Op classifier so mutations get an auto-generated idempotency key. */
  op: StandardOp | 'custom';
  /** Validated args object. The caller is responsible for `Zod.parse(args)`. */
  args: unknown;
  /** Caller-supplied idempotency key. Mutations get one auto-generated. */
  idempotencyKey?: string;
  /** Optional Ed25519-style signer over the canonical bytes. */
  signRequest?: SignRequest;
}

/**
 * Build a single `RpcRequest` envelope. Generates `id`, `signedAt`,
 * idempotency key (mutations only), and the canonical signature.
 */
export async function buildRpcRequest(
  options: BuildRpcRequestOptions,
): Promise<RpcRequest> {
  const { senderAddress, kind, op, args, signRequest } = options;
  const requestId = generateRequestId();
  const isMutation = MUTATION_OPS.has(op);
  const idempotencyKey =
    options.idempotencyKey ?? (isMutation ? generateIdempotencyKey() : undefined);
  const signedAt = Date.now();

  let sig = 'unsigned';
  if (signRequest !== undefined) {
    const canonical = canonicalRpcBytes({
      address: senderAddress,
      signedAt,
      kind,
      id: requestId,
      argsHash: hashArgs(args),
    });
    sig = await signRequest(canonical);
  }

  const env: RpcRequest = {
    kind,
    id: requestId,
    args,
    ...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
    sig,
    signedAt,
  };
  return env;
}

/**
 * Helper for the typed standard ops: validates args via the supplied Zod
 * schema, calls {@link buildRpcRequest}, and forwards the result.
 *
 * The HTTP and channel clients both call this from each op-method to
 * guarantee identical wire-shape. Custom ops use {@link buildRpcRequest}
 * directly because they ship `KIND_CUSTOM_V1` plus runtime-validated args.
 */
export async function buildStandardRpcRequest<TArgs>(
  schema: { parse(input: unknown): TArgs },
  rawArgs: unknown,
  options: Omit<BuildRpcRequestOptions, 'kind' | 'op' | 'args'> & {
    op: StandardOp;
  },
): Promise<{ request: RpcRequest; args: TArgs }> {
  const args = schema.parse(rawArgs) as TArgs;
  const kind = KIND_BY_OP[options.op];
  const request = await buildRpcRequest({
    ...options,
    kind,
    op: options.op,
    args,
  });
  return { request, args };
}

/**
 * Validate a returned RpcResponse `result` against the supplied schema.
 * Centralised so the HTTP and channel clients fail-loudly with the same
 * error type when a server returns an off-spec response.
 */
export function parseResult<T>(schema: ZodTypeAny, raw: unknown): T {
  return schema.parse(raw) as T;
}
