import { createHash, randomBytes } from 'node:crypto';
import { resolveAvatarUrl } from '@/lib/avatar';
import { sendMail } from '@/lib/mailer';
import { db } from '@/lib/prisma';
import type {
  AcceptComplexInvitationsInput,
  AcceptComplexInvitationsResponse,
  CancelComplexInvitationResponse,
  ComplexInvitation,
  ComplexInvitationStatus,
  ComplexUsersOverview,
  InviteComplexUserInput,
  InviteComplexUserResponse,
  RevokeComplexUserInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

const INVITATION_TTL_DAYS = Number(Bun.env.COMPLEX_INVITATION_TTL_DAYS ?? 7);
const APP_BASE_URL = Bun.env.APP_BASE_URL ?? 'http://localhost:5173';

export class ComplexMembersError extends Error {
  status: 400 | 403 | 404 | 409;

  constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
    super(message);
    this.name = 'ComplexMembersError';
    this.status = status;
  }
}

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase();
}

function hashToken(token: string): string {
  return createHash('sha256').update(token).digest('hex');
}

function createInvitationToken(): string {
  return randomBytes(32).toString('hex');
}

function nowPlusDays(days: number): Date {
  const date = new Date();
  date.setDate(date.getDate() + days);
  return date;
}

function getInvitationStatus(invitation: {
  expiresAt: Date;
  acceptedAt: Date | null;
  revokedAt: Date | null;
}): ComplexInvitationStatus {
  if (invitation.acceptedAt) return 'ACCEPTED';
  if (invitation.revokedAt) return 'REVOKED';
  if (invitation.expiresAt < new Date()) return 'EXPIRED';
  return 'PENDING';
}

function formatInvitation(invitation: {
  id: string;
  complexId: string;
  email: string;
  expiresAt: Date;
  acceptedAt: Date | null;
  revokedAt: Date | null;
  createdAt: Date;
}): ComplexInvitation {
  return {
    id: invitation.id,
    complexId: invitation.complexId,
    email: invitation.email,
    status: getInvitationStatus(invitation),
    createdAt: invitation.createdAt.toISOString(),
    expiresAt: invitation.expiresAt.toISOString(),
    acceptedAt: invitation.acceptedAt?.toISOString() ?? null,
    revokedAt: invitation.revokedAt?.toISOString() ?? null,
  };
}

async function ensureComplexAdmin(userId: string, complexId: string) {
  const membership = await db.complexUser.findUnique({
    where: {
      complexId_userId: {
        complexId,
        userId,
      },
    },
    select: {
      role: true,
    },
  });

  if (!membership) {
    throw new ComplexMembersError('No tenés acceso a este complejo.', 403);
  }

  if (membership.role !== 'ADMIN') {
    throw new ComplexMembersError('Solo un ADMIN puede administrar usuarios.', 403);
  }
}

async function ensureComplexMember(userId: string, complexId: string) {
  const membership = await db.complexUser.findUnique({
    where: {
      complexId_userId: {
        complexId,
        userId,
      },
    },
  });

  if (!membership) {
    throw new ComplexMembersError('No tenés acceso a este complejo.', 403);
  }

  return membership;
}

async function sendInvitationEmail(input: {
  complexName: string;
  inviteeEmail: string;
  token: string;
}) {
  const inviteUrl = new URL('/invite', APP_BASE_URL);
  inviteUrl.searchParams.set('inviteToken', input.token);
  inviteUrl.searchParams.set('email', input.inviteeEmail);

  await sendMail({
    to: input.inviteeEmail,
    subject: `Te invitaron a ${input.complexName}`,
    text: `Te invitaron a sumarte a ${input.complexName}. Abrí este enlace para crear tu cuenta o iniciar sesión: ${inviteUrl.toString()}`,
    html: `
      <h2>Te invitaron a <strong>${input.complexName}</strong></h2>
      <p>Hacé click en el siguiente enlace para crear tu cuenta o iniciar sesión y aceptar la invitación:</p>
      <p><a href="${inviteUrl.toString()}">${inviteUrl.toString()}</a></p>
    `,
  });
}

async function refreshPendingInvitation(invitationId: string) {
  const token = createInvitationToken();
  const tokenHash = hashToken(token);
  const expiresAt = nowPlusDays(INVITATION_TTL_DAYS);

  const invitation = await db.complexInvitation.update({
    where: {
      id: invitationId,
    },
    data: {
      tokenHash,
      expiresAt,
      acceptedAt: null,
      revokedAt: null,
    },
    include: {
      complex: {
        select: {
          complexName: true,
        },
      },
    },
  });

  return {
    invitation,
    token,
  };
}

export async function listComplexUsers(complexId: string) {
  const [members, invitations] = await Promise.all([
    db.complexUser.findMany({
      where: { complexId },
      include: {
        user: true,
      },
      orderBy: {
        createdAt: 'asc',
      },
    }),
    db.complexInvitation.findMany({
      where: {
        complexId,
        acceptedAt: null,
        revokedAt: null,
        expiresAt: {
          gte: new Date(),
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
    }),
  ]);

  const membersWithAvatars = members.map((member) => ({
    userId: member.userId,
    fullName: member.user.name,
    email: member.user.email,
    avatarUrl: resolveAvatarUrl({
      email: member.user.email,
      image: member.user.image,
    }),
    role: member.role,
    createdAt: member.createdAt.toISOString(),
  }));

  return {
    members: membersWithAvatars,
    invitations: invitations.map(formatInvitation),
  } satisfies ComplexUsersOverview;
}

export async function inviteComplexUser(
  userId: string,
  complexId: string,
  input: InviteComplexUserInput
): Promise<InviteComplexUserResponse> {
  await ensureComplexAdmin(userId, complexId);

  const email = normalizeEmail(input.email);
  const complex = await db.complex.findUnique({
    where: { id: complexId },
    select: {
      complexName: true,
    },
  });

  if (!complex) {
    throw new ComplexMembersError('El complejo no existe.', 404);
  }

  const existingUser = await db.user.findUnique({
    where: { email },
    select: { id: true },
  });

  if (existingUser) {
    const existingMembership = await db.complexUser.findUnique({
      where: {
        complexId_userId: {
          complexId,
          userId: existingUser.id,
        },
      },
      select: { userId: true },
    });

    if (existingMembership) {
      throw new ComplexMembersError('Ese usuario ya pertenece al complejo.', 409);
    }
  }

  const token = createInvitationToken();

  const invitation = await db.$transaction(async (tx) => {
    const pendingInvitation = await tx.complexInvitation.findFirst({
      where: {
        complexId,
        email,
        acceptedAt: null,
        revokedAt: null,
        expiresAt: {
          gte: new Date(),
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
    });

    if (pendingInvitation) {
      const refreshed = await tx.complexInvitation.update({
        where: {
          id: pendingInvitation.id,
        },
        data: {
          tokenHash: hashToken(token),
          expiresAt: nowPlusDays(INVITATION_TTL_DAYS),
          acceptedAt: null,
          revokedAt: null,
        },
      });

      return refreshed;
    }

    return tx.complexInvitation.create({
      data: {
        id: uuidv7(),
        complexId,
        email,
        tokenHash: hashToken(token),
        expiresAt: nowPlusDays(INVITATION_TTL_DAYS),
      },
    });
  });

  await sendInvitationEmail({
    complexName: complex.complexName,
    inviteeEmail: email,
    token,
  });

  return {
    invitation: formatInvitation(invitation),
  };
}

export async function resendComplexInvitation(
  userId: string,
  complexId: string,
  invitationId: string
): Promise<InviteComplexUserResponse> {
  await ensureComplexAdmin(userId, complexId);

  const pendingInvitation = await db.complexInvitation.findUnique({
    where: {
      id: invitationId,
    },
    include: {
      complex: {
        select: {
          complexName: true,
        },
      },
    },
  });

  if (!pendingInvitation || pendingInvitation.complexId !== complexId) {
    throw new ComplexMembersError('La invitación no existe.', 404);
  }

  if (pendingInvitation.acceptedAt) {
    throw new ComplexMembersError('La invitación ya fue aceptada.', 409);
  }

  if (pendingInvitation.revokedAt) {
    throw new ComplexMembersError('La invitación fue cancelada.', 409);
  }

  const refreshed = await refreshPendingInvitation(invitationId);

  await sendInvitationEmail({
    complexName: refreshed.invitation.complex.complexName,
    inviteeEmail: pendingInvitation.email,
    token: refreshed.token,
  });

  return {
    invitation: formatInvitation({
      id: refreshed.invitation.id,
      complexId: refreshed.invitation.complexId,
      email: refreshed.invitation.email,
      expiresAt: refreshed.invitation.expiresAt,
      acceptedAt: refreshed.invitation.acceptedAt,
      revokedAt: refreshed.invitation.revokedAt,
      createdAt: refreshed.invitation.createdAt,
    }),
  };
}

export async function acceptComplexInvitations(
  userId: string,
  email: string,
  input: AcceptComplexInvitationsInput = {}
): Promise<AcceptComplexInvitationsResponse> {
  const normalizedEmail = normalizeEmail(email);
  const tokenHash = input.inviteToken ? hashToken(input.inviteToken) : null;

  const invitations = await db.complexInvitation.findMany({
    where: {
      email: normalizedEmail,
      acceptedAt: null,
      revokedAt: null,
      expiresAt: {
        gte: new Date(),
      },
      ...(tokenHash
        ? {
            tokenHash,
          }
        : {}),
    },
    orderBy: {
      createdAt: 'asc',
    },
  });

  if (tokenHash && invitations.length === 0) {
    throw new ComplexMembersError('La invitación es inválida o ya venció.', 404);
  }

  let acceptedCount = 0;

  for (const invitation of invitations) {
    await db.$transaction(async (tx) => {
      const existingMembership = await tx.complexUser.findUnique({
        where: {
          complexId_userId: {
            complexId: invitation.complexId,
            userId,
          },
        },
        select: {
          userId: true,
        },
      });

      if (!existingMembership) {
        await tx.complexUser.create({
          data: {
            complexId: invitation.complexId,
            userId,
            role: 'EMPLOYEE',
          },
        });
      }

      await tx.complexInvitation.update({
        where: {
          id: invitation.id,
        },
        data: {
          acceptedAt: new Date(),
        },
      });
    });

    acceptedCount += 1;
  }

  return {
    acceptedCount,
  };
}

export async function revokeComplexUser(
  userId: string,
  complexId: string,
  input: RevokeComplexUserInput
) {
  await ensureComplexAdmin(userId, complexId);

  const membership = await db.complexUser.findUnique({
    where: {
      complexId_userId: {
        complexId,
        userId: input.userId,
      },
    },
    select: {
      role: true,
    },
  });

  if (!membership) {
    throw new ComplexMembersError('Ese usuario no pertenece al complejo.', 404);
  }

  if (membership.role !== 'EMPLOYEE') {
    throw new ComplexMembersError('Solo podés revocar usuarios con rol EMPLOYEE.', 409);
  }

  await db.complexUser.delete({
    where: {
      complexId_userId: {
        complexId,
        userId: input.userId,
      },
    },
  });

  return {
    ok: true,
  };
}

export async function cancelComplexInvitation(
  userId: string,
  complexId: string,
  invitationId: string
): Promise<CancelComplexInvitationResponse> {
  await ensureComplexAdmin(userId, complexId);

  const invitation = await db.complexInvitation.findUnique({
    where: {
      id: invitationId,
    },
    select: {
      complexId: true,
      acceptedAt: true,
      revokedAt: true,
    },
  });

  if (!invitation || invitation.complexId !== complexId) {
    throw new ComplexMembersError('La invitación no existe.', 404);
  }

  if (invitation.acceptedAt) {
    throw new ComplexMembersError('La invitación ya fue aceptada.', 409);
  }

  if (invitation.revokedAt) {
    return { ok: true };
  }

  await db.complexInvitation.update({
    where: {
      id: invitationId,
    },
    data: {
      revokedAt: new Date(),
    },
  });

  return { ok: true };
}

export async function getComplexUsersOverview(userId: string, complexId: string) {
  await ensureComplexMember(userId, complexId);
  return listComplexUsers(complexId);
}
