import { randomInt } from 'node:crypto';
import { db } from '@/lib/prisma';
import { isSlotInPast } from '@/lib/slot-validator';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type {
  CancelPublicBookingInput,
  CreatePublicBookingInput,
  DayOfWeek,
  PublicAvailabilityQuery,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

type Slot = {
  startTime: string;
  endTime: string;
};

type PriceableCourt = {
  basePrice: unknown;
  priceRules: Array<{
    dayOfWeek: DayOfWeek | null;
    startTime: string | null;
    endTime: string | null;
    price: unknown;
  }>;
};

type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>;

const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
  'SUNDAY',
  'MONDAY',
  'TUESDAY',
  'WEDNESDAY',
  'THURSDAY',
  'FRIDAY',
  'SATURDAY',
];
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;

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

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

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): { bookingDate: Date; dayOfWeek: DayOfWeek } {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);

  if (!match) {
    throw new PublicBookingServiceError('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 bookingDate = new Date(Date.UTC(year, month - 1, day));

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

  const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()];

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

  return { bookingDate, dayOfWeek };
}

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

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 hasOverlap(slot: Slot, existing: Slot): boolean {
  return slot.startTime < existing.endTime && slot.endTime > existing.startTime;
}

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

  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;
}

function resolveSports(data: ComplexWithPublicBookingData) {
  const map = new Map<string, { id: string; name: string; slug: string }>();

  for (const court of data.courts) {
    if (!court.sport.isActive) {
      continue;
    }

    if (!map.has(court.sport.id)) {
      map.set(court.sport.id, {
        id: court.sport.id,
        name: court.sport.name,
        slug: court.sport.slug,
      });
    }
  }

  return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'es'));
}

function validateSportSelection(
  availableSports: Array<{ id: string }>,
  sportId: string | undefined
): { sportSelectionRequired: boolean; selectedSportId: string | undefined } {
  const sportSelectionRequired = availableSports.length > 1;

  if (sportSelectionRequired && !sportId) {
    throw new PublicBookingServiceError(
      'Debes seleccionar un deporte para consultar disponibilidad.',
      400
    );
  }

  if (sportId && !availableSports.some((sport) => sport.id === sportId)) {
    throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
  }

  if (sportSelectionRequired) {
    return {
      sportSelectionRequired,
      selectedSportId: sportId,
    };
  }

  return {
    sportSelectionRequired,
    selectedSportId: sportId ?? availableSports[0]?.id,
  };
}

async function getComplexWithBookingData(complexSlug: string) {
  const complex = await db.complex.findUnique({
    where: { complexSlug },
    select: {
      id: true,
      complexName: true,
      complexSlug: true,
      physicalAddress: true,
      plan: {
        select: {
          rules: true,
        },
      },
      courts: {
        include: {
          sport: {
            select: {
              id: true,
              name: true,
              slug: true,
              isActive: true,
            },
          },
          availabilities: {
            orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
          },
          priceRules: {
            where: { isActive: true },
            orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
          },
        },
        orderBy: { createdAt: 'asc' },
      },
    },
  });

  if (!complex) {
    throw new PublicBookingServiceError('Complejo no encontrado.', 404);
  }

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

    if (!rules.features.publicBookingPage) {
      throw new PublicBookingServiceError(
        'El complejo no tiene habilitada la reserva publica.',
        403
      );
    }
  }

  return complex;
}

function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number {
  const slotStart = toMinutes(slot.startTime);
  const slotEnd = toMinutes(slot.endTime);
  const matchingRules = court.priceRules
    .filter((rule) => {
      if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) {
        return false;
      }

      if (!rule.startTime || !rule.endTime) {
        return true;
      }

      return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
    })
    .sort((first, second) => {
      const firstSpecificity =
        (first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
      const secondSpecificity =
        (second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);

      return secondSpecificity - firstSpecificity;
    });

  return Number(matchingRules[0]?.price ?? court.basePrice);
}

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

export async function getPublicBookingConfirmation(complexSlug: string, bookingCode: string) {
  const normalizedBookingCode = bookingCode.toUpperCase();
  const booking = await db.courtBooking.findFirst({
    where: {
      bookingCode: normalizedBookingCode,
      court: {
        complex: {
          complexSlug,
        },
      },
    },
    select: {
      bookingCode: true,
      bookingDate: true,
      startTime: true,
      endTime: true,
      customerEmail: true,
      status: true,
      createdAt: true,
      court: {
        select: {
          name: true,
          basePrice: true,
          priceRules: {
            where: { isActive: true },
            orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
          },
          sport: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          complex: {
            select: {
              complexName: true,
              physicalAddress: true,
              complexSlug: true,
            },
          },
        },
      },
    },
  });

  if (!booking) {
    throw new PublicBookingServiceError('Reserva no encontrada.', 404);
  }

  const date = formatIsoDate(booking.bookingDate);
  const { dayOfWeek } = parseIsoDate(date);
  const price = resolveSlotPrice(booking.court, dayOfWeek, {
    startTime: booking.startTime,
    endTime: booking.endTime,
  });

  return {
    bookingCode: booking.bookingCode,
    complexName: booking.court.complex.complexName,
    complexAddress: booking.court.complex.physicalAddress ?? undefined,
    complexSlug: booking.court.complex.complexSlug,
    date,
    startTime: booking.startTime,
    endTime: booking.endTime,
    price,
    courtName: booking.court.name,
    sport: {
      id: booking.court.sport.id,
      name: booking.court.sport.name,
      slug: booking.court.sport.slug,
    },
    status: booking.status,
    customerEmail: booking.customerEmail,
    createdAt: booking.createdAt.toISOString(),
  };
}

async function hasVerifiedAdmin(complexId: string): Promise<boolean> {
  const verifiedAdmin = await db.complexUser.findFirst({
    where: {
      complexId,
      role: 'ADMIN',
      user: { emailVerified: true },
    },
  });
  return verifiedAdmin !== null;
}

export async function listPublicAvailability(complexSlug: string, query: PublicAvailabilityQuery) {
  const { bookingDate, dayOfWeek } = parseIsoDate(query.date);
  const complex = await getComplexWithBookingData(complexSlug);

  if (!(await hasVerifiedAdmin(complex.id))) {
    return {
      complexId: complex.id,
      complexName: complex.complexName,
      complexAddress: complex.physicalAddress ?? null,
      complexSlug: complex.complexSlug,
      date: query.date,
      sportSelectionRequired: false,
      sports: [],
      courts: [],
    };
  }

  const sports = resolveSports(complex);
  const sportSelectionRequired = sports.length > 1;

  if (query.sportId && !sports.some((sport) => sport.id === query.sportId)) {
    throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
  }

  const selectedSportId = sportSelectionRequired ? query.sportId : (query.sportId ?? sports[0]?.id);

  const candidateCourts = complex.courts.filter((court) => {
    if (!court.sport.isActive) {
      return false;
    }

    if (selectedSportId) {
      return court.sportId === selectedSportId;
    }

    return true;
  });

  const courtIds = candidateCourts.map((court) => court.id);

  const bookings =
    courtIds.length > 0
      ? await db.courtBooking.findMany({
          where: {
            bookingDate,
            status: 'CONFIRMED',
            courtId: {
              in: courtIds,
            },
          },
          select: {
            courtId: true,
            startTime: true,
            endTime: true,
          },
        })
      : [];

  const bookingsByCourt = new Map<string, Slot[]>();

  for (const booking of bookings) {
    const current = bookingsByCourt.get(booking.courtId) ?? [];
    current.push({
      startTime: booking.startTime,
      endTime: booking.endTime,
    });
    bookingsByCourt.set(booking.courtId, current);
  }

  const courts = candidateCourts
    .map((court) => {
      const dayAvailability = court.availabilities.filter(
        (availability) => availability.dayOfWeek === dayOfWeek
      );

      const allSlots = buildSlots(dayAvailability, court.slotDurationMinutes);
      const occupiedSlots = bookingsByCourt.get(court.id) ?? [];

      const availableSlots = allSlots.filter(
        (slot) => !occupiedSlots.some((occupied) => hasOverlap(slot, occupied))
      );

      return {
        courtId: court.id,
        courtName: court.name,
        sport: {
          id: court.sport.id,
          name: court.sport.name,
          slug: court.sport.slug,
        },
        slotDurationMinutes: court.slotDurationMinutes,
        availabilityDay: dayOfWeek,
        availableSlots: availableSlots.map((slot) => ({
          ...slot,
          price: resolveSlotPrice(court, dayOfWeek, slot),
        })),
      };
    })
    .filter((court) => court.availableSlots.length > 0);

  return {
    complexId: complex.id,
    complexName: complex.complexName,
    complexAddress: complex.physicalAddress ?? null,
    complexSlug: complex.complexSlug,
    date: query.date,
    sportSelectionRequired,
    sports,
    courts,
  };
}

export async function createPublicBooking(complexSlug: string, input: CreatePublicBookingInput) {
  const { bookingDate, dayOfWeek } = parseIsoDate(input.date);
  const complex = await getComplexWithBookingData(complexSlug);

  if (!(await hasVerifiedAdmin(complex.id))) {
    throw new PublicBookingServiceError(
      'El complejo no puede recibir reservas hasta que un administrador verifique su email.',
      403
    );
  }

  const sports = resolveSports(complex);
  const { sportSelectionRequired } = validateSportSelection(sports, input.sportId);

  const selectedCourt = complex.courts.find(
    (court) => court.id === input.courtId && court.sport.isActive
  );

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

  if (sportSelectionRequired && !input.sportId) {
    throw new PublicBookingServiceError(
      'Debes seleccionar un deporte para reservar en este complejo.',
      400
    );
  }

  if (input.sportId && input.sportId !== selectedCourt.sportId) {
    throw new PublicBookingServiceError(
      'La cancha seleccionada no corresponde al deporte indicado.',
      400
    );
  }

  const dayAvailability = selectedCourt.availabilities.filter(
    (availability) => availability.dayOfWeek === dayOfWeek
  );

  const validSlots = buildSlots(dayAvailability, selectedCourt.slotDurationMinutes);
  const selectedStartMinutes = toMinutes(input.startTime);
  const selectedEndMinutes = selectedStartMinutes + selectedCourt.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 PublicBookingServiceError(
      'El horario seleccionado no esta disponible para esa cancha.',
      409
    );
  }

  if (isSlotInPast(bookingDate, input.startTime, selectedCourt.slotDurationMinutes)) {
    throw new PublicBookingServiceError('No se pueden crear reservas en el pasado.', 400);
  }

  for (let attempt = 0; attempt < 5; attempt += 1) {
    try {
      const booking = await db.$transaction(async (tx) => {
        if (complex.plan) {
          const rules = parsePlanRules(complex.plan.rules);
          const bookingsForDate = await tx.courtBooking.count({
            where: {
              bookingDate,
              status: 'CONFIRMED',
              court: {
                complexId: complex.id,
              },
            },
          });

          const violations = evaluatePlanUsage(rules, {
            courtsCount: complex.courts.length,
            bookingsToday: bookingsForDate,
          });

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

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

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

        if (overlappingBooking) {
          throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
        }

        return tx.courtBooking.create({
          data: {
            id: uuidv7(),
            bookingCode: generateBookingCode(),
            courtId: selectedCourt.id,
            bookingDate,
            startTime: selectedSlot.startTime,
            endTime: selectedSlot.endTime,
            customerName: input.customerName.trim(),
            customerPhone: input.customerPhone.trim(),
            customerEmail: input.customerEmail.trim(),
            status: 'CONFIRMED',
          },
          select: {
            id: true,
            bookingCode: true,
            bookingDate: true,
            createdAt: true,
            startTime: true,
            endTime: true,
            customerName: true,
            customerPhone: true,
            customerEmail: true,
            status: true,
          },
        });
      });

      const price = resolveSlotPrice(selectedCourt, dayOfWeek, selectedSlot);

      return mapBookingResponse({
        bookingId: booking.id,
        bookingCode: booking.bookingCode,
        bookingDate: booking.bookingDate,
        createdAt: booking.createdAt,
        startTime: booking.startTime,
        endTime: booking.endTime,
        customerName: booking.customerName,
        customerPhone: booking.customerPhone,
        customerEmail: booking.customerEmail,
        price,
        status: booking.status,
        court: {
          id: selectedCourt.id,
          name: selectedCourt.name,
          complexId: complex.id,
          complexName: complex.complexName,
          complexSlug: complex.complexSlug,
          sport: {
            id: selectedCourt.sport.id,
            name: selectedCourt.sport.name,
            slug: selectedCourt.sport.slug,
          },
        },
      });
    } catch (error) {
      if (error instanceof PublicBookingServiceError) {
        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 PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
      }

      throw error;
    }
  }

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

export async function cancelPublicBooking(complexSlug: string, input: CancelPublicBookingInput) {
  const normalizedBookingCode = input.bookingCode.toUpperCase();
  const booking = await db.courtBooking.findFirst({
    where: {
      bookingCode: normalizedBookingCode,
      court: {
        complex: {
          complexSlug,
        },
      },
    },
    include: {
      court: {
        select: {
          id: true,
          name: true,
          basePrice: true,
          priceRules: {
            where: { isActive: true },
            orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
          },
          sport: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          complex: {
            select: {
              id: true,
              complexName: true,
              complexSlug: true,
            },
          },
        },
      },
    },
  });

  if (!booking) {
    throw new PublicBookingServiceError('Reserva no encontrada.', 404);
  }

  if (booking.status !== 'CONFIRMED') {
    throw new PublicBookingServiceError(
      'Solo se pueden cancelar reservas en estado confirmada.',
      409
    );
  }

  if (booking.customerPhone !== input.customerPhone.trim()) {
    throw new PublicBookingServiceError('Los datos ingresados no coinciden con la reserva.', 400);
  }

  const date = formatIsoDate(booking.bookingDate);
  const { dayOfWeek } = parseIsoDate(date);
  const price = resolveSlotPrice(booking.court, dayOfWeek, {
    startTime: booking.startTime,
    endTime: booking.endTime,
  });

  await db.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',
    },
  });

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

  return mapBookingResponse({
    bookingId: booking.id,
    bookingCode: booking.bookingCode,
    bookingDate: booking.bookingDate,
    createdAt: booking.createdAt,
    startTime: booking.startTime,
    endTime: booking.endTime,
    customerName: booking.customerName,
    customerPhone: booking.customerPhone,
    customerEmail: booking.customerEmail,
    price,
    status: 'CANCELLED',
    court: {
      id: booking.court.id,
      name: booking.court.name,
      complexId: booking.court.complex.id,
      complexName: booking.court.complex.complexName,
      complexSlug: booking.court.complex.complexSlug,
      sport: booking.court.sport,
    },
  });
}
