// Device pairing access helpers evaluate pairing scopes and role permissions.
import { normalizeUniqueSingleOrTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
import { normalizeDeviceAuthScopes } from "./device-auth.js";

export type DevicePairingAccessSummary = {
  /** Normalized role ids requested or approved for a device. */
  roles: string[];
  /** Normalized scope ids, including implied operator scopes. */
  scopes: string[];
};

/** Approval classification shown when a pending pairing differs from existing grants. */
export type PendingDeviceApprovalKind =
  | "new-pairing"
  | "role-upgrade"
  | "scope-upgrade"
  | "re-approval";

export type PendingDeviceApprovalState = {
  kind: PendingDeviceApprovalKind;
  /** Access requested by the pending pairing attempt. */
  requested: DevicePairingAccessSummary;
  /** Existing active access, or null for a new pairing. */
  approved: DevicePairingAccessSummary | null;
};

type PendingLike = {
  role?: string;
  roles?: string[];
  scopes?: string[];
};

type PairedLike = {
  role?: string;
  roles?: string[];
  scopes?: string[];
  tokens?:
    | Array<{
        role?: string;
        revokedAtMs?: number | null;
      }>
    | Record<
        string,
        {
          role?: string;
          revokedAtMs?: number | null;
        }
      >;
};

function normalizeRoleList(...items: Array<string | string[] | undefined>): string[] {
  const roles = new Set<string>();
  for (const item of items) {
    // On-disk pairing records are blind-cast, so roles/role may be non-strings; the shared
    // normalizer drops them instead of crashing on .trim() (matches the scopes path + mergeRoles).
    for (const role of normalizeUniqueSingleOrTrimmedStringList(item)) {
      roles.add(role);
    }
  }
  return [...roles].toSorted();
}

function includesAll(allowed: readonly string[], requested: readonly string[]): boolean {
  const allowedSet = new Set(allowed);
  return requested.every((value) => allowedSet.has(value));
}

/** Normalizes requested roles/scopes from pending pairing records, including legacy singular role. */
export function summarizePendingDeviceAccess(request: PendingLike): DevicePairingAccessSummary {
  return {
    roles: normalizeRoleList(request.roles, request.role),
    scopes: normalizeDeviceAuthScopes(request.scopes),
  };
}

/** Summarizes currently approved device access, excluding roles whose tokens are revoked. */
export function summarizeApprovedDeviceAccess(device: PairedLike): DevicePairingAccessSummary {
  const approvedRoles = normalizeRoleList(device.roles, device.role);
  const tokenList = Array.isArray(device.tokens)
    ? device.tokens
    : device.tokens
      ? Object.values(device.tokens)
      : undefined;
  const activeTokenRoles =
    tokenList === undefined
      ? approvedRoles
      : normalizeRoleList(
          tokenList.filter((token) => !token.revokedAtMs).flatMap((token) => token.role ?? []),
        ).filter((role) => approvedRoles.includes(role));
  return {
    roles: activeTokenRoles,
    scopes: normalizeDeviceAuthScopes(device.scopes),
  };
}

/** Classifies a pending pairing request as new pairing, role upgrade, scope upgrade, or re-approval. */
export function resolvePendingDeviceApprovalState(
  request: PendingLike,
  paired?: PairedLike,
): PendingDeviceApprovalState {
  const requested = summarizePendingDeviceAccess(request);
  const approved = paired ? summarizeApprovedDeviceAccess(paired) : null;
  if (!approved) {
    return { kind: "new-pairing", requested, approved: null };
  }
  if (!includesAll(approved.roles, requested.roles)) {
    return { kind: "role-upgrade", requested, approved };
  }
  if (!includesAll(approved.scopes, requested.scopes)) {
    return { kind: "scope-upgrade", requested, approved };
  }
  return { kind: "re-approval", requested, approved };
}
