import { InsufficientPermissionError } from "./errors";
import type { Result } from "./result";
import { ok, err } from "./result";
import type { CommandContext, QueryContext } from "./types";

type PermissionKey<M extends string, S extends string, C extends string> = `${M}:${S}:${C}`;

type NestedPermissionsRecord<M extends string, T extends Record<string, readonly string[]>> = {
  readonly [S in keyof T & string]: {
    readonly [C in T[S][number]]: PermissionKey<M, S, C>;
  };
};

interface PermissionResult<M extends string, T extends Record<string, readonly string[]>> {
  permissions: NestedPermissionsRecord<M, T>;
}

// Builds a typed `module:scope:command` permission map for a module.
export function definePermissions<
  const M extends string,
  const T extends Record<string, readonly string[]>,
>(moduleName: M, scopes: T): PermissionResult<M, T> {
  const permissions: Record<string, Record<string, string>> = {};

  for (const [scope, commands] of Object.entries(scopes)) {
    permissions[scope] = {};
    for (const cmd of commands) {
      permissions[scope][cmd] = `${moduleName}:${scope}:${cmd}`;
    }
  }

  return { permissions } as unknown as PermissionResult<M, T>;
}

type PermissionsMap = Record<string, Record<string, string>>;

// Collects `module:scope` keys from one or more permission maps for SDK config.
export function collectPermissionScopes(...permissionMaps: PermissionsMap[]): string[] {
  const scopes: string[] = [];

  for (const map of permissionMaps) {
    for (const [, commands] of Object.entries(map)) {
      const keys = Object.values(commands);
      if (keys.length === 0) continue;
      const scopeKey = keys[0].substring(0, keys[0].lastIndexOf(":"));
      scopes.push(scopeKey);
    }
  }

  return scopes.sort();
}

// Hierarchical prefix-match: an umbrella scope grants every descendant key.
function hasPermission(permissions: readonly string[], requiredKey: string): boolean {
  return permissions.some((p) => requiredKey === p || requiredKey.startsWith(p + ":"));
}

// Result-based permission gate for command/query composition.
export function requirePermission(
  ctx: CommandContext | QueryContext,
  key: string,
): Result<void, InstanceType<typeof InsufficientPermissionError>> {
  if (!ctx.permissions || !hasPermission(ctx.permissions, key)) {
    return err(new InsufficientPermissionError(ctx.actorId, key));
  }
  return ok(undefined);
}

// Throw-based variant of requirePermission for resolver entry points.
export function ensurePermission(ctx: CommandContext | QueryContext, key: string): void {
  const result = requirePermission(ctx, key);
  if (!result.ok) {
    throw result.error;
  }
}
