import type { ObjectSchema } from "@wrnexus/validation";
import type { AnyProcedures, InferInput, ProcedureDef, ServiceContract } from "./types.ts";

/** A service name lands in a URL path, so keep it unescaped-safe. */
const SAFE_SERVICE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
/** A procedure name is also a property the caller writes as client.doThing(). */
const SAFE_PROCEDURE = /^[a-z][a-zA-Z0-9]*$/;

/**
 * Fluent, IMMUTABLE builder: every method returns a new builder, so a shared
 * base can be branched without one branch mutating another.
 */
export class ProcedureBuilder<Input, Output> {
  private constructor(private readonly def: ProcedureDef<Input, Output>) {}

  static create(): ProcedureBuilder<void, void> {
    return new ProcedureBuilder<void, void>({});
  }

  input<S extends ObjectSchema<object>>(schema: S): ProcedureBuilder<InferInput<S>, Output> {
    return new ProcedureBuilder<InferInput<S>, Output>({
      ...this.def,
      input: schema,
    } as ProcedureDef<InferInput<S>, Output>);
  }

  output<T>(): ProcedureBuilder<Input, T> {
    return new ProcedureBuilder<Input, T>({ ...this.def } as ProcedureDef<Input, T>);
  }

  permission(id: string): ProcedureBuilder<Input, Output> {
    return new ProcedureBuilder<Input, Output>({ ...this.def, permission: id });
  }

  /** Mark safe to retry. Anything not marked is never retried. */
  idempotent(): ProcedureBuilder<Input, Output> {
    return new ProcedureBuilder<Input, Output>({ ...this.def, idempotent: true });
  }

  build(): ProcedureDef<Input, Output> {
    return Object.freeze({ ...this.def });
  }
}

export const procedure = ProcedureBuilder.create();

export function defineService<Procedures extends AnyProcedures>(def: {
  name: string;
  procedures: Procedures;
}): ServiceContract<Procedures> {
  if (!SAFE_SERVICE.test(def.name)) {
    throw new Error(
      `WRN-RPC-CONTRACT: service name ${JSON.stringify(def.name)} must be lowercase ` +
        `alphanumeric with single hyphens, e.g. "billing" or "billing-v2".`,
    );
  }
  for (const name of Object.keys(def.procedures)) {
    if (!SAFE_PROCEDURE.test(name)) {
      throw new Error(
        `WRN-RPC-CONTRACT: procedure name ${JSON.stringify(name)} on service ` +
          `'${def.name}' must be a lowercase-initial identifier, e.g. "createInvoice".`,
      );
    }
  }
  // Freeze each procedure, not just the map. AnyProcedures accepts any object
  // of ProcedureDef shape, so a hand-built def that never went through
  // procedure.build() would otherwise stay mutable and the "single source of
  // truth" guarantee would rest on every call site remembering the builder.
  const frozen: Record<string, ProcedureDef> = {};
  for (const [name, value] of Object.entries(def.procedures)) {
    frozen[name] = Object.freeze({ ...value });
  }
  return Object.freeze({
    name: def.name,
    procedures: Object.freeze(frozen) as Procedures,
  });
}
