import { Response, Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { ServerChannelPayload, ServerPayload } from '../cqrs/types';
import {
  getAllPublicServers,
  getFeaturedServers,
  getTrendingServers,
  getServerById,
  getUserById,
  upsertServer,
  deleteServer,
  getPendingRequestsForServer
} from '../cqrs';
import {
  banServerUser,
  buildSignalingUrl,
  createServerInvite,
  joinServerWithAccess,
  leaveServerUser,
  passwordHashForInput,
  ServerAccessError,
  kickServerUser,
  ensureServerMembership,
  unbanServerUser,
  countServerMemberships
} from '../services/server-access.service';
import {
  buildAppInviteUrl,
  buildBrowserInviteUrl,
  buildInviteUrl,
  getRequestOrigin
} from './invite-utils';
import {
  canManageServerUpdate,
  canModerateServerMember,
  resolveServerPermission
} from '../services/server-permissions.service';
import {
  getAuthenticatedUserId,
  requireAuth,
  rejectSpoofedUserId
} from '../middleware/require-auth';

const router = Router();

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

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

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

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

    const id = typeof channel.id === 'string' ? channel.id.trim() : '';
    const name = typeof channel.name === 'string' ? channel.name.trim().replace(/\s+/g, ' ') : '';
    const type = channel.type === 'text' || channel.type === 'voice' ? channel.type : null;
    const position = typeof channel.position === 'number' ? channel.position : index;
    const nameKey = type ? channelNameKey(type, name) : '';

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

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

  return channels;
}

async function enrichServer(server: ServerPayload, sourceUrl?: string) {
  const owner = await getUserById(server.ownerId);
  const userCount = await countServerMemberships(server.id);
  const { passwordHash, ...publicServer } = server;

  return {
    ...publicServer,
    hasPassword: server.hasPassword ?? !!passwordHash,
    ownerName: owner?.displayName,
    sourceUrl,
    currentUsers: userCount,
    userCount
  };
}

function sendAccessError(error: unknown, res: Response) {
  if (error instanceof ServerAccessError) {
    res.status(error.status).json({ error: error.message, errorCode: error.code });
    return;
  }

  console.error('Unhandled server access error:', error);
  res.status(500).json({ error: 'Internal server error', errorCode: 'INTERNAL_ERROR' });
}

async function buildInviteResponse(invite: {
  id: string;
  createdAt: number;
  expiresAt: number;
  createdBy: string;
  createdByDisplayName: string | null;
  serverId: string;
}, server: ServerPayload, signalOrigin: string) {
  return {
    id: invite.id,
    serverId: invite.serverId,
    createdAt: invite.createdAt,
    expiresAt: invite.expiresAt,
    inviteUrl: buildInviteUrl(signalOrigin, invite.id),
    browserUrl: buildBrowserInviteUrl(signalOrigin, invite.id),
    appUrl: buildAppInviteUrl(signalOrigin, invite.id),
    sourceUrl: signalOrigin,
    createdBy: invite.createdBy,
    createdByDisplayName: invite.createdByDisplayName ?? undefined,
    isExpired: invite.expiresAt <= Date.now(),
    server: await enrichServer(server, signalOrigin)
  };
}

router.get('/', async (req, res) => {
  const { q, tags, limit = 20, offset = 0 } = req.query;

  let results = await getAllPublicServers();

  if (q) {
    const search = String(q).toLowerCase();

    results = results.filter(server =>
      server.name.toLowerCase().includes(search) ||
      server.description?.toLowerCase().includes(search)
    );
  }

  if (tags) {
    const tagList = String(tags).split(',');

    results = results.filter(server => tagList.some(tag => server.tags.includes(tag)));
  }

  const total = results.length;

  results = results.slice(Number(offset), Number(offset) + Number(limit));

  const enrichedResults = await Promise.all(results.map((server) => enrichServer(server)));

  res.json({ servers: enrichedResults, total, limit: Number(limit), offset: Number(offset) });
});

function parseDiscoveryLimit(value: unknown, fallback: number): number {
  const parsed = Number(value);

  if (!Number.isFinite(parsed) || parsed <= 0) {
    return fallback;
  }

  return Math.min(Math.floor(parsed), 50);
}

// NOTE: `/featured` and `/trending` must be registered before `/:id`,
// otherwise Express resolves them as a server id.
router.get('/featured', async (req, res) => {
  const limit = parseDiscoveryLimit(req.query.limit, 12);
  const servers = await getFeaturedServers(limit);
  const enrichedResults = await Promise.all(servers.map((server) => enrichServer(server)));

  res.json({ servers: enrichedResults, total: enrichedResults.length, limit });
});

router.get('/trending', async (req, res) => {
  const limit = parseDiscoveryLimit(req.query.limit, 12);
  const servers = await getTrendingServers(limit);
  const enrichedResults = await Promise.all(servers.map((server) => enrichServer(server)));

  res.json({ servers: enrichedResults, total: enrichedResults.length, limit });
});

router.post('/', requireAuth, async (req, res) => {
  const {
    id: clientId,
    name,
    description,
    ownerId,
    ownerPublicKey,
    isPrivate,
    maxUsers,
    password,
    tags,
    channels,
    icon,
    iconUpdatedAt
  } = req.body;

  if (!name || !ownerId || !ownerPublicKey)
    return res.status(400).json({ error: 'Missing required fields' });

  if (!rejectSpoofedUserId(req, res, ownerId, 'ownerId')) {
    return;
  }

  const authenticatedOwnerId = getAuthenticatedUserId(req);
  const passwordHash = passwordHashForInput(password);
  const server: ServerPayload = {
    id: clientId || uuidv4(),
    name,
    description,
    ownerId: authenticatedOwnerId,
    ownerPublicKey,
    hasPassword: !!passwordHash,
    passwordHash,
    isPrivate: isPrivate ?? false,
    maxUsers: maxUsers ?? 0,
    currentUsers: 0,
    icon: typeof icon === 'string' ? icon : undefined,
    iconUpdatedAt: typeof iconUpdatedAt === 'number' ? iconUpdatedAt : undefined,
    tags: tags ?? [],
    channels: normalizeServerChannels(channels),
    createdAt: Date.now(),
    lastSeen: Date.now()
  };

  await upsertServer(server);
  await ensureServerMembership(server.id, authenticatedOwnerId);

  res.status(201).json(await enrichServer(server, getRequestOrigin(req)));
});

router.put('/:id', requireAuth, async (req, res) => {
  const { id } = req.params;
  const {
    currentOwnerId,
    actingRole,
    password,
    hasPassword: _ignoredHasPassword,
    passwordHash: _ignoredPasswordHash,
    channels,
    ...updates
  } = req.body;
  const existing = await getServerById(id);
  const authenticatedOwnerId = getAuthenticatedUserId(req);

  if (!existing)
    return res.status(404).json({ error: 'Server not found' });

  if (currentOwnerId && currentOwnerId !== authenticatedOwnerId) {
    return res.status(400).json({
      error: 'currentOwnerId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  if (!canManageServerUpdate(existing, authenticatedOwnerId, {
    ...updates,
    channels,
    password,
    actingRole
  })) {
    return res.status(403).json({ error: 'Not authorized' });
  }

  const hasPasswordUpdate = Object.prototype.hasOwnProperty.call(req.body, 'password');
  const hasChannelsUpdate = Object.prototype.hasOwnProperty.call(req.body, 'channels');
  const nextPasswordHash = hasPasswordUpdate ? passwordHashForInput(password) : (existing.passwordHash ?? null);
  const server: ServerPayload = {
    ...existing,
    ...updates,
    channels: hasChannelsUpdate ? normalizeServerChannels(channels) : existing.channels,
    hasPassword: !!nextPasswordHash,
    passwordHash: nextPasswordHash,
    lastSeen: Date.now()
  };

  await upsertServer(server);
  res.json(await enrichServer(server, getRequestOrigin(req)));
});

router.post('/:id/join', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { userId, password, inviteId } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);

  if (userId && userId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'userId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  try {
    const result = await joinServerWithAccess({
      serverId,
      userId: authenticatedUserId,
      password: typeof password === 'string' ? password : undefined,
      inviteId: typeof inviteId === 'string' ? inviteId : undefined
    });
    const origin = getRequestOrigin(req);

    res.json({
      success: true,
      signalingUrl: buildSignalingUrl(origin),
      joinedBefore: result.joinedBefore,
      via: result.via,
      server: await enrichServer(result.server, origin)
    });
  } catch (error) {
    sendAccessError(error, res);
  }
});

router.post('/:id/invites', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { requesterUserId, requesterDisplayName } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);

  if (requesterUserId && requesterUserId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'requesterUserId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  const server = await getServerById(serverId);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  try {
    const invite = await createServerInvite(
      serverId,
      authenticatedUserId,
      typeof requesterDisplayName === 'string' ? requesterDisplayName : undefined
    );

    res.status(201).json(await buildInviteResponse(invite, server, getRequestOrigin(req)));
  } catch (error) {
    sendAccessError(error, res);
  }
});

router.post('/:id/moderation/kick', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { actorUserId, targetUserId } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);
  const server = await getServerById(serverId);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  if (!targetUserId) {
    return res.status(400).json({ error: 'Missing targetUserId', errorCode: 'MISSING_TARGET' });
  }

  if (actorUserId && actorUserId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'actorUserId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  if (!canModerateServerMember(server, authenticatedUserId, String(targetUserId), 'kickMembers')) {
    return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
  }

  await kickServerUser(serverId, String(targetUserId));

  res.json({ ok: true });
});

router.post('/:id/moderation/ban', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { actorUserId, targetUserId, banId, displayName, reason, expiresAt } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);
  const server = await getServerById(serverId);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  if (!targetUserId) {
    return res.status(400).json({ error: 'Missing targetUserId', errorCode: 'MISSING_TARGET' });
  }

  if (actorUserId && actorUserId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'actorUserId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  if (!canModerateServerMember(server, authenticatedUserId, String(targetUserId), 'banMembers')) {
    return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
  }

  await banServerUser({
    serverId,
    userId: String(targetUserId),
    banId: typeof banId === 'string' ? banId : undefined,
    bannedBy: authenticatedUserId,
    displayName: typeof displayName === 'string' ? displayName : undefined,
    reason: typeof reason === 'string' ? reason : undefined,
    expiresAt: typeof expiresAt === 'number' ? expiresAt : undefined
  });

  res.json({ ok: true });
});

router.post('/:id/moderation/unban', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { actorUserId, banId, targetUserId } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);
  const server = await getServerById(serverId);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  if (actorUserId && actorUserId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'actorUserId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  if (!resolveServerPermission(server, authenticatedUserId, 'manageBans')) {
    return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
  }

  await unbanServerUser({
    serverId,
    banId: typeof banId === 'string' ? banId : undefined,
    userId: typeof targetUserId === 'string' ? targetUserId : undefined
  });

  res.json({ ok: true });
});

router.post('/:id/leave', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const { userId } = req.body;
  const authenticatedUserId = getAuthenticatedUserId(req);
  const server = await getServerById(serverId);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  if (userId && userId !== authenticatedUserId) {
    return res.status(400).json({
      error: 'userId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  await leaveServerUser(serverId, authenticatedUserId);

  res.json({ ok: true });
});

router.post('/:id/heartbeat', requireAuth, async (req, res) => {
  const { id } = req.params;
  const { currentUsers } = req.body;
  const existing = await getServerById(id);

  if (!existing)
    return res.status(404).json({ error: 'Server not found' });

  const server: ServerPayload = {
    ...existing,
    lastSeen: Date.now(),
    currentUsers: typeof currentUsers === 'number' ? currentUsers : existing.currentUsers
  };

  await upsertServer(server);
  res.json({ ok: true });
});

router.delete('/:id', requireAuth, async (req, res) => {
  const { id } = req.params;
  const { ownerId } = req.body;
  const authenticatedOwnerId = getAuthenticatedUserId(req);
  const existing = await getServerById(id);

  if (!existing)
    return res.status(404).json({ error: 'Server not found' });

  if (ownerId && ownerId !== authenticatedOwnerId) {
    return res.status(400).json({
      error: 'ownerId must match the authenticated user',
      errorCode: 'USER_ID_MISMATCH'
    });
  }

  if (existing.ownerId !== authenticatedOwnerId)
    return res.status(403).json({ error: 'Not authorized' });

  await deleteServer(id);
  res.json({ ok: true });
});

router.get('/:id/requests', requireAuth, async (req, res) => {
  const { id: serverId } = req.params;
  const authenticatedUserId = getAuthenticatedUserId(req);
  const server = await getServerById(serverId);

  if (!server)
    return res.status(404).json({ error: 'Server not found' });

  if (!resolveServerPermission(server, authenticatedUserId, 'manageServer'))
    return res.status(403).json({ error: 'Not authorized' });

  const requests = await getPendingRequestsForServer(serverId);

  res.json({ requests });
});

router.get('/:id', async (req, res) => {
  const { id } = req.params;
  const server = await getServerById(id);

  if (!server) {
    return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
  }

  res.json(await enrichServer(server, getRequestOrigin(req)));
});

export default router;
