import { RoomMember, User } from '../../shared-kernel';

/** Remove members that have not been seen for roughly two months. */
export const ROOM_MEMBER_STALE_MS = 1000 * 60 * 60 * 24 * 60;

function fallbackDisplayName(member: Partial<RoomMember>): string {
  return member.displayName || member.username || member.oderId || member.id || 'User';
}

function fallbackUsername(member: Partial<RoomMember>): string {
  const base = fallbackDisplayName(member)
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '_');

  return base || member.oderId || member.id || 'user';
}

function normalizeRoleIds(roleIds: readonly string[] | undefined): string[] | undefined {
  if (!Array.isArray(roleIds)) {
    return undefined;
  }

  const normalized = Array.from(new Set(
    roleIds
      .filter((roleId): roleId is string => typeof roleId === 'string' && roleId.trim().length > 0)
      .map((roleId) => roleId.trim())
  ));

  return normalized.length > 0 ? normalized : undefined;
}

function normalizeAvatarUpdatedAt(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) && value > 0
    ? value
    : undefined;
}

function normalizeProfileUpdatedAt(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) && value > 0
    ? value
    : undefined;
}

function normalizeDescription(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim();

  return normalized || undefined;
}

function hasOwnProperty(object: object, key: string): boolean {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function mergeProfileFields(
  existingMember: Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'>,
  incomingMember: Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'>,
  preferIncomingFallback: boolean
): Pick<RoomMember, 'displayName' | 'description' | 'profileUpdatedAt'> {
  const existingUpdatedAt = existingMember.profileUpdatedAt ?? 0;
  const incomingUpdatedAt = incomingMember.profileUpdatedAt ?? 0;
  const preferIncoming = incomingUpdatedAt === existingUpdatedAt
    ? preferIncomingFallback
    : incomingUpdatedAt > existingUpdatedAt;
  const incomingHasDescription = hasOwnProperty(incomingMember, 'description');
  const incomingDescription = normalizeDescription(incomingMember.description);
  const existingDescription = normalizeDescription(existingMember.description);

  return {
    displayName: preferIncoming
      ? (incomingMember.displayName || existingMember.displayName)
      : (existingMember.displayName || incomingMember.displayName),
    description: preferIncoming
      ? (incomingHasDescription ? incomingDescription : existingDescription)
      : existingDescription,
    profileUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
  };
}

function mergeAvatarFields(
  existingMember: Pick<RoomMember, 'avatarUrl' | 'avatarHash' | 'avatarMime' | 'avatarUpdatedAt'>,
  incomingMember: Pick<RoomMember, 'avatarUrl' | 'avatarHash' | 'avatarMime' | 'avatarUpdatedAt'>,
  preferIncomingFallback: boolean
): Pick<RoomMember, 'avatarUrl' | 'avatarHash' | 'avatarMime' | 'avatarUpdatedAt'> {
  const existingUpdatedAt = existingMember.avatarUpdatedAt ?? 0;
  const incomingUpdatedAt = incomingMember.avatarUpdatedAt ?? 0;
  const preferIncoming = incomingUpdatedAt === existingUpdatedAt
    ? preferIncomingFallback
    : incomingUpdatedAt > existingUpdatedAt;

  return {
    avatarUrl: preferIncoming
      ? (incomingMember.avatarUrl || existingMember.avatarUrl)
      : (existingMember.avatarUrl || incomingMember.avatarUrl),
    avatarHash: preferIncoming
      ? (incomingMember.avatarHash || existingMember.avatarHash)
      : (existingMember.avatarHash || incomingMember.avatarHash),
    avatarMime: preferIncoming
      ? (incomingMember.avatarMime || existingMember.avatarMime)
      : (existingMember.avatarMime || incomingMember.avatarMime),
    avatarUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
  };
}

function normalizeMember(member: RoomMember, now = Date.now()): RoomMember {
  const key = getRoomMemberKey(member);
  const lastSeenAt =
    typeof member.lastSeenAt === 'number' && Number.isFinite(member.lastSeenAt)
      ? member.lastSeenAt
      : typeof member.joinedAt === 'number' && Number.isFinite(member.joinedAt)
        ? member.joinedAt
        : now;
  const joinedAt =
    typeof member.joinedAt === 'number' && Number.isFinite(member.joinedAt)
      ? member.joinedAt
      : lastSeenAt;
  const nextMember: RoomMember = {
    id: member.id || key,
    oderId: member.oderId || undefined,
    username: member.username || fallbackUsername(member),
    displayName: fallbackDisplayName(member),
    profileUpdatedAt: normalizeProfileUpdatedAt(member.profileUpdatedAt),
    avatarUrl: member.avatarUrl || undefined,
    avatarHash: member.avatarHash || undefined,
    avatarMime: member.avatarMime || undefined,
    avatarUpdatedAt: normalizeAvatarUpdatedAt(member.avatarUpdatedAt),
    role: member.role || 'member',
    roleIds: normalizeRoleIds(member.roleIds),
    joinedAt,
    lastSeenAt
  };

  if (hasOwnProperty(member, 'description')) {
    nextMember.description = normalizeDescription(member.description);
  }

  return nextMember;
}

function compareMembers(firstMember: RoomMember, secondMember: RoomMember): number {
  const displayNameCompare = firstMember.displayName.localeCompare(secondMember.displayName, undefined, { sensitivity: 'base' });

  if (displayNameCompare !== 0)
    return displayNameCompare;

  return getRoomMemberKey(firstMember).localeCompare(getRoomMemberKey(secondMember));
}

function mergeRole(
  existingRole: RoomMember['role'],
  incomingRole: RoomMember['role'],
  preferIncoming: boolean
): RoomMember['role'] {
  if (existingRole === incomingRole)
    return existingRole;

  if (incomingRole === 'member' && existingRole !== 'member')
    return existingRole;

  if (existingRole === 'member' && incomingRole !== 'member')
    return incomingRole;

  return preferIncoming ? incomingRole : existingRole;
}

function mergeMembers(
  existingMember: RoomMember | undefined,
  incomingMember: RoomMember,
  now = Date.now()
): RoomMember {
  const normalizedIncoming = normalizeMember(incomingMember, now);

  if (!existingMember)
    return normalizedIncoming;

  const normalizedExisting = normalizeMember(existingMember, now);
  const preferIncoming = normalizedIncoming.lastSeenAt >= normalizedExisting.lastSeenAt;
  const profileFields = mergeProfileFields(normalizedExisting, normalizedIncoming, preferIncoming);
  const avatarFields = mergeAvatarFields(normalizedExisting, normalizedIncoming, preferIncoming);

  return {
    id: normalizedExisting.id || normalizedIncoming.id,
    oderId: normalizedIncoming.oderId || normalizedExisting.oderId,
    username: preferIncoming
      ? (normalizedIncoming.username || normalizedExisting.username)
      : (normalizedExisting.username || normalizedIncoming.username),
    ...profileFields,
    ...avatarFields,
    role: mergeRole(normalizedExisting.role, normalizedIncoming.role, preferIncoming),
    roleIds: preferIncoming
      ? (normalizedIncoming.roleIds || normalizedExisting.roleIds)
      : (normalizedExisting.roleIds || normalizedIncoming.roleIds),
    joinedAt: Math.min(normalizedExisting.joinedAt, normalizedIncoming.joinedAt),
    lastSeenAt: Math.max(normalizedExisting.lastSeenAt, normalizedIncoming.lastSeenAt)
  };
}

/** Stable member key, preferring `oderId` when available. */
export function getRoomMemberKey(member: Pick<RoomMember, 'id' | 'oderId'>): string {
  return member.oderId || member.id || '';
}

/** Find a room member by either their local ID or their `oderId`. */
export function findRoomMember(
  members: RoomMember[] = [],
  identifier?: string
): RoomMember | undefined {
  if (!identifier)
    return undefined;

  return members.find((member) => member.id === identifier || member.oderId === identifier);
}

/** Convert a live `User` into a persisted room-member record. */
export function roomMemberFromUser(
  user: User,
  seenAt = Date.now(),
  roleOverride?: RoomMember['role']
): RoomMember {
  return normalizeMember(
    {
      id: user.id || user.oderId,
      oderId: user.oderId || undefined,
      username: user.username || '',
      displayName: user.displayName || user.username || 'User',
      description: user.description,
      profileUpdatedAt: user.profileUpdatedAt,
      avatarUrl: user.avatarUrl,
      avatarHash: user.avatarHash,
      avatarMime: user.avatarMime,
      avatarUpdatedAt: user.avatarUpdatedAt,
      role: roleOverride || user.role || 'member',
      joinedAt: user.joinedAt || seenAt,
      lastSeenAt: seenAt
    },
    seenAt
  );
}

/** Deduplicate, sanitize, sort, and prune stale room members. */
export function pruneRoomMembers(
  members: RoomMember[] = [],
  now = Date.now()
): RoomMember[] {
  const cutoff = now - ROOM_MEMBER_STALE_MS;
  const deduplicatedMembers = new Map<string, RoomMember>();

  for (const member of members) {
    const key = getRoomMemberKey(member);

    if (!key)
      continue;

    const normalizedMember = normalizeMember(member, now);

    if (normalizedMember.lastSeenAt < cutoff)
      continue;

    deduplicatedMembers.set(
      key,
      mergeMembers(deduplicatedMembers.get(key), normalizedMember, now)
    );
  }

  return Array.from(deduplicatedMembers.values()).sort(compareMembers);
}

/** Upsert a member into a room roster while preserving the best known data. */
export function upsertRoomMember(
  members: RoomMember[] = [],
  member: RoomMember,
  now = Date.now()
): RoomMember[] {
  const key = getRoomMemberKey(member);
  const nextMembers = pruneRoomMembers(members, now);

  if (!key)
    return nextMembers;

  const memberIndex = nextMembers.findIndex((entry) => getRoomMemberKey(entry) === key);
  const mergedMember = mergeMembers(memberIndex >= 0 ? nextMembers[memberIndex] : undefined, member, now);

  if (memberIndex >= 0) {
    const updatedMembers = [...nextMembers];

    updatedMembers[memberIndex] = mergedMember;
    return pruneRoomMembers(updatedMembers, now);
  }

  return pruneRoomMembers([...nextMembers, mergedMember], now);
}

/** Merge a remote roster into the local roster. */
export function mergeRoomMembers(
  localMembers: RoomMember[] = [],
  incomingMembers: RoomMember[] = [],
  now = Date.now()
): RoomMember[] {
  let mergedMembers = pruneRoomMembers(localMembers, now);

  for (const incomingMember of incomingMembers) {
    mergedMembers = upsertRoomMember(mergedMembers, incomingMember, now);
  }

  return pruneRoomMembers(mergedMembers, now);
}

/** Update the last-seen timestamp of a known room member. */
export function touchRoomMemberLastSeen(
  members: RoomMember[] = [],
  identifier: string,
  seenAt = Date.now()
): RoomMember[] {
  const nextMembers = pruneRoomMembers(members, seenAt);
  const memberIndex = nextMembers.findIndex((member) => member.id === identifier || member.oderId === identifier);

  if (memberIndex < 0)
    return nextMembers;

  const updatedMembers = [...nextMembers];

  updatedMembers[memberIndex] = normalizeMember(
    {
      ...updatedMembers[memberIndex],
      lastSeenAt: Math.max(updatedMembers[memberIndex].lastSeenAt, seenAt)
    },
    seenAt
  );

  return pruneRoomMembers(updatedMembers, seenAt);
}

/** Remove a member from a room roster by either ID flavor. */
export function removeRoomMember(
  members: RoomMember[] = [],
  ...identifiers: (string | undefined)[]
): RoomMember[] {
  const ids = new Set(identifiers.filter((identifier): identifier is string => !!identifier));

  if (ids.size === 0)
    return pruneRoomMembers(members);

  return pruneRoomMembers(members).filter(
    (member) => !ids.has(member.id) && !ids.has(member.oderId || '')
  );
}

/** Reassign ownership within a room roster, optionally leaving the room ownerless. */
export function transferRoomOwnership(
  members: RoomMember[] = [],
  nextOwner: Partial<RoomMember> | null,
  previousOwner?: Pick<RoomMember, 'id' | 'oderId'>,
  now = Date.now()
): RoomMember[] {
  const nextMembers = pruneRoomMembers(members, now).map((member) => {
    const isPreviousOwner =
      member.role === 'host'
      || (!!previousOwner?.id && member.id === previousOwner.id)
      || (!!previousOwner?.oderId && member.oderId === previousOwner.oderId);

    return isPreviousOwner
      ? { ...member,
        role: 'member' as const }
      : member;
  });

  if (!nextOwner || !(nextOwner.id || nextOwner.oderId))
    return pruneRoomMembers(nextMembers, now);

  const existingNextOwner = findRoomMember(nextMembers, nextOwner.id || nextOwner.oderId);
  const nextOwnerMember: RoomMember = {
    id: existingNextOwner?.id || nextOwner.id || nextOwner.oderId || '',
    oderId: existingNextOwner?.oderId || nextOwner.oderId || undefined,
    username: existingNextOwner?.username || nextOwner.username || '',
    displayName: existingNextOwner?.displayName || nextOwner.displayName || 'User',
    avatarUrl: existingNextOwner?.avatarUrl || nextOwner.avatarUrl || undefined,
    avatarHash: existingNextOwner?.avatarHash || nextOwner.avatarHash || undefined,
    avatarMime: existingNextOwner?.avatarMime || nextOwner.avatarMime || undefined,
    avatarUpdatedAt: existingNextOwner?.avatarUpdatedAt || nextOwner.avatarUpdatedAt || undefined,
    role: 'host',
    joinedAt: existingNextOwner?.joinedAt || nextOwner.joinedAt || now,
    lastSeenAt: existingNextOwner?.lastSeenAt || nextOwner.lastSeenAt || now
  };

  return upsertRoomMember(nextMembers, nextOwnerMember, now);
}

/** Update a persisted member role without touching presence timestamps. */
export function updateRoomMemberRole(
  members: RoomMember[] = [],
  identifier: string,
  role: RoomMember['role']
): RoomMember[] {
  const nextMembers = pruneRoomMembers(members);
  const memberIndex = nextMembers.findIndex((member) => member.id === identifier || member.oderId === identifier);

  if (memberIndex < 0)
    return nextMembers;

  const updatedMembers = [...nextMembers];

  updatedMembers[memberIndex] = {
    ...updatedMembers[memberIndex],
    role
  };

  return pruneRoomMembers(updatedMembers);
}

/** Compare two room rosters after normalization and pruning. */
export function areRoomMembersEqual(
  firstMembers: RoomMember[] = [],
  secondMembers: RoomMember[] = []
): boolean {
  const now = Date.now();
  const first = pruneRoomMembers(firstMembers, now);
  const second = pruneRoomMembers(secondMembers, now);

  if (first.length !== second.length) {
    return false;
  }

  for (let memberIndex = 0; memberIndex < first.length; memberIndex++) {
    if (!areRoomMembersIdentical(first[memberIndex], second[memberIndex])) {
      return false;
    }
  }

  return true;
}

function areRoomMembersIdentical(firstMember: RoomMember, secondMember: RoomMember): boolean {
  if (firstMember === secondMember) {
    return true;
  }

  return firstMember.id === secondMember.id
    && firstMember.oderId === secondMember.oderId
    && firstMember.username === secondMember.username
    && firstMember.displayName === secondMember.displayName
    && firstMember.role === secondMember.role
    && firstMember.avatarUrl === secondMember.avatarUrl
    && firstMember.avatarHash === secondMember.avatarHash
    && firstMember.avatarMime === secondMember.avatarMime
    && firstMember.avatarUpdatedAt === secondMember.avatarUpdatedAt
    && firstMember.profileUpdatedAt === secondMember.profileUpdatedAt
    && firstMember.description === secondMember.description
    && firstMember.joinedAt === secondMember.joinedAt
    && firstMember.lastSeenAt === secondMember.lastSeenAt
    && areRoleIdsEqual(firstMember.roleIds, secondMember.roleIds);
}

function areRoleIdsEqual(
  firstRoleIds: string[] | undefined,
  secondRoleIds: string[] | undefined
): boolean {
  if (firstRoleIds === secondRoleIds) {
    return true;
  }

  const first = firstRoleIds ?? [];
  const second = secondRoleIds ?? [];

  if (first.length !== second.length) {
    return false;
  }

  for (let roleIndex = 0; roleIndex < first.length; roleIndex++) {
    if (first[roleIndex] !== second[roleIndex]) {
      return false;
    }
  }

  return true;
}
