import { RPC_ERROR_CODES, failure, success } from "./errors.ts";
import { importSubjectContext, type SubjectContext } from "./identity.ts";
import type {
  AnyProcedures,
  InferProcedureInput,
  InferProcedureOutput,
  ServiceContract,
  ServiceResult,
} from "./types.ts";

export interface HandlerContext {
  subject?: SubjectContext;
}

export type ServiceHandlers<Procedures extends AnyProcedures> = {
  [K in keyof Procedures]: (
    input: InferProcedureInput<Procedures[K]>,
    ctx: HandlerContext,
  ) => Promise<InferProcedureOutput<Procedures[K]>> | InferProcedureOutput<Procedures[K]>;
};

export interface ImplementOptions {
  selfApp: string;
  checkPermission?: (permission: string, subject?: SubjectContext) => Promise<boolean> | boolean;
}

export interface ServiceImplementation<Procedures extends AnyProcedures = AnyProcedures> {
  contract: ServiceContract<Procedures>;
  invoke(procedure: string, payload: unknown, identity?: string): Promise<ServiceResult>;
}

export function implement<Procedures extends AnyProcedures>(
  contract: ServiceContract<Procedures>,
  handlers: ServiceHandlers<Procedures>,
  options: ImplementOptions,
): ServiceImplementation<Procedures> {
  // A declared procedure with no own handler would otherwise only surface at
  // invoke time as RPC_UNKNOWN — a silent, permanent 404. Catch it now.
  for (const procedureName of Object.keys(contract.procedures)) {
    if (!Object.hasOwn(handlers, procedureName)) {
      throw new Error(
        `WRN-RPC-HANDLER: service "${contract.name}" declares procedure "${procedureName}" ` +
          `but implement() was not given a handler for it.`,
      );
    }
  }
  return {
    contract,
    async invoke(procedureName, payload, identity) {
      // Object.hasOwn, not plain indexing: "constructor", "toString" and every
      // other Object.prototype member otherwise resolve as truthy, and a
      // prototype member carries no `permission`, so the gate below is skipped
      // entirely and an unintended function runs with attacker-controlled input.
      const known =
        Object.hasOwn(contract.procedures, procedureName) && Object.hasOwn(handlers, procedureName);
      const definition = known ? contract.procedures[procedureName as keyof Procedures] : undefined;
      const handler = known ? handlers[procedureName as keyof Procedures] : undefined;
      if (!definition || !handler) return failure(RPC_ERROR_CODES.unknown, "Unknown procedure");

      let subject: SubjectContext | undefined;
      if (identity !== undefined) {
        try {
          subject = await importSubjectContext(identity, options.selfApp);
        } catch {
          return failure(RPC_ERROR_CODES.identity, "Invalid identity");
        }
      }

      if (definition.permission) {
        if (!options.checkPermission) return failure(RPC_ERROR_CODES.denied, "Forbidden");
        try {
          if (!(await options.checkPermission(definition.permission, subject))) {
            return failure(RPC_ERROR_CODES.denied, "Forbidden");
          }
        } catch {
          return failure(RPC_ERROR_CODES.denied, "Forbidden");
        }
      }

      let input: unknown = payload;
      if (definition.input) {
        // InputSchema is structural: any custom or wrapped schema may throw
        // instead of returning { ok: false }. A throw must not escape invoke()
        // with its raw message — that text can carry internals — so it is
        // caught the same way the permission check above is.
        let parsed: { ok: boolean; value?: unknown };
        try {
          parsed = definition.input.parse(payload as Record<string, unknown>);
        } catch (error) {
          console.error(`[wrnexus] RPC input schema threw for ${String(procedureName)}`, error);
          return failure(RPC_ERROR_CODES.invalid, "Invalid input");
        }
        if (!parsed.ok) return failure(RPC_ERROR_CODES.invalid, "Invalid input");
        input = parsed.value;
      }

      try {
        const value = await (handler as (value: unknown, ctx: HandlerContext) => unknown)(input, {
          subject,
        });
        return success(value);
      } catch {
        return failure(RPC_ERROR_CODES.handler, "Internal error");
      }
    },
  };
}
