import { randomInt } from 'node:crypto';
import { CourtBookingStatus } from '@/generated/prisma/enums';
import { db } from '@/lib/prisma';
import { isSlotInPast } from '@/lib/slot-validator';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { DayOfWeek } from '@repo/api-contract';
import type {
  AdminBooking,
  CreateAdminBookingInput,
  ListAdminBookingsQuery,
  UpdateAdminBookingStatusInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

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

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

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

  constructor(message: string, status: 400 | 403 | 404 | 409) {
    super(message);
    this.name = 'AdminBookingServiceError';
    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): 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 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 AdminBookingServiceError('La fecha enviada no es valida.', 400);
  }

  return bookingDate;
}

function getDayOfWeek(date: Date) {
  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 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 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;
}

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 resolvePrice(
  court: {
    basePrice: unknown;
    priceRules: Array<{
      dayOfWeek: string | null;
      startTime: string | null;
      endTime: string | null;
      price: unknown;
    }>;
  },
  dayOfWeek: string,
  startTime: string,
  endTime: string
): number {
  const slotStart = toMinutes(startTime);
  const slotEnd = toMinutes(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(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;
}): AdminBooking {
  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 listAdminBookings(
  userId: string,
  complexId: string,
  query: ListAdminBookingsQuery
) {
  await ensureComplexAccess(complexId, userId);
  const fromDate = parseIsoDate(query.fromDate);

  const bookings = await db.courtBooking.findMany({
    where: {
      bookingDate: {
        gte: fromDate,
      },
      court: {
        complexId,
      },
    },
    include: {
      court: {
        select: {
          id: true,
          name: true,
          sport: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          complex: {
            select: {
              id: true,
              complexName: true,
            },
          },
        },
      },
    },
    orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
  });

  const response = bookings.map((booking) => mapBookingResponse(booking));

  return {
    bookings: response,
  };
}

export async function createAdminBooking(
  userId: string,
  complexId: string,
  input: CreateAdminBookingInput
) {
  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);
  }

  const bookingDate = parseIsoDate(input.date);
  const dayOfWeek = getDayOfWeek(bookingDate);

  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 selectedStartMinutes = toMinutes(input.startTime);
  const selectedEndMinutes = selectedStartMinutes + 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(bookingDate, input.startTime, court.slotDurationMinutes)) {
    throw new AdminBookingServiceError('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,
              },
            },
          });

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

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

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

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

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

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

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

      const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);

      return mapBookingResponse({ ...booking, price });
    } 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 updateAdminBookingStatus(
  userId: string,
  bookingId: string,
  input: UpdateAdminBookingStatusInput
) {
  const booking = await db.courtBooking.findFirst({
    where: {
      id: bookingId,
      court: {
        complex: {
          users: {
            some: {
              userId,
            },
          },
        },
      },
    },
    include: {
      court: {
        select: {
          id: true,
          name: true,
          sport: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          complex: {
            select: {
              id: true,
              complexName: true,
            },
          },
        },
      },
    },
  });

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

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

  if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
    throw new AdminBookingServiceError(
      'Solo se pueden marcar como cumplidas las reservas confirmadas.',
      409
    );
  }

  if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
    throw new AdminBookingServiceError(
      'Solo se pueden marcar como no show las reservas confirmadas.',
      409
    );
  }

  await db.courtBookingLog.create({
    data: {
      id: uuidv7(),
      bookingCode: booking.bookingCode,
      courtId: booking.court.id,
      bookingDate: booking.bookingDate,
      startTime: booking.startTime,
      endTime: booking.endTime,
      customerName: booking.customerName,
      customerPhone: booking.customerPhone,
      customerEmail: booking.customerEmail,
      previousStatus: booking.status,
      newStatus: input.status,
      changedAt: new Date(),
    },
  });

  if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
    await db.courtBooking.update({
      where: { id: booking.id },
      data: {
        status: input.status,
      },
    });
  } else {
    // if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
    await db.courtBooking.delete({
      where: { id: booking.id },
    });
  }

  return mapBookingResponse({ ...booking, status: input.status });
}
