/**
 * Model Permissions
 *
 * Utilities for checking model access permissions.
 * Model permissions are stored in the role permission JSON under the "models" key
 * as composite "keyId:modelId" strings (e.g. "aik_abc123:claude-opus-4-5").
 */

import type { Kysely } from "kysely";
import { hasAdminRole } from "@decocms/shared/auth/roles";
import type { Database, Permission } from "@/storage/types";

/**
 * Extract the "models" array from a permission object.
 * Returns undefined if the "models" key is absent (meaning all models are allowed).
 * Returns the array as-is if present (even if empty — empty means "no models allowed").
 */
export function extractModelPermissions(
  permission: Permission | undefined | null,
): string[] | undefined {
  if (!permission) return undefined;
  if (!("models" in permission)) return undefined;
  return permission["models"] ?? undefined;
}

/**
 * Check if a specific API key is accessible at all (i.e. the user has at least
 * one allowed model entry for it). Used to filter AI provider keys.
 *
 * @param models - The "models" array from the permission object, or undefined for "all allowed"
 * @param keyId - The AI provider key ID (e.g. "aik_abc123")
 * @returns true if the key has any allowed models
 */
export function checkKeyPermission(
  models: string[] | undefined,
  keyId: string,
): boolean {
  if (!models) return true;
  if (models.includes("*:*")) return true;
  return models.some((entry) => entry.startsWith(`${keyId}:`));
}

/**
 * Check if a specific model from a specific key is allowed.
 *
 * @param models - The "models" array from the permission object, or undefined for "all allowed"
 * @param keyId - The AI provider key ID (e.g. "aik_abc123")
 * @param modelId - The model ID to check
 * @returns true if the model is allowed
 */
export function checkModelPermission(
  models: string[] | undefined,
  keyId: string,
  modelId: string,
): boolean {
  // No models key = all models allowed (backward compat)
  if (!models) return true;

  return (
    models.includes("*:*") ||
    models.includes(`${keyId}:*`) ||
    models.includes(`${keyId}:${modelId}`)
  );
}

/**
 * Strip per-tool tier slots (image, deepResearch) when the caller's role
 * does not have permission to use the underlying (keyId, modelId) pair.
 *
 * The image and web_research tier slots are configured at the org level and
 * may reference a credential the current user's role cannot access. Silently
 * dropping the slot disables the corresponding built-in tool for this user
 * rather than failing the whole request — failing would block every chat
 * turn for a restricted role just because the admin paired the web_search
 * tier with a key that role can't touch.
 *
 * Chat (thinking) is intentionally NOT filtered here — chat must error out
 * loudly via `checkModelPermission` at the caller. Only optional tool tiers
 * are silently downgraded.
 */
export function filterToolTiersByPermission<
  M extends {
    image?: { credentialId: string; id: string } | undefined;
    webSearch?: { credentialId: string; id: string } | undefined;
    deepResearch?: { credentialId: string; id: string } | undefined;
  },
>(allowedModels: string[] | undefined, models: M): M {
  if (allowedModels === undefined) return models;
  let next = models;
  if (
    next.image &&
    !checkModelPermission(allowedModels, next.image.credentialId, next.image.id)
  ) {
    const { image: _image, ...rest } = next;
    next = rest as M;
  }
  if (
    next.webSearch &&
    !checkModelPermission(
      allowedModels,
      next.webSearch.credentialId,
      next.webSearch.id,
    )
  ) {
    const { webSearch: _webSearch, ...rest } = next;
    next = rest as M;
  }
  if (
    next.deepResearch &&
    !checkModelPermission(
      allowedModels,
      next.deepResearch.credentialId,
      next.deepResearch.id,
    )
  ) {
    const { deepResearch: _deepResearch, ...rest } = next;
    next = rest as M;
  }
  return next;
}

/**
 * Parse the models array into a key-scoped map.
 * Used by the allowed-models API endpoint to return structured data to the client.
 *
 * @returns { allowAll: boolean, models: Record<keyId, modelId[]> }
 */
export function parseModelsToMap(models: string[] | undefined): {
  allowAll: boolean;
  models: Record<string, string[]>;
} {
  if (!models) {
    return { allowAll: true, models: {} };
  }

  if (models.includes("*:*")) {
    return { allowAll: true, models: {} };
  }

  const result: Record<string, string[]> = {};
  for (const entry of models) {
    const colonIdx = entry.indexOf(":");
    if (colonIdx === -1) continue;
    const keyId = entry.slice(0, colonIdx);
    const modelId = entry.slice(colonIdx + 1);
    if (!result[keyId]) {
      result[keyId] = [];
    }
    result[keyId].push(modelId);
  }

  return { allowAll: false, models: result };
}

/**
 * Merge the per-role "models" arrays of a multi-role member into one effective
 * allow-list, matching Better Auth's OR-over-roles semantics (permission.mjs
 * splits `member.role` on "," and grants access if ANY role allows it): a
 * model is allowed if any single role allows it. If any role has no "models"
 * key (that role permits all models), the merged result is "all allowed".
 */
export function mergeModelPermissions(
  perRoleModels: Array<string[] | undefined>,
): string[] | undefined {
  if (perRoleModels.some((models) => models === undefined)) {
    return undefined;
  }
  const merged = new Set<string>();
  for (const models of perRoleModels) {
    for (const entry of models ?? []) merged.add(entry);
  }
  return [...merged];
}

/**
 * Fetch model permissions for a user's role.
 * Returns undefined for admin/owner roles (they bypass all checks).
 * Returns the "models" array for custom roles.
 * Returns undefined if no "models" key exists (all allowed).
 *
 * `role` may be Better Auth's comma-joined multi-role string (e.g.
 * "editor,billing-manager" — ORGANIZATION_MEMBER_UPDATE_ROLE takes an array of
 * roles). Each individual role is looked up and the results are OR-merged via
 * `mergeModelPermissions` — a single-string `where("role", "=", role)` lookup
 * would never match a stored per-role record for a multi-role string, silently
 * falling back to "all models allowed" instead of honoring each role's limits.
 */
export async function fetchModelPermissions(
  db: Kysely<Database>,
  organizationId: string,
  role: string | undefined,
): Promise<string[] | undefined> {
  // No role or admin/owner = all models allowed. `hasAdminRole` recognizes a
  // comma-joined multi-role owner/admin (e.g. "admin,billing-manager"), which
  // a plain ADMIN_ROLES.includes(role) exact match would miss.
  if (!role || hasAdminRole(role)) {
    return undefined;
  }

  const roles = role.split(",");

  // Query custom role permissions from the organizationRole table
  const roleRecords = await db
    .selectFrom("organizationRole")
    .select(["role", "permission"])
    .where("organizationId", "=", organizationId)
    .where("role", "in", roles)
    .execute();

  const permissionByRole = new Map(
    roleRecords.map((record) => [record.role, record.permission]),
  );

  const perRoleModels = roles.map((r) => {
    const permissionJson = permissionByRole.get(r);
    if (!permissionJson) return undefined;

    try {
      const permission = JSON.parse(permissionJson) as Permission;
      return extractModelPermissions(permission);
    } catch {
      console.error(
        `[model-permissions] Failed to parse permissions for role: ${r}`,
      );
      // Fail-closed: corrupted permission data should deny access, not grant it.
      // Returning undefined would mean "all models allowed" per the data model.
      return [];
    }
  });

  return mergeModelPermissions(perRoleModels);
}
