import { randomInt } from 'node:crypto';
import { DayOfWeek as DayOfWeekEnum } from '@/generated/prisma/enums';
import { db } from '@/lib/prisma';
import { isSlotInPast } from '@/lib/slot-validator';
import {
  evaluatePlanUsage,
  isFeatureEnabled,
  parsePlanRules,
} from '@/modules/plan/services/plan-rules.service';
import type {
  CreateRecurringBookingInput,
  RecurringBookingGroup,
  UpdateRecurringGroupInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { AdminBookingServiceError } from './admin-booking.service';

const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
const MAX_RECURRING_WEEKS = 52;

const DAY_INDEX_BY_VALUE: Record<string, number> = {
  SUNDAY: 0,
  MONDAY: 1,
  TUESDAY: 2,
  WEDNESDAY: 3,
  THURSDAY: 4,
  FRIDAY: 5,
  SATURDAY: 6,
};

const DAY_OF_WEEK_BY_INDEX = [
  DayOfWeekEnum.SUNDAY,
  DayOfWeekEnum.MONDAY,
  DayOfWeekEnum.TUESDAY,
  DayOfWeekEnum.WEDNESDAY,
  DayOfWeekEnum.THURSDAY,
  DayOfWeekEnum.FRIDAY,
  DayOfWeekEnum.SATURDAY,
] as const;

type DayOfWeek = (typeof DAY_OF_WEEK_BY_INDEX)[number];

function toMinutes(value: string): number {
  const [hours, minutes] = value.split(':').map((part) => Number(part));
  return hours * 60 + minutes;
}

function minutesToTime(minutes: number): string {
  const safeMinutes = Math.max(0, minutes);
  const hours = Math.floor(safeMinutes / 60);
  const mins = safeMinutes % 60;
  return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}

function parseIsoDate(date: string): Date {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);

  if (!match) {
    throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
  }

  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);

  const parsed = new Date(Date.UTC(year, month - 1, day));

  if (
    Number.isNaN(parsed.getTime()) ||
    parsed.getUTCFullYear() !== year ||
    parsed.getUTCMonth() + 1 !== month ||
    parsed.getUTCDate() !== day
  ) {
    throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
  }

  return parsed;
}

function formatIsoDate(date: Date): string {
  return date.toISOString().slice(0, 10);
}

function getDayOfWeekValue(date: Date): DayOfWeek {
  const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];

  if (!dayOfWeek) {
    throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
  }

  return dayOfWeek;
}

function generateBookingCode(): string {
  let code = '';

  for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
    code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
  }

  return code;
}

function buildSlots(
  availability: Array<{
    startTime: string;
    endTime: string;
  }>,
  slotDurationMinutes: number
) {
  const slots: Array<{ startTime: string; endTime: string }> = [];

  for (const range of availability) {
    const start = toMinutes(range.startTime);
    const end = toMinutes(range.endTime);

    for (
      let current = start;
      current + slotDurationMinutes <= end;
      current += slotDurationMinutes
    ) {
      slots.push({
        startTime: minutesToTime(current),
        endTime: minutesToTime(current + slotDurationMinutes),
      });
    }
  }

  return slots;
}

async function ensureComplexAccess(complexId: string, userId: string) {
  const complexUser = await db.complexUser.findUnique({
    where: {
      complexId_userId: {
        complexId,
        userId,
      },
    },
    include: {
      complex: {
        select: {
          id: true,
          complexName: true,
          plan: {
            select: {
              rules: true,
            },
          },
        },
      },
    },
  });

  if (!complexUser) {
    throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
  }

  return complexUser.complex;
}

function* generateRecurringDates(
  startDate: Date,
  endDate: Date | null,
  dayOfWeek: number
): Generator<Date> {
  const current = new Date(startDate);
  current.setUTCDate(current.getUTCDate() + ((dayOfWeek - current.getUTCDay() + 7) % 7));

  if (current < startDate) {
    current.setUTCDate(current.getUTCDate() + 7);
  }

  const maxDate = endDate ?? new Date(startDate);
  if (!endDate) {
    maxDate.setUTCDate(maxDate.getUTCDate() + MAX_RECURRING_WEEKS * 7);
  }

  while (current <= maxDate) {
    yield new Date(current);
    current.setUTCDate(current.getUTCDate() + 7);
  }
}

function mapBookingResponse(booking: {
  id: string;
  bookingCode: string;
  bookingDate: Date;
  startTime: string;
  endTime: string;
  customerName: string;
  customerPhone: string;
  customerEmail: string;
  status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
  recurringGroupId: string | null;
  createdAt: Date;
  updatedAt: Date;
  court: {
    id: string;
    name: string;
    complex: {
      id: string;
      complexName: string;
    };
    sport: {
      id: string;
      name: string;
      slug: string;
    };
  };
  price?: number;
}) {
  return {
    id: booking.id,
    bookingCode: booking.bookingCode,
    complexId: booking.court.complex.id,
    complexName: booking.court.complex.complexName,
    courtId: booking.court.id,
    courtName: booking.court.name,
    sport: {
      id: booking.court.sport.id,
      name: booking.court.sport.name,
      slug: booking.court.sport.slug,
    },
    date: formatIsoDate(booking.bookingDate),
    startTime: booking.startTime,
    endTime: booking.endTime,
    customerName: booking.customerName,
    customerPhone: booking.customerPhone,
    customerEmail: booking.customerEmail,
    price: booking.price ?? 0,
    status: booking.status,
    recurringGroupId: booking.recurringGroupId,
    createdAt: booking.createdAt.toISOString(),
    updatedAt: booking.updatedAt.toISOString(),
  };
}

export async function createAdminRecurringBooking(
  userId: string,
  complexId: string,
  input: CreateRecurringBookingInput
): Promise<RecurringBookingGroup> {
  const complex = await ensureComplexAccess(complexId, userId);

  const adminUser = await db.user.findUnique({
    where: { id: userId },
    select: { emailVerified: true },
  });

  if (!adminUser?.emailVerified) {
    throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
  }

  if (!complex.plan) {
    throw new AdminBookingServiceError('El complejo no tiene un plan asignado.', 403);
  }

  const rules = parsePlanRules(complex.plan.rules);

  if (!isFeatureEnabled(rules, 'fixedSlots')) {
    throw new AdminBookingServiceError(
      'Tu plan no permite la creación de turnos fijos. Comunicate con el administrador.',
      403
    );
  }

  const startDate = parseIsoDate(input.date);
  const dayOfWeek = getDayOfWeekValue(startDate);
  const endDate = input.recurringEndDate ? parseIsoDate(input.recurringEndDate) : null;

  if (endDate && endDate <= startDate) {
    throw new AdminBookingServiceError(
      'La fecha de fin debe ser posterior a la fecha de inicio.',
      400
    );
  }

  const court = await db.court.findFirst({
    where: {
      id: input.courtId,
      complexId,
    },
    include: {
      availabilities: {
        where: {
          dayOfWeek,
        },
        orderBy: {
          startTime: 'asc',
        },
      },
      sport: {
        select: {
          id: true,
          name: true,
          slug: true,
        },
      },
      priceRules: {
        where: { isActive: true },
        orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
      },
    },
  });

  if (!court) {
    throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
  }

  const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
  const selectedEndMinutes = toMinutes(input.startTime) + court.slotDurationMinutes;
  const selectedSlot = {
    startTime: input.startTime,
    endTime: minutesToTime(selectedEndMinutes),
  };

  const slotExists = validSlots.some(
    (slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
  );

  if (!slotExists) {
    throw new AdminBookingServiceError(
      'El horario seleccionado no esta disponible para esa cancha.',
      409
    );
  }

  if (isSlotInPast(startDate, input.startTime, court.slotDurationMinutes)) {
    throw new AdminBookingServiceError('La fecha de inicio no puede estar en el pasado.', 400);
  }

  const recurringDates = Array.from(
    generateRecurringDates(startDate, endDate, startDate.getUTCDay())
  );

  if (recurringDates.length === 0) {
    throw new AdminBookingServiceError(
      'No se generaron fechas para la reserva periódica. Verifica las fechas ingresadas.',
      400
    );
  }

  for (let attempt = 0; attempt < 5; attempt += 1) {
    try {
      const result = await db.$transaction(async (tx) => {
        const groupId = uuidv7();

        const bookingsForDate = await tx.courtBooking.count({
          where: {
            startTime: selectedSlot.startTime,
            bookingDate: startDate,
            status: 'CONFIRMED',
            court: {
              complexId,
            },
          },
        });

        const courtsCount = await tx.court.count({
          where: { complexId },
        });

        const violations = evaluatePlanUsage(rules, {
          courtsCount,
          bookingsToday: bookingsForDate,
        });

        const maxBookingsViolation = violations.find(
          (v) => v.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
        );

        if (maxBookingsViolation) {
          throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
        }

        const group = await tx.recurringBookingGroup.create({
          data: {
            id: groupId,
            complexId,
            courtId: court.id,
            startTime: selectedSlot.startTime,
            endTime: selectedSlot.endTime,
            dayOfWeek,
            startDate,
            endDate,
            status: 'ACTIVE',
            customerName: input.customerName.trim(),
            customerPhone: input.customerPhone.trim(),
            customerEmail: input.customerEmail?.trim() ?? '',
          },
        });

        const createdBookings = [];

        for (const date of recurringDates) {
          if (isSlotInPast(date, input.startTime, court.slotDurationMinutes)) {
            continue;
          }

          const overlappingBooking = await tx.courtBooking.findFirst({
            where: {
              courtId: court.id,
              bookingDate: date,
              status: 'CONFIRMED',
              startTime: {
                lt: selectedSlot.endTime,
              },
              endTime: {
                gt: selectedSlot.startTime,
              },
            },
          });

          if (overlappingBooking) {
            throw new AdminBookingServiceError(
              `El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
              409
            );
          }

          const booking = await tx.courtBooking.create({
            data: {
              id: uuidv7(),
              bookingCode: generateBookingCode(),
              courtId: court.id,
              bookingDate: date,
              startTime: selectedSlot.startTime,
              endTime: selectedSlot.endTime,
              customerName: input.customerName.trim(),
              customerPhone: input.customerPhone.trim(),
              customerEmail: input.customerEmail?.trim() ?? '',
              status: 'CONFIRMED',
              recurringGroupId: groupId,
            },
            include: {
              court: {
                select: {
                  id: true,
                  name: true,
                  sport: {
                    select: {
                      id: true,
                      name: true,
                      slug: true,
                    },
                  },
                  complex: {
                    select: {
                      id: true,
                      complexName: true,
                    },
                  },
                },
              },
            },
          });

          createdBookings.push(booking);
        }

        return { group, bookings: createdBookings };
      });

      return {
        id: result.group.id,
        complexId,
        courtId: court.id,
        startTime: result.group.startTime,
        endTime: result.group.endTime,
        dayOfWeek: result.group.dayOfWeek,
        startDate: formatIsoDate(result.group.startDate),
        endDate: result.group.endDate ? formatIsoDate(result.group.endDate) : null,
        status: 'ACTIVE' as const,
        customerName: result.group.customerName,
        customerPhone: result.group.customerPhone,
        customerEmail: result.group.customerEmail,
        bookings: result.bookings.map((b) => mapBookingResponse(b)),
        createdAt: result.group.createdAt.toISOString(),
        updatedAt: result.group.updatedAt.toISOString(),
      };
    } catch (error) {
      if (error instanceof AdminBookingServiceError) {
        throw error;
      }

      const prismaError = error as {
        code?: string;
        meta?: {
          target?: string[] | string;
        };
      };

      if (prismaError.code === 'P2002') {
        const targets = Array.isArray(prismaError.meta?.target)
          ? prismaError.meta?.target
          : [prismaError.meta?.target];
        const isBookingCodeCollision = targets.some((target) =>
          String(target).includes('booking_code')
        );

        if (isBookingCodeCollision) {
          continue;
        }

        throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
      }

      throw error;
    }
  }

  throw new AdminBookingServiceError(
    'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
    409
  );
}

export async function cancelRecurringGroup(userId: string, groupId: string) {
  const group = await db.recurringBookingGroup.findUnique({
    where: { id: groupId },
    include: {
      complex: {
        select: {
          id: true,
          users: {
            where: { userId },
            select: { userId: true },
          },
        },
      },
    },
  });

  if (!group) {
    throw new AdminBookingServiceError('Grupo de reservas no encontrado.', 404);
  }

  if (group.complex.users.length === 0) {
    throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
  }

  if (group.status === 'CANCELLED') {
    throw new AdminBookingServiceError('El grupo ya fue cancelado anteriormente.', 409);
  }

  const now = new Date();
  const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));

  await db.$transaction(async (tx) => {
    await tx.recurringBookingGroup.update({
      where: { id: groupId },
      data: { status: 'CANCELLED' },
    });

    const futureBookings = await tx.courtBooking.findMany({
      where: {
        recurringGroupId: groupId,
        bookingDate: { gte: todayStart },
        status: 'CONFIRMED',
      },
    });

    for (const booking of futureBookings) {
      await tx.courtBookingLog.create({
        data: {
          id: uuidv7(),
          bookingCode: booking.bookingCode,
          courtId: booking.courtId,
          bookingDate: booking.bookingDate,
          startTime: booking.startTime,
          endTime: booking.endTime,
          customerName: booking.customerName,
          customerPhone: booking.customerPhone,
          customerEmail: booking.customerEmail,
          previousStatus: booking.status,
          newStatus: 'CANCELLED',
          changedAt: new Date(),
        },
      });

      await tx.courtBooking.delete({
        where: { id: booking.id },
      });
    }
  });

  return { ok: true };
}

export async function listRecurringGroups(complexId: string): Promise<RecurringBookingGroup[]> {
  const groups = await db.recurringBookingGroup.findMany({
    where: { complexId, status: 'ACTIVE' },
    include: {
      court: {
        select: {
          id: true,
          name: true,
          sport: {
            select: { id: true, name: true, slug: true },
          },
          complex: {
            select: { id: true, complexName: true },
          },
        },
      },
      bookings: {
        orderBy: { bookingDate: 'asc' },
        include: {
          court: {
            select: {
              id: true,
              name: true,
              sport: {
                select: { id: true, name: true, slug: true },
              },
              complex: {
                select: { id: true, complexName: true },
              },
            },
          },
        },
      },
    },
    orderBy: { createdAt: 'desc' },
  });

  return groups.map((group) => ({
    id: group.id,
    complexId: group.complexId,
    courtId: group.courtId,
    startTime: group.startTime,
    endTime: group.endTime,
    dayOfWeek: group.dayOfWeek,
    startDate: formatIsoDate(group.startDate),
    endDate: group.endDate ? formatIsoDate(group.endDate) : null,
    status: group.status,
    customerName: group.customerName,
    customerPhone: group.customerPhone,
    customerEmail: group.customerEmail,
    bookings: group.bookings.map(mapBookingResponse),
    createdAt: group.createdAt.toISOString(),
    updatedAt: group.updatedAt.toISOString(),
  }));
}

export async function updateRecurringGroup(
  userId: string,
  groupId: string,
  input: UpdateRecurringGroupInput
): Promise<RecurringBookingGroup> {
  const group = await db.recurringBookingGroup.findUnique({
    where: { id: groupId },
    include: {
      complex: {
        select: {
          id: true,
          users: {
            where: { userId },
            select: { userId: true },
          },
        },
      },
    },
  });

  if (!group) {
    throw new AdminBookingServiceError('Grupo de turnos fijos no encontrado.', 404);
  }

  if (group.complex.users.length === 0) {
    throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
  }

  if (group.status === 'CANCELLED') {
    throw new AdminBookingServiceError('No se puede editar un grupo cancelado.', 409);
  }

  const courtId = input.courtId ?? group.courtId;
  const dayOfWeek = input.dayOfWeek ?? group.dayOfWeek;
  const startTime = input.startTime ?? group.startTime;

  const scheduleChanged =
    input.courtId !== undefined || input.dayOfWeek !== undefined || input.startTime !== undefined;

  if (scheduleChanged) {
    const court = await db.court.findFirst({
      where: { id: courtId, complexId: group.complexId },
      include: {
        availabilities: {
          where: { dayOfWeek: dayOfWeek as DayOfWeek },
          orderBy: { startTime: 'asc' },
        },
      },
    });

    if (!court) {
      throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
    }

    const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
    const selectedEndMinutes = toMinutes(startTime) + court.slotDurationMinutes;
    const endTime = minutesToTime(selectedEndMinutes);

    const slotExists = validSlots.some(
      (slot) => slot.startTime === startTime && slot.endTime === endTime
    );

    if (!slotExists) {
      throw new AdminBookingServiceError(
        'El horario seleccionado no esta disponible para esa cancha.',
        409
      );
    }

    const now = new Date();
    const todayStart = new Date(
      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
    );

    await db.$transaction(async (tx) => {
      await tx.recurringBookingGroup.update({
        where: { id: groupId },
        data: {
          courtId,
          dayOfWeek: dayOfWeek as DayOfWeek,
          startTime,
          endTime,
          customerName: input.customerName?.trim() ?? group.customerName,
          customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
          customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
        },
      });

      const futureBookings = await tx.courtBooking.findMany({
        where: {
          recurringGroupId: groupId,
          bookingDate: { gte: todayStart },
          status: 'CONFIRMED',
        },
      });

      for (const booking of futureBookings) {
        await tx.courtBookingLog.create({
          data: {
            id: uuidv7(),
            bookingCode: booking.bookingCode,
            courtId: booking.courtId,
            bookingDate: booking.bookingDate,
            startTime: booking.startTime,
            endTime: booking.endTime,
            customerName: booking.customerName,
            customerPhone: booking.customerPhone,
            customerEmail: booking.customerEmail,
            previousStatus: booking.status,
            newStatus: 'CANCELLED',
            changedAt: new Date(),
          },
        });

        await tx.courtBooking.delete({
          where: { id: booking.id },
        });
      }

      const dayIndex = DAY_INDEX_BY_VALUE[dayOfWeek] ?? 0;
      const recurringDates = Array.from(
        generateRecurringDates(todayStart, group.endDate, dayIndex)
      );

      for (const date of recurringDates) {
        const overlappingBooking = await tx.courtBooking.findFirst({
          where: {
            courtId,
            bookingDate: date,
            status: 'CONFIRMED',
            startTime: { lt: endTime },
            endTime: { gt: startTime },
          },
        });

        if (overlappingBooking) {
          throw new AdminBookingServiceError(
            `El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`,
            409
          );
        }

        await tx.courtBooking.create({
          data: {
            id: uuidv7(),
            bookingCode: generateBookingCode(),
            courtId,
            bookingDate: date,
            startTime,
            endTime,
            customerName: input.customerName?.trim() ?? group.customerName,
            customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
            customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
            status: 'CONFIRMED',
            recurringGroupId: groupId,
          },
          include: {
            court: {
              select: {
                id: true,
                name: true,
                sport: { select: { id: true, name: true, slug: true } },
                complex: { select: { id: true, complexName: true } },
              },
            },
          },
        });
      }
    });
  } else {
    await db.recurringBookingGroup.update({
      where: { id: groupId },
      data: {
        ...(input.customerName !== undefined && { customerName: input.customerName.trim() }),
        ...(input.customerPhone !== undefined && { customerPhone: input.customerPhone.trim() }),
        ...(input.customerEmail !== undefined && { customerEmail: input.customerEmail.trim() }),
      },
    });

    const now = new Date();
    const todayStart = new Date(
      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
    );

    const updateData: Record<string, string> = {};
    if (input.customerName !== undefined) updateData.customerName = input.customerName.trim();
    if (input.customerPhone !== undefined) updateData.customerPhone = input.customerPhone.trim();
    if (input.customerEmail !== undefined) updateData.customerEmail = input.customerEmail.trim();

    if (Object.keys(updateData).length > 0) {
      await db.courtBooking.updateMany({
        where: { recurringGroupId: groupId, bookingDate: { gte: todayStart } },
        data: updateData,
      });
    }
  }

  const updated = await db.recurringBookingGroup.findUnique({
    where: { id: groupId },
    include: {
      court: {
        select: {
          id: true,
          name: true,
          sport: { select: { id: true, name: true, slug: true } },
          complex: { select: { id: true, complexName: true } },
        },
      },
      bookings: {
        orderBy: { bookingDate: 'asc' },
        include: {
          court: {
            select: {
              id: true,
              name: true,
              sport: { select: { id: true, name: true, slug: true } },
              complex: { select: { id: true, complexName: true } },
            },
          },
        },
      },
    },
  });

  if (!updated) {
    throw new AdminBookingServiceError('Error al actualizar el grupo.', 409);
  }

  return {
    id: updated.id,
    complexId: updated.complexId,
    courtId: updated.courtId,
    startTime: updated.startTime,
    endTime: updated.endTime,
    dayOfWeek: updated.dayOfWeek,
    startDate: formatIsoDate(updated.startDate),
    endDate: updated.endDate ? formatIsoDate(updated.endDate) : null,
    status: updated.status,
    customerName: updated.customerName,
    customerPhone: updated.customerPhone,
    customerEmail: updated.customerEmail,
    bookings: updated.bookings.map(mapBookingResponse),
    createdAt: updated.createdAt.toISOString(),
    updatedAt: updated.updatedAt.toISOString(),
  };
}
