import type { Court, CourtAvailability, CourtPriceRule, Sport } from '@/generated/prisma/client';
import { db } from '@/lib/prisma';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { CreateCourtInput, DayOfWeek, UpdateCourtInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';

type CourtWithRelations = Court & {
  sport: Sport;
  availabilities: CourtAvailability[];
  priceRules: CourtPriceRule[];
};

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

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

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

function assertAvailabilityRanges(
  availability: Array<{
    dayOfWeek: DayOfWeek;
    startTime: string;
    endTime: string;
  }>
) {
  const grouped = new Map<DayOfWeek, Array<{ start: number; end: number }>>();

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

    if (start >= end) {
      throw new CourtServiceError(`El rango ${range.startTime}-${range.endTime} es invalido.`, 400);
    }

    const current = grouped.get(range.dayOfWeek) ?? [];
    current.push({ start, end });
    grouped.set(range.dayOfWeek, current);
  }

  for (const ranges of grouped.values()) {
    ranges.sort((a, b) => a.start - b.start);

    for (let index = 1; index < ranges.length; index += 1) {
      const previous = ranges[index - 1];
      const current = ranges[index];

      if (previous.end > current.start) {
        throw new CourtServiceError('Hay rangos horarios superpuestos para el mismo dia.', 400);
      }
    }
  }
}

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

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

  return complexUser.complex;
}

async function ensureActiveSport(sportId: string) {
  const sport = await db.sport.findFirst({
    where: {
      id: sportId,
      isActive: true,
    },
    select: { id: true },
  });

  if (!sport) {
    throw new CourtServiceError('El deporte seleccionado no existe o esta inactivo.', 400);
  }
}

async function enforcePlanCourtLimit(complexId: string) {
  const complex = await db.complex.findUnique({
    where: { id: complexId },
    select: {
      plan: {
        select: {
          rules: true,
        },
      },
    },
  });

  if (!complex?.plan) {
    return;
  }

  const rules = parsePlanRules(complex.plan.rules);
  const courtsCount = await db.court.count({
    where: { complexId },
  });

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

  const maxCourtViolation = violations.find((violation) => violation.code === 'MAX_COURTS_REACHED');

  if (maxCourtViolation) {
    throw new CourtServiceError(maxCourtViolation.message, 409);
  }
}

async function getCourtByIdForUser(courtId: string, userId: string) {
  return db.court.findFirst({
    where: {
      id: courtId,
      complex: {
        users: {
          some: {
            userId,
          },
        },
      },
    },
    include: {
      sport: true,
      availabilities: {
        orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
      },
      priceRules: {
        where: { isActive: true },
        orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
      },
    },
  });
}

function mapCourtResponse(court: CourtWithRelations) {
  return {
    id: court.id,
    complexId: court.complexId,
    name: court.name,
    sportId: court.sportId,
    sport: {
      id: court.sport.id,
      name: court.sport.name,
      slug: court.sport.slug,
    },
    slotDurationMinutes: court.slotDurationMinutes,
    basePrice: Number(court.basePrice),
    availability: court.availabilities.map((availability) => ({
      id: availability.id,
      dayOfWeek: availability.dayOfWeek,
      startTime: availability.startTime,
      endTime: availability.endTime,
    })),
    priceRules: court.priceRules.map((rule) => ({
      id: rule.id,
      dayOfWeek: rule.dayOfWeek,
      startTime: rule.startTime,
      endTime: rule.endTime,
      price: Number(rule.price),
      isActive: rule.isActive,
    })),
    hasCustomPricing: court.priceRules.length > 0,
    createdAt: court.createdAt.toISOString(),
    updatedAt: court.updatedAt.toISOString(),
  };
}

export async function listCourtsByComplex(complexId: string, userId: string) {
  await ensureComplexAccess(complexId, userId);

  const courts = await db.court.findMany({
    where: { complexId },
    include: {
      sport: true,
      availabilities: {
        orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
      },
      priceRules: {
        where: { isActive: true },
        orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
      },
    },
    orderBy: {
      createdAt: 'asc',
    },
  });

  return courts.map((court) => mapCourtResponse(court));
}

export async function createCourt(userId: string, complexId: string, input: CreateCourtInput) {
  await ensureComplexAccess(complexId, userId);
  await ensureActiveSport(input.sportId);
  await enforcePlanCourtLimit(complexId);
  assertAvailabilityRanges(input.availability);

  const createdCourt = await db.$transaction(async (tx) => {
    const court = await tx.court.create({
      data: {
        id: uuidv7(),
        complexId,
        sportId: input.sportId,
        name: input.name.trim(),
        slotDurationMinutes: input.slotDurationMinutes,
        basePrice: input.basePrice,
      },
    });

    await tx.courtAvailability.createMany({
      data: input.availability.map((availability) => ({
        id: uuidv7(),
        courtId: court.id,
        dayOfWeek: availability.dayOfWeek,
        startTime: availability.startTime,
        endTime: availability.endTime,
      })),
    });

    return court;
  });

  const court = await getCourtByIdForUser(createdCourt.id, userId);

  if (!court) {
    throw new CourtServiceError('No se pudo obtener la cancha creada.', 404);
  }

  return mapCourtResponse(court);
}

export async function updateCourt(userId: string, courtId: string, input: UpdateCourtInput) {
  const existingCourt = await getCourtByIdForUser(courtId, userId);

  if (!existingCourt) {
    throw new CourtServiceError('Cancha no encontrada.', 404);
  }

  if (input.sportId) {
    await ensureActiveSport(input.sportId);
  }

  if (input.availability) {
    assertAvailabilityRanges(input.availability);
  }

  await db.$transaction(async (tx) => {
    await tx.court.update({
      where: {
        id: courtId,
      },
      data: {
        ...(input.name !== undefined ? { name: input.name.trim() } : {}),
        ...(input.sportId !== undefined ? { sportId: input.sportId } : {}),
        ...(input.slotDurationMinutes !== undefined
          ? { slotDurationMinutes: input.slotDurationMinutes }
          : {}),
        ...(input.basePrice !== undefined ? { basePrice: input.basePrice } : {}),
      },
    });

    if (input.availability) {
      await tx.courtAvailability.deleteMany({
        where: { courtId },
      });

      await tx.courtAvailability.createMany({
        data: input.availability.map((availability) => ({
          id: uuidv7(),
          courtId,
          dayOfWeek: availability.dayOfWeek,
          startTime: availability.startTime,
          endTime: availability.endTime,
        })),
      });
    }
  });

  const updatedCourt = await getCourtByIdForUser(courtId, userId);

  if (!updatedCourt) {
    throw new CourtServiceError('Cancha no encontrada.', 404);
  }

  return mapCourtResponse(updatedCourt);
}
