import type {
  AccessRolePayload,
  PermissionStatePayload,
  RoleAssignmentPayload,
  ServerPayload,
  ServerPermissionKeyPayload
} from '../cqrs/types';
import { normalizeServerRoleAssignments, normalizeServerRoles } from '../cqrs/relations';

const SYSTEM_ROLE_IDS = {
  everyone: 'system-everyone'
} as const;

interface ServerIdentity {
  userId: string;
  oderId?: string;
}

function getServerRoles(server: Pick<ServerPayload, 'roles'>): AccessRolePayload[] {
  return normalizeServerRoles(server.roles);
}

function getServerAssignments(server: Pick<ServerPayload, 'roleAssignments' | 'roles'>): RoleAssignmentPayload[] {
  return normalizeServerRoleAssignments(server.roleAssignments, getServerRoles(server));
}

function matchesIdentity(identity: ServerIdentity, assignment: RoleAssignmentPayload): boolean {
  return assignment.userId === identity.userId
    || assignment.oderId === identity.userId
    || (!!identity.oderId && (assignment.userId === identity.oderId || assignment.oderId === identity.oderId));
}

function resolveAssignedRoleIds(server: Pick<ServerPayload, 'roleAssignments' | 'roles'>, identity: ServerIdentity): string[] {
  const assignment = getServerAssignments(server).find((candidateAssignment) => matchesIdentity(identity, candidateAssignment));

  return assignment?.roleIds ?? [];
}

function compareRolePosition(firstRole: AccessRolePayload, secondRole: AccessRolePayload): number {
  if (firstRole.position !== secondRole.position) {
    return firstRole.position - secondRole.position;
  }

  return firstRole.name.localeCompare(secondRole.name, undefined, { sensitivity: 'base' });
}

function resolveRolePermissionState(
  roles: readonly AccessRolePayload[],
  assignedRoleIds: readonly string[],
  permission: ServerPermissionKeyPayload
): PermissionStatePayload {
  const roleLookup = new Map(roles.map((role) => [role.id, role]));
  const effectiveRoles = [roleLookup.get(SYSTEM_ROLE_IDS.everyone), ...assignedRoleIds.map((roleId) => roleLookup.get(roleId))]
    .filter((role): role is AccessRolePayload => !!role)
    .sort(compareRolePosition);

  let state: PermissionStatePayload = 'inherit';

  for (const role of effectiveRoles) {
    const nextState = role.permissions?.[permission] ?? 'inherit';

    if (nextState !== 'inherit') {
      state = nextState;
    }
  }

  return state;
}

function resolveHighestRole(
  server: Pick<ServerPayload, 'roleAssignments' | 'roles'>,
  identity: ServerIdentity
): AccessRolePayload | null {
  const roles = getServerRoles(server);
  const assignedRoleIds = resolveAssignedRoleIds(server, identity);
  const roleLookup = new Map(roles.map((role) => [role.id, role]));
  const assignedRoles = assignedRoleIds
    .map((roleId) => roleLookup.get(roleId))
    .filter((role): role is AccessRolePayload => !!role)
    .sort((firstRole, secondRole) => compareRolePosition(secondRole, firstRole));

  return assignedRoles[0] ?? roleLookup.get(SYSTEM_ROLE_IDS.everyone) ?? null;
}

export function isServerOwner(server: Pick<ServerPayload, 'ownerId'>, actorUserId: string): boolean {
  return server.ownerId === actorUserId;
}

export function resolveServerPermission(
  server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
  actorUserId: string,
  permission: ServerPermissionKeyPayload,
  actorOderId?: string
): boolean {
  if (isServerOwner(server, actorUserId)) {
    return true;
  }

  const roles = getServerRoles(server);
  const assignedRoleIds = resolveAssignedRoleIds(server, {
    userId: actorUserId,
    oderId: actorOderId
  });

  return resolveRolePermissionState(roles, assignedRoleIds, permission) === 'allow';
}

export function canManageServerUpdate(
  server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
  actorUserId: string,
  updates: Record<string, unknown>,
  actorOderId?: string
): boolean {
  if (isServerOwner(server, actorUserId)) {
    return true;
  }

  if (typeof updates['ownerId'] === 'string' || typeof updates['ownerPublicKey'] === 'string') {
    return false;
  }

  const requiredPermissions = new Set<ServerPermissionKeyPayload>();

  if (
    Array.isArray(updates['roles'])
    || Array.isArray(updates['roleAssignments'])
    || Array.isArray(updates['channelPermissions'])
  ) {
    requiredPermissions.add('manageRoles');
  }

  if (Array.isArray(updates['channels'])) {
    requiredPermissions.add('manageChannels');
  }

  if (typeof updates['icon'] === 'string') {
    requiredPermissions.add('manageIcon');
  }

  if (
    typeof updates['name'] === 'string'
    || typeof updates['description'] === 'string'
    || typeof updates['isPrivate'] === 'boolean'
    || typeof updates['maxUsers'] === 'number'
    || typeof updates['password'] === 'string'
    || typeof updates['passwordHash'] === 'string'
    || typeof updates['slowModeInterval'] === 'number'
  ) {
    requiredPermissions.add('manageServer');
  }

  return Array.from(requiredPermissions).every((permission) =>
    resolveServerPermission(server, actorUserId, permission, actorOderId)
  );
}

export function canModerateServerMember(
  server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
  actorUserId: string,
  targetUserId: string,
  permission: 'kickMembers' | 'banMembers' | 'manageBans',
  actorOderId?: string,
  targetOderId?: string
): boolean {
  if (!actorUserId || !targetUserId || actorUserId === targetUserId) {
    return false;
  }

  if (isServerOwner(server, targetUserId) && !isServerOwner(server, actorUserId)) {
    return false;
  }

  if (isServerOwner(server, actorUserId)) {
    return true;
  }

  if (!resolveServerPermission(server, actorUserId, permission, actorOderId)) {
    return false;
  }

  const actorRole = resolveHighestRole(server, {
    userId: actorUserId,
    oderId: actorOderId
  });
  const targetRole = resolveHighestRole(server, {
    userId: targetUserId,
    oderId: targetOderId
  });

  return (actorRole?.position ?? 0) > (targetRole?.position ?? 0);
}
