import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
import { getDataSource } from '../db/database';
import {
  ServerBanEntity,
  ServerEntity,
  ServerInviteEntity,
  ServerMembershipEntity
} from '../entities';
import { rowToServer } from '../cqrs/mappers';
import { loadServerRelationsMap } from '../cqrs/relations';
import { ServerPayload } from '../cqrs/types';

export const SERVER_INVITE_EXPIRY_MS = 10 * 24 * 60 * 60 * 1000;

export type JoinAccessVia = 'membership' | 'password' | 'invite' | 'public';

export interface JoinServerAccessResult {
  joinedBefore: boolean;
  server: ServerPayload;
  via: JoinAccessVia;
}

export interface BanServerUserOptions {
  banId?: string;
  bannedBy: string;
  displayName?: string;
  expiresAt?: number;
  reason?: string;
  serverId: string;
  userId: string;
}

export class ServerAccessError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string
  ) {
    super(message);
    this.name = 'ServerAccessError';
  }
}

function getServerRepository() {
  return getDataSource().getRepository(ServerEntity);
}

function getMembershipRepository() {
  return getDataSource().getRepository(ServerMembershipEntity);
}

function getInviteRepository() {
  return getDataSource().getRepository(ServerInviteEntity);
}

function getBanRepository() {
  return getDataSource().getRepository(ServerBanEntity);
}

async function toServerPayload(server: ServerEntity): Promise<ServerPayload> {
  const relationsByServerId = await loadServerRelationsMap(getDataSource(), [server.id]);

  return rowToServer(server, relationsByServerId.get(server.id));
}

function normalizePassword(password?: string | null): string | null {
  const normalized = password?.trim() ?? '';

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

function isServerOwner(server: ServerEntity, userId: string): boolean {
  return server.ownerId === userId;
}

export function hashServerPassword(password: string): string {
  return crypto.createHash('sha256').update(password)
    .digest('hex');
}

export function passwordHashForInput(password?: string | null): string | null {
  const normalized = normalizePassword(password);

  return normalized ? hashServerPassword(normalized) : null;
}

export function buildSignalingUrl(origin: string): string {
  return origin.replace(/^http/i, 'ws');
}

export async function pruneExpiredServerAccessArtifacts(now: number = Date.now()): Promise<void> {
  await getInviteRepository()
    .createQueryBuilder()
    .delete()
    .where('expiresAt <= :now', { now })
    .execute();

  await getBanRepository()
    .createQueryBuilder()
    .delete()
    .where('expiresAt IS NOT NULL AND expiresAt <= :now', { now })
    .execute();
}

export async function getServerRecord(serverId: string): Promise<ServerEntity | null> {
  return await getServerRepository().findOne({ where: { id: serverId } });
}

export async function getActiveServerBan(serverId: string, userId: string): Promise<ServerBanEntity | null> {
  const banRepo = getBanRepository();
  const ban = await banRepo.findOne({ where: { serverId, userId } });

  if (!ban)
    return null;

  if (ban.expiresAt && ban.expiresAt <= Date.now()) {
    await banRepo.delete({ id: ban.id });
    return null;
  }

  return ban;
}

export async function isServerUserBanned(serverId: string, userId: string): Promise<boolean> {
  return !!(await getActiveServerBan(serverId, userId));
}

export async function findServerMembership(serverId: string, userId: string): Promise<ServerMembershipEntity | null> {
  return await getMembershipRepository().findOne({ where: { serverId, userId } });
}

export async function countServerMemberships(serverId: string): Promise<number> {
  return await getMembershipRepository().count({ where: { serverId } });
}

export async function usersShareServerMembership(userA: string, userB: string): Promise<boolean> {
  if (userA === userB) {
    return true;
  }

  const repo = getMembershipRepository();
  const membershipsForA = await repo.find({
    where: { userId: userA },
    select: ['serverId']
  });

  if (membershipsForA.length === 0) {
    return false;
  }

  for (const membership of membershipsForA) {
    const shared = await repo.findOne({
      where: { serverId: membership.serverId, userId: userB }
    });

    if (shared) {
      return true;
    }
  }

  return false;
}

export async function ensureServerMembership(serverId: string, userId: string): Promise<ServerMembershipEntity> {
  const repo = getMembershipRepository();
  const now = Date.now();
  const existing = await repo.findOne({ where: { serverId, userId } });

  if (existing) {
    existing.lastAccessAt = now;
    await repo.save(existing);
    return existing;
  }

  const entity = repo.create({
    id: uuidv4(),
    serverId,
    userId,
    joinedAt: now,
    lastAccessAt: now
  });

  await repo.save(entity);
  return entity;
}

export async function removeServerMembership(serverId: string, userId: string): Promise<void> {
  await getMembershipRepository().delete({ serverId, userId });
}

export async function assertCanCreateInvite(serverId: string, requesterUserId: string): Promise<ServerEntity> {
  const server = await getServerRecord(serverId);

  if (!server) {
    throw new ServerAccessError(404, 'SERVER_NOT_FOUND', 'Server not found');
  }

  if (await isServerUserBanned(serverId, requesterUserId)) {
    throw new ServerAccessError(403, 'BANNED', 'Banned users cannot create invites');
  }

  const membership = await findServerMembership(serverId, requesterUserId);

  if (server.ownerId !== requesterUserId && !membership) {
    throw new ServerAccessError(403, 'NOT_MEMBER', 'Only joined users can create invites');
  }

  return server;
}

export async function createServerInvite(
  serverId: string,
  createdBy: string,
  createdByDisplayName?: string
): Promise<ServerInviteEntity> {
  await assertCanCreateInvite(serverId, createdBy);

  const repo = getInviteRepository();
  const now = Date.now();
  const invite = repo.create({
    id: uuidv4(),
    serverId,
    createdBy,
    createdByDisplayName: createdByDisplayName ?? null,
    createdAt: now,
    expiresAt: now + SERVER_INVITE_EXPIRY_MS
  });

  await repo.save(invite);
  return invite;
}

export async function getActiveServerInvite(
  inviteId: string
): Promise<{ invite: ServerInviteEntity; server: ServerPayload } | null> {
  await pruneExpiredServerAccessArtifacts();

  const invite = await getInviteRepository().findOne({ where: { id: inviteId } });

  if (!invite) {
    return null;
  }

  if (invite.expiresAt <= Date.now()) {
    await getInviteRepository().delete({ id: invite.id });
    return null;
  }

  const server = await getServerRecord(invite.serverId);

  if (!server) {
    return null;
  }

  return {
    invite,
    server: await toServerPayload(server)
  };
}

export async function joinServerWithAccess(options: {
  inviteId?: string;
  password?: string;
  serverId: string;
  userId: string;
}): Promise<JoinServerAccessResult> {
  await pruneExpiredServerAccessArtifacts();

  const server = await getServerRecord(options.serverId);

  if (!server) {
    throw new ServerAccessError(404, 'SERVER_NOT_FOUND', 'Server not found');
  }

  if (await isServerUserBanned(server.id, options.userId)) {
    throw new ServerAccessError(403, 'BANNED', 'Banned users cannot join this server');
  }

  if (isServerOwner(server, options.userId)) {
    const existingMembership = await findServerMembership(server.id, options.userId);

    await ensureServerMembership(server.id, options.userId);

    return {
      joinedBefore: !!existingMembership,
      server: await toServerPayload(server),
      via: 'membership'
    };
  }

  if (options.inviteId) {
    const inviteBundle = await getActiveServerInvite(options.inviteId);

    if (!inviteBundle || inviteBundle.server.id !== server.id) {
      throw new ServerAccessError(410, 'INVITE_EXPIRED', 'Invite link has expired or is invalid');
    }

    const existingMembership = await findServerMembership(server.id, options.userId);

    await ensureServerMembership(server.id, options.userId);

    return {
      joinedBefore: !!existingMembership,
      server: await toServerPayload(server),
      via: 'invite'
    };
  }

  const membership = await findServerMembership(server.id, options.userId);

  if (membership) {
    await ensureServerMembership(server.id, options.userId);

    return {
      joinedBefore: true,
      server: await toServerPayload(server),
      via: 'membership'
    };
  }

  if (server.passwordHash) {
    const passwordHash = passwordHashForInput(options.password);

    if (!passwordHash || passwordHash !== server.passwordHash) {
      throw new ServerAccessError(403, 'PASSWORD_REQUIRED', 'Password required to join this server');
    }

    await ensureServerMembership(server.id, options.userId);

    return {
      joinedBefore: false,
      server: await toServerPayload(server),
      via: 'password'
    };
  }

  if (server.isPrivate) {
    throw new ServerAccessError(403, 'PRIVATE_SERVER', 'Private servers require an invite link');
  }

  await ensureServerMembership(server.id, options.userId);

  return {
    joinedBefore: false,
    server: await toServerPayload(server),
    via: 'public'
  };
}

export async function authorizeWebSocketJoin(serverId: string, userId: string): Promise<{ allowed: boolean; reason?: string }> {
  await pruneExpiredServerAccessArtifacts();

  const server = await getServerRecord(serverId);

  if (!server) {
    return { allowed: false,
      reason: 'SERVER_NOT_FOUND' };
  }

  if (await isServerUserBanned(serverId, userId)) {
    return { allowed: false,
      reason: 'BANNED' };
  }

  if (isServerOwner(server, userId)) {
    await ensureServerMembership(serverId, userId);
    return { allowed: true };
  }

  const membership = await findServerMembership(serverId, userId);

  if (membership) {
    await ensureServerMembership(serverId, userId);
    return { allowed: true };
  }

  if (!server.isPrivate && !server.passwordHash) {
    await ensureServerMembership(serverId, userId);
    return { allowed: true };
  }

  return {
    allowed: false,
    reason: server.isPrivate ? 'PRIVATE_SERVER' : 'PASSWORD_REQUIRED'
  };
}

export async function kickServerUser(serverId: string, userId: string): Promise<void> {
  await removeServerMembership(serverId, userId);
}

export async function leaveServerUser(serverId: string, userId: string): Promise<void> {
  await removeServerMembership(serverId, userId);
}

export async function banServerUser(options: BanServerUserOptions): Promise<ServerBanEntity> {
  await removeServerMembership(options.serverId, options.userId);

  const repo = getBanRepository();
  const existing = await repo.findOne({ where: { serverId: options.serverId, userId: options.userId } });

  if (existing) {
    await repo.delete({ id: existing.id });
  }

  const entity = repo.create({
    id: options.banId ?? uuidv4(),
    serverId: options.serverId,
    userId: options.userId,
    bannedBy: options.bannedBy,
    displayName: options.displayName ?? null,
    reason: options.reason ?? null,
    expiresAt: options.expiresAt ?? null,
    createdAt: Date.now()
  });

  await repo.save(entity);
  return entity;
}

export async function unbanServerUser(options: { banId?: string; serverId: string; userId?: string }): Promise<void> {
  const repo = getBanRepository();

  if (options.banId) {
    await repo.delete({ id: options.banId, serverId: options.serverId });
  }

  if (options.userId) {
    await repo.delete({ serverId: options.serverId, userId: options.userId });
  }
}
