import { listAssignmentBookings } from "@capakraken/application";
import { type WeekdayAvailability } from "@capakraken/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  countEffectiveWorkingDays,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { planningReadProcedure } from "../trpc.js";
import {
  ACTIVE_STATUSES,
  averagePerWorkingDay,
  calculateAllocatedHoursForDay,
  getEffectiveDayAvailability,
  round1,
  toIsoDate,
} from "./staffing-shared.js";

export const staffingCapacityReadProcedures = {
  searchCapacity: planningReadProcedure
    .input(
      z.object({
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        minHoursPerDay: z.number().optional().default(4),
        roleName: z.string().optional(),
        chapter: z.string().optional(),
        limit: z.number().int().min(1).max(100).optional().default(20),
      }),
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = { isActive: true };
      if (input.roleName) {
        where.areaRole = { name: { contains: input.roleName, mode: "insensitive" } };
      }
      if (input.chapter) {
        where.chapter = { contains: input.chapter, mode: "insensitive" };
      }

      const resources = await ctx.db.resource.findMany({
        where,
        select: {
          id: true,
          displayName: true,
          eid: true,
          fte: true,
          availability: true,
          countryId: true,
          federalState: true,
          metroCityId: true,
          country: { select: { code: true } },
          metroCity: { select: { name: true } },
          areaRole: { select: { name: true } },
          chapter: true,
        },
        take: 100,
      });

      const bookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: resources.map((resource) => resource.id),
      });

      const bookingsByResourceId = new Map<string, typeof bookings>();
      for (const booking of bookings) {
        if (!booking.resourceId) {
          continue;
        }
        const existing = bookingsByResourceId.get(booking.resourceId) ?? [];
        existing.push(booking);
        bookingsByResourceId.set(booking.resourceId, existing);
      }

      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        resources.map((resource) => ({
          id: resource.id,
          availability: resource.availability as unknown as WeekdayAvailability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        })),
        input.startDate,
        input.endDate,
      );

      const results = resources
        .map((resource) => {
          const availability = resource.availability as unknown as WeekdayAvailability;
          const context = contexts.get(resource.id);
          const workingDays = countEffectiveWorkingDays({
            availability,
            periodStart: input.startDate,
            periodEnd: input.endDate,
            context,
          });
          const availableHours = calculateEffectiveAvailableHours({
            availability,
            periodStart: input.startDate,
            periodEnd: input.endDate,
            context,
          });
          const bookedHours = (bookingsByResourceId.get(resource.id) ?? []).reduce(
            (sum, booking) =>
              sum + calculateEffectiveBookedHours({
                availability,
                startDate: booking.startDate,
                endDate: booking.endDate,
                hoursPerDay: booking.hoursPerDay,
                periodStart: input.startDate,
                periodEnd: input.endDate,
                context,
              }),
            0,
          );
          const remainingHours = Math.max(0, availableHours - bookedHours);

          return {
            id: resource.id,
            name: resource.displayName,
            eid: resource.eid,
            role: resource.areaRole?.name ?? null,
            chapter: resource.chapter,
            workingDays,
            availableHours: round1(remainingHours),
            availableHoursPerDay: averagePerWorkingDay(remainingHours, workingDays),
          };
        })
        .filter((resource) => resource.availableHoursPerDay >= input.minHoursPerDay)
        .sort((left, right) => right.availableHours - left.availableHours)
        .slice(0, input.limit);

      return {
        period: `${toIsoDate(input.startDate)} to ${toIsoDate(input.endDate)}`,
        minHoursFilter: input.minHoursPerDay,
        results,
        totalFound: results.length,
      };
    }),

  analyzeUtilization: planningReadProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const resource = await findUniqueOrThrow(
        ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: {
            id: true,
            displayName: true,
            chargeabilityTarget: true,
            availability: true,
            countryId: true,
            federalState: true,
            metroCityId: true,
            country: { select: { code: true } },
            metroCity: { select: { name: true } },
          },
        }),
        "Resource",
      );

      const resourceBookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: [resource.id],
      });

      const availability = resource.availability as unknown as WeekdayAvailability;
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        [{
          id: resource.id,
          availability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        }],
        input.startDate,
        input.endDate,
      );
      const context = contexts.get(resource.id);
      const activeBookings = resourceBookings.map((booking) => ({
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        status: booking.status,
        projectName: booking.project.name,
        isChargeable: booking.project.orderType === "CHARGEABLE",
      }));

      const overallocatedDays: string[] = [];
      const underutilizedDays: string[] = [];
      let totalAvailableHours = 0;
      let totalChargeableHours = 0;
      const cursor = new Date(input.startDate);
      cursor.setUTCHours(0, 0, 0, 0);
      const end = new Date(input.endDate);
      end.setUTCHours(0, 0, 0, 0);

      while (cursor <= end) {
        const availableHoursForDay = getEffectiveDayAvailability(availability, cursor, context);
        if (availableHoursForDay > 0) {
          const { allocatedHours, chargeableHours } = calculateAllocatedHoursForDay({
            bookings: activeBookings,
            date: cursor,
            context,
          });
          totalAvailableHours += availableHoursForDay;
          totalChargeableHours += chargeableHours;

          if (allocatedHours > availableHoursForDay) {
            overallocatedDays.push(toIsoDate(cursor));
          } else if (allocatedHours < availableHoursForDay * 0.5) {
            underutilizedDays.push(toIsoDate(cursor));
          }
        }

        cursor.setUTCDate(cursor.getUTCDate() + 1);
      }

      const currentChargeability = totalAvailableHours > 0
        ? (totalChargeableHours / totalAvailableHours) * 100
        : 0;

      return {
        resourceId: resource.id,
        resourceName: resource.displayName,
        chargeabilityTarget: resource.chargeabilityTarget,
        currentChargeability,
        chargeabilityGap: resource.chargeabilityTarget - currentChargeability,
        allocations: activeBookings
          .filter((booking) => ACTIVE_STATUSES.has(booking.status))
          .map((booking) => ({
            startDate: booking.startDate,
            endDate: booking.endDate,
            hoursPerDay: booking.hoursPerDay,
            projectName: booking.projectName,
            isChargeable: booking.isChargeable,
          })),
        overallocatedDays,
        underutilizedDays,
      };
    }),

  findCapacity: planningReadProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        minAvailableHoursPerDay: z.number().optional().default(4),
      }),
    )
    .query(async ({ ctx, input }) => {
      const resource = await findUniqueOrThrow(
        ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: {
            id: true,
            displayName: true,
            availability: true,
            countryId: true,
            federalState: true,
            metroCityId: true,
            country: { select: { code: true } },
            metroCity: { select: { name: true } },
          },
        }),
        "Resource",
      );

      const resourceBookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: [resource.id],
      });

      const availability = resource.availability as unknown as WeekdayAvailability;
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        [{
          id: resource.id,
          availability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        }],
        input.startDate,
        input.endDate,
      );
      const context = contexts.get(resource.id);
      const windows: Array<{
        resourceId: string;
        resourceName: string;
        startDate: Date;
        endDate: Date;
        availableHoursPerDay: number;
        availableDays: number;
        totalAvailableHours: number;
      }> = [];

      let windowStart: Date | null = null;
      let windowAvailableDays = 0;
      let windowTotalHours = 0;
      let windowMinHours = Number.POSITIVE_INFINITY;

      const closeWindow = (closeDate: Date) => {
        if (windowStart && windowAvailableDays > 0) {
          const previousDay = new Date(closeDate);
          previousDay.setUTCDate(previousDay.getUTCDate() - 1);
          windows.push({
            resourceId: resource.id,
            resourceName: resource.displayName,
            startDate: new Date(windowStart),
            endDate: previousDay,
            availableHoursPerDay: Number.isFinite(windowMinHours) ? windowMinHours : 0,
            availableDays: windowAvailableDays,
            totalAvailableHours: Math.round(windowTotalHours * 10) / 10,
          });
        }
        windowStart = null;
        windowAvailableDays = 0;
        windowTotalHours = 0;
        windowMinHours = Number.POSITIVE_INFINITY;
      };

      const cursor = new Date(input.startDate);
      cursor.setUTCHours(0, 0, 0, 0);
      const end = new Date(input.endDate);
      end.setUTCHours(0, 0, 0, 0);

      while (cursor <= end) {
        const availableHoursForDay = getEffectiveDayAvailability(availability, cursor, context);
        if (availableHoursForDay <= 0) {
          closeWindow(cursor);
          cursor.setUTCDate(cursor.getUTCDate() + 1);
          continue;
        }

        const { allocatedHours } = calculateAllocatedHoursForDay({
          bookings: resourceBookings.map((booking) => ({
            startDate: booking.startDate,
            endDate: booking.endDate,
            hoursPerDay: booking.hoursPerDay,
            status: booking.status,
          })),
          date: cursor,
          context,
        });
        const freeHours = Math.max(0, availableHoursForDay - allocatedHours);

        if (freeHours >= input.minAvailableHoursPerDay) {
          if (!windowStart) {
            windowStart = new Date(cursor);
          }
          windowAvailableDays += 1;
          windowTotalHours += freeHours;
          windowMinHours = Math.min(windowMinHours, freeHours);
        } else {
          closeWindow(cursor);
        }

        cursor.setUTCDate(cursor.getUTCDate() + 1);
      }

      closeWindow(new Date(end.getTime() + 86_400_000));

      return windows;
    }),
};
