import {
  DataSource,
  EntityManager,
  In
} from 'typeorm';
import {
  ServerChannelEntity,
  ServerTagEntity,
  ServerRoleEntity,
  ServerUserRoleEntity,
  ServerChannelPermissionEntity
} from '../entities';
import {
  AccessRolePayload,
  ChannelPermissionPayload,
  RoleAssignmentPayload,
  ServerChannelPayload,
  ServerPayload,
  ServerPermissionKeyPayload,
  PermissionStatePayload
} from './types';

const SERVER_PERMISSION_KEYS: ServerPermissionKeyPayload[] = [
  'manageServer',
  'manageRoles',
  'manageChannels',
  'manageIcon',
  'kickMembers',
  'banMembers',
  'manageBans',
  'deleteMessages',
  'joinVoice',
  'shareScreen',
  'uploadFiles'
];
const SYSTEM_ROLE_IDS = {
  everyone: 'system-everyone',
  moderator: 'system-moderator',
  admin: 'system-admin'
} as const;

interface ServerRelationRecord {
  tags: string[];
  channels: ServerChannelPayload[];
  roles: AccessRolePayload[];
  roleAssignments: RoleAssignmentPayload[];
  channelPermissions: ChannelPermissionPayload[];
}

function normalizeChannelName(name: string): string {
  return name.trim().replace(/\s+/g, ' ');
}

function channelNameKey(type: ServerChannelPayload['type'], name: string): string {
  return `${type}:${normalizeChannelName(name).toLocaleLowerCase()}`;
}

function compareText(firstValue: string, secondValue: string): number {
  return firstValue.localeCompare(secondValue, undefined, { sensitivity: 'base' });
}

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function uniqueStrings(values: readonly string[] | undefined): string[] {
  return Array.from(new Set((values ?? [])
    .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
    .map((value) => value.trim())));
}

function normalizePermissionState(value: unknown): PermissionStatePayload {
  return value === 'allow' || value === 'deny' || value === 'inherit'
    ? value
    : 'inherit';
}

function normalizePermissionMatrix(rawMatrix: unknown): Partial<Record<ServerPermissionKeyPayload, PermissionStatePayload>> {
  const matrix = rawMatrix && typeof rawMatrix === 'object'
    ? rawMatrix as Record<string, unknown>
    : {};
  const normalized: Partial<Record<ServerPermissionKeyPayload, PermissionStatePayload>> = {};

  for (const key of SERVER_PERMISSION_KEYS) {
    const value = normalizePermissionState(matrix[key]);

    if (value !== 'inherit') {
      normalized[key] = value;
    }
  }

  return normalized;
}

function buildDefaultServerRoles(): AccessRolePayload[] {
  return [
    {
      id: SYSTEM_ROLE_IDS.everyone,
      name: '@everyone',
      color: '#6b7280',
      position: 0,
      isSystem: true,
      permissions: {
        joinVoice: 'allow',
        shareScreen: 'allow',
        uploadFiles: 'allow'
      }
    },
    {
      id: SYSTEM_ROLE_IDS.moderator,
      name: 'Moderator',
      color: '#10b981',
      position: 200,
      isSystem: true,
      permissions: {
        kickMembers: 'allow',
        deleteMessages: 'allow'
      }
    },
    {
      id: SYSTEM_ROLE_IDS.admin,
      name: 'Admin',
      color: '#60a5fa',
      position: 300,
      isSystem: true,
      permissions: {
        kickMembers: 'allow',
        banMembers: 'allow',
        manageBans: 'allow',
        deleteMessages: 'allow',
        manageChannels: 'allow',
        manageIcon: 'allow'
      }
    }
  ];
}

function normalizeServerRole(rawRole: Partial<AccessRolePayload>, fallbackRole?: AccessRolePayload): AccessRolePayload | null {
  const id = typeof rawRole.id === 'string' ? rawRole.id.trim() : fallbackRole?.id ?? '';
  const name = typeof rawRole.name === 'string' ? rawRole.name.trim().replace(/\s+/g, ' ') : fallbackRole?.name ?? '';

  if (!id || !name) {
    return null;
  }

  return {
    id,
    name,
    color: typeof rawRole.color === 'string' && rawRole.color.trim() ? rawRole.color.trim() : fallbackRole?.color,
    position: isFiniteNumber(rawRole.position) ? rawRole.position : fallbackRole?.position ?? 0,
    isSystem: typeof rawRole.isSystem === 'boolean' ? rawRole.isSystem : fallbackRole?.isSystem,
    permissions: normalizePermissionMatrix(rawRole.permissions ?? fallbackRole?.permissions)
  };
}

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

  return compareText(firstRole.name, secondRole.name);
}

function compareAssignments(firstAssignment: RoleAssignmentPayload, secondAssignment: RoleAssignmentPayload): number {
  return compareText(firstAssignment.oderId || firstAssignment.userId, secondAssignment.oderId || secondAssignment.userId);
}

export function normalizeServerTags(rawTags: unknown): string[] {
  if (!Array.isArray(rawTags)) {
    return [];
  }

  return rawTags
    .filter((tag): tag is string => typeof tag === 'string')
    .map((tag) => tag.trim())
    .filter(Boolean);
}

export function normalizeServerChannels(rawChannels: unknown): ServerChannelPayload[] {
  if (!Array.isArray(rawChannels)) {
    return [];
  }

  const seenIds = new Set<string>();
  const seenNames = new Set<string>();
  const channels: ServerChannelPayload[] = [];

  for (const [index, rawChannel] of rawChannels.entries()) {
    if (!rawChannel || typeof rawChannel !== 'object') {
      continue;
    }

    const channel = rawChannel as Record<string, unknown>;
    const id = typeof channel['id'] === 'string' ? channel['id'].trim() : '';
    const name = typeof channel['name'] === 'string' ? normalizeChannelName(channel['name']) : '';
    const type = channel['type'] === 'text' || channel['type'] === 'voice' ? channel['type'] : null;
    const position = isFiniteNumber(channel['position']) ? channel['position'] : index;
    const nameKey = type ? channelNameKey(type, name) : '';

    if (!id || !name || !type || seenIds.has(id) || seenNames.has(nameKey)) {
      continue;
    }

    seenIds.add(id);
    seenNames.add(nameKey);
    channels.push({
      id,
      name,
      type,
      position
    });
  }

  return channels;
}

export function normalizeServerRoles(rawRoles: unknown): AccessRolePayload[] {
  const rolesById = new Map<string, AccessRolePayload>();

  if (Array.isArray(rawRoles)) {
    for (const rawRole of rawRoles) {
      if (!rawRole || typeof rawRole !== 'object') {
        continue;
      }

      const normalizedRole = normalizeServerRole(rawRole as Record<string, unknown>);

      if (normalizedRole) {
        rolesById.set(normalizedRole.id, normalizedRole);
      }
    }
  }

  for (const defaultRole of buildDefaultServerRoles()) {
    const mergedRole = normalizeServerRole(rolesById.get(defaultRole.id) ?? defaultRole, defaultRole) ?? defaultRole;

    rolesById.set(defaultRole.id, mergedRole);
  }

  return Array.from(rolesById.values()).sort(compareRoles);
}

export function normalizeServerRoleAssignments(rawAssignments: unknown, roles: readonly AccessRolePayload[]): RoleAssignmentPayload[] {
  const validRoleIds = new Set(roles.map((role) => role.id).filter((roleId) => roleId !== SYSTEM_ROLE_IDS.everyone));
  const assignmentsByKey = new Map<string, RoleAssignmentPayload>();

  if (!Array.isArray(rawAssignments)) {
    return [];
  }

  for (const rawAssignment of rawAssignments) {
    if (!rawAssignment || typeof rawAssignment !== 'object') {
      continue;
    }

    const assignment = rawAssignment as Record<string, unknown>;
    const userId = typeof assignment['userId'] === 'string' ? assignment['userId'].trim() : '';
    const oderId = typeof assignment['oderId'] === 'string' ? assignment['oderId'].trim() : undefined;
    const key = oderId || userId;

    if (!key) {
      continue;
    }

    const roleIds = uniqueStrings(Array.isArray(assignment['roleIds']) ? assignment['roleIds'] as string[] : undefined)
      .filter((roleId) => validRoleIds.has(roleId));

    if (roleIds.length === 0) {
      continue;
    }

    assignmentsByKey.set(key, {
      userId: userId || key,
      oderId,
      roleIds
    });
  }

  return Array.from(assignmentsByKey.values()).sort(compareAssignments);
}

export function normalizeServerChannelPermissions(
  rawChannelPermissions: unknown,
  roles: readonly AccessRolePayload[]
): ChannelPermissionPayload[] {
  if (!Array.isArray(rawChannelPermissions)) {
    return [];
  }

  const validRoleIds = new Set(roles.map((role) => role.id));
  const overridesByKey = new Map<string, ChannelPermissionPayload>();

  for (const rawOverride of rawChannelPermissions) {
    if (!rawOverride || typeof rawOverride !== 'object') {
      continue;
    }

    const override = rawOverride as Record<string, unknown>;
    const channelId = typeof override['channelId'] === 'string' ? override['channelId'].trim() : '';
    const targetType = override['targetType'] === 'role' || override['targetType'] === 'user' ? override['targetType'] : null;
    const targetId = typeof override['targetId'] === 'string' ? override['targetId'].trim() : '';
    const permission = SERVER_PERMISSION_KEYS.find((key) => key === override['permission']);
    const value = normalizePermissionState(override['value']);

    if (!channelId || !targetType || !targetId || !permission || value === 'inherit') {
      continue;
    }

    if (targetType === 'role' && !validRoleIds.has(targetId)) {
      continue;
    }

    const key = `${channelId}:${targetType}:${targetId}:${permission}`;

    overridesByKey.set(key, {
      channelId,
      targetType,
      targetId,
      permission,
      value
    });
  }

  return Array.from(overridesByKey.values()).sort((firstOverride, secondOverride) => {
    const channelCompare = compareText(firstOverride.channelId, secondOverride.channelId);

    if (channelCompare !== 0) {
      return channelCompare;
    }

    if (firstOverride.targetType !== secondOverride.targetType) {
      return compareText(firstOverride.targetType, secondOverride.targetType);
    }

    const targetCompare = compareText(firstOverride.targetId, secondOverride.targetId);

    if (targetCompare !== 0) {
      return targetCompare;
    }

    return compareText(firstOverride.permission, secondOverride.permission);
  });
}

export async function replaceServerRelations(
  manager: EntityManager,
  serverId: string,
  options: {
    tags: unknown;
    channels: unknown;
    roles?: unknown;
    roleAssignments?: unknown;
    channelPermissions?: unknown;
  }
): Promise<void> {
  const tagRepo = manager.getRepository(ServerTagEntity);
  const channelRepo = manager.getRepository(ServerChannelEntity);
  const roleRepo = manager.getRepository(ServerRoleEntity);
  const userRoleRepo = manager.getRepository(ServerUserRoleEntity);
  const channelPermissionRepo = manager.getRepository(ServerChannelPermissionEntity);
  const tags = normalizeServerTags(options.tags);
  const channels = normalizeServerChannels(options.channels);
  const roles = options.roles !== undefined ? normalizeServerRoles(options.roles) : [];

  await tagRepo.delete({ serverId });
  await channelRepo.delete({ serverId });

  if (options.roles !== undefined) {
    await roleRepo.delete({ serverId });
  }

  if (options.roleAssignments !== undefined) {
    await userRoleRepo.delete({ serverId });
  }

  if (options.channelPermissions !== undefined) {
    await channelPermissionRepo.delete({ serverId });
  }

  if (tags.length > 0) {
    await tagRepo.insert(
      tags.map((tag, position) => ({
        serverId,
        position,
        value: tag
      }))
    );
  }

  if (channels.length > 0) {
    await channelRepo.insert(
      channels.map((channel) => ({
        serverId,
        channelId: channel.id,
        name: channel.name,
        type: channel.type,
        position: channel.position
      }))
    );
  }

  if (options.roles !== undefined && roles.length > 0) {
    await roleRepo.insert(
      roles.map((role) => ({
        serverId,
        roleId: role.id,
        name: role.name,
        color: role.color ?? null,
        position: role.position,
        isSystem: role.isSystem ? 1 : 0,
        manageServer: normalizePermissionState(role.permissions?.manageServer),
        manageRoles: normalizePermissionState(role.permissions?.manageRoles),
        manageChannels: normalizePermissionState(role.permissions?.manageChannels),
        manageIcon: normalizePermissionState(role.permissions?.manageIcon),
        kickMembers: normalizePermissionState(role.permissions?.kickMembers),
        banMembers: normalizePermissionState(role.permissions?.banMembers),
        manageBans: normalizePermissionState(role.permissions?.manageBans),
        deleteMessages: normalizePermissionState(role.permissions?.deleteMessages),
        joinVoice: normalizePermissionState(role.permissions?.joinVoice),
        shareScreen: normalizePermissionState(role.permissions?.shareScreen),
        uploadFiles: normalizePermissionState(role.permissions?.uploadFiles)
      }))
    );
  }

  if (options.roleAssignments !== undefined) {
    const roleAssignments = normalizeServerRoleAssignments(options.roleAssignments, roles.length > 0 ? roles : normalizeServerRoles([]));
    const rows = roleAssignments.flatMap((assignment) =>
      assignment.roleIds.map((roleId) => ({
        serverId,
        userId: assignment.userId,
        roleId,
        oderId: assignment.oderId ?? null
      }))
    );

    if (rows.length > 0) {
      await userRoleRepo.insert(rows);
    }
  }

  if (options.channelPermissions !== undefined) {
    const channelPermissions = normalizeServerChannelPermissions(
      options.channelPermissions,
      roles.length > 0 ? roles : normalizeServerRoles([])
    );

    if (channelPermissions.length > 0) {
      await channelPermissionRepo.insert(
        channelPermissions.map((channelPermission) => ({
          serverId,
          channelId: channelPermission.channelId,
          targetType: channelPermission.targetType,
          targetId: channelPermission.targetId,
          permission: channelPermission.permission,
          value: channelPermission.value
        }))
      );
    }
  }
}

export async function loadServerRelationsMap(
  dataSource: DataSource,
  serverIds: readonly string[]
): Promise<Map<string, ServerRelationRecord>> {
  const groupedRelations = new Map<string, ServerRelationRecord>();

  if (serverIds.length === 0) {
    return groupedRelations;
  }

  const [
    tagRows,
    channelRows,
    roleRows,
    userRoleRows,
    channelPermissionRows
  ] = await Promise.all([
    dataSource.getRepository(ServerTagEntity).find({
      where: { serverId: In([...serverIds]) }
    }),
    dataSource.getRepository(ServerChannelEntity).find({
      where: { serverId: In([...serverIds]) }
    }),
    dataSource.getRepository(ServerRoleEntity).find({
      where: { serverId: In([...serverIds]) }
    }),
    dataSource.getRepository(ServerUserRoleEntity).find({
      where: { serverId: In([...serverIds]) }
    }),
    dataSource.getRepository(ServerChannelPermissionEntity).find({
      where: { serverId: In([...serverIds]) }
    })
  ]);

  for (const serverId of serverIds) {
    groupedRelations.set(serverId, {
      tags: [],
      channels: [],
      roles: [],
      roleAssignments: [],
      channelPermissions: []
    });
  }

  for (const row of tagRows) {
    groupedRelations.get(row.serverId)?.tags.push(row.value);
  }

  for (const row of channelRows) {
    groupedRelations.get(row.serverId)?.channels.push({
      id: row.channelId,
      name: row.name,
      type: row.type,
      position: row.position
    });
  }

  for (const row of roleRows) {
    groupedRelations.get(row.serverId)?.roles.push({
      id: row.roleId,
      name: row.name,
      color: row.color ?? undefined,
      position: row.position,
      isSystem: !!row.isSystem,
      permissions: normalizePermissionMatrix({
        manageServer: row.manageServer,
        manageRoles: row.manageRoles,
        manageChannels: row.manageChannels,
        manageIcon: row.manageIcon,
        kickMembers: row.kickMembers,
        banMembers: row.banMembers,
        manageBans: row.manageBans,
        deleteMessages: row.deleteMessages,
        joinVoice: row.joinVoice,
        shareScreen: row.shareScreen,
        uploadFiles: row.uploadFiles
      })
    });
  }

  for (const row of userRoleRows) {
    const relation = groupedRelations.get(row.serverId);

    if (!relation) {
      continue;
    }

    const existing = relation.roleAssignments.find((assignment) => assignment.userId === row.userId || assignment.oderId === row.oderId);

    if (existing) {
      existing.roleIds = uniqueStrings([...existing.roleIds, row.roleId]);
      continue;
    }

    relation.roleAssignments.push({
      userId: row.userId,
      oderId: row.oderId ?? undefined,
      roleIds: [row.roleId]
    });
  }

  for (const row of channelPermissionRows) {
    groupedRelations.get(row.serverId)?.channelPermissions.push({
      channelId: row.channelId,
      targetType: row.targetType,
      targetId: row.targetId,
      permission: row.permission as ServerPermissionKeyPayload,
      value: normalizePermissionState(row.value)
    });
  }

  for (const [serverId, relation] of groupedRelations) {
    relation.tags = tagRows
      .filter((row) => row.serverId === serverId)
      .sort((firstTag, secondTag) => firstTag.position - secondTag.position)
      .map((row) => row.value);

    relation.channels.sort(
      (firstChannel, secondChannel) => firstChannel.position - secondChannel.position || compareText(firstChannel.name, secondChannel.name)
    );

    relation.roles.sort(compareRoles);
    relation.roleAssignments.sort(compareAssignments);
  }

  return groupedRelations;
}

export function relationRecordToServerPayload(
  row: Pick<ServerPayload, 'slowModeInterval'>,
  relations: ServerRelationRecord
): Pick<ServerPayload, 'tags' | 'channels' | 'roles' | 'roleAssignments' | 'channelPermissions' | 'slowModeInterval'> {
  return {
    tags: relations.tags,
    channels: relations.channels,
    roles: relations.roles,
    roleAssignments: relations.roleAssignments,
    channelPermissions: relations.channelPermissions,
    slowModeInterval: row.slowModeInterval ?? 0
  };
}
