import {
  isChargeabilityActualBooking,
  isChargeabilityRelevantProject,
  listAssignmentBookings,
} from "@capakraken/application";
import type { WeekdayAvailability } from "@capakraken/shared";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  countEffectiveWorkingDays,
  getAvailabilityHoursForDate,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import type { ReportInput, ReportQueryResult } from "./report-query-config.js";
import { RESOURCE_MONTH_COLUMNS } from "./report-columns.js";
import {
  buildReportGroups,
  matchesInMemoryFilter,
  pickColumns,
  sortInMemoryRows,
} from "./report-query-utils.js";

export async function executeResourceMonthReport(
  db: any,
  input: ReportInput,
): Promise<ReportQueryResult> {
  const periodMonth = input.periodMonth ?? new Date().toISOString().slice(0, 7);
  const [year, month] = periodMonth.split("-").map(Number) as [number, number];
  const periodStart = new Date(Date.UTC(year, month - 1, 1));
  const periodEnd = new Date(Date.UTC(year, month, 0));

  const resources = await db.resource.findMany({
    select: {
      id: true,
      eid: true,
      displayName: true,
      email: true,
      chapter: true,
      resourceType: true,
      isActive: true,
      chgResponsibility: true,
      rolledOff: true,
      departed: true,
      lcrCents: true,
      ucrCents: true,
      currency: true,
      fte: true,
      availability: true,
      chargeabilityTarget: true,
      federalState: true,
      countryId: true,
      metroCityId: true,
      country: { select: { code: true, name: true } },
      metroCity: { select: { name: true } },
      orgUnit: { select: { name: true } },
      managementLevelGroup: { select: { name: true, targetPercentage: true } },
      managementLevel: { select: { name: true } },
    },
    orderBy: { displayName: "asc" },
  });

  const resourceIds = resources.map((resource: any) => resource.id);
  const [bookings, contexts] = await Promise.all([
    resourceIds.length > 0
      ? listAssignmentBookings(db, {
        startDate: periodStart,
        endDate: periodEnd,
        resourceIds,
      })
      : Promise.resolve([]),
    loadResourceDailyAvailabilityContexts(
      db,
      resources.map((resource: any) => ({
        id: resource.id,
        availability: resource.availability as WeekdayAvailability,
        countryId: resource.countryId,
        countryCode: resource.country?.code,
        federalState: resource.federalState,
        metroCityId: resource.metroCityId,
        metroCityName: resource.metroCity?.name,
      })),
      periodStart,
      periodEnd,
    ),
  ]);

  const rows = resources.map((resource: any) => {
    const availability = resource.availability as WeekdayAvailability;
    const context = contexts.get(resource.id);
    const resourceBookings = bookings.filter((booking) => booking.resourceId === resource.id);
    const baseWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart,
      periodEnd,
      context: undefined,
    });
    const effectiveWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart,
      periodEnd,
      context,
    });
    const baseAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart,
      periodEnd,
      context: undefined,
    });
    const sahHours = calculateEffectiveAvailableHours({
      availability,
      periodStart,
      periodEnd,
      context,
    });

    const holidayDates = [...(context?.holidayDates ?? new Set<string>())];
    const publicHolidayWorkdayCount = holidayDates.reduce((count, isoDate) => (
      count + (getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
    ), 0);
    const publicHolidayHoursDeduction = holidayDates.reduce((sum, isoDate) => (
      sum + getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`))
    ), 0);

    let absenceDayEquivalent = 0;
    let absenceHoursDeduction = 0;
    for (const [isoDate, fraction] of context?.vacationFractionsByDate ?? []) {
      const dayHours = getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`));
      if (dayHours <= 0 || context?.holidayDates.has(isoDate)) {
        continue;
      }
      absenceDayEquivalent += fraction;
      absenceHoursDeduction += dayHours * fraction;
    }

    const actualBookedHours = resourceBookings
      .filter((booking) => isChargeabilityActualBooking(booking, false))
      .reduce((sum, booking) => sum + calculateEffectiveBookedHours({
        availability,
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        periodStart,
        periodEnd,
        context,
      }), 0);
    const expectedBookedHours = resourceBookings
      .filter((booking) => isChargeabilityRelevantProject(booking.project, true))
      .reduce((sum, booking) => sum + calculateEffectiveBookedHours({
        availability,
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        periodStart,
        periodEnd,
        context,
      }), 0);

    const targetPct = resource.managementLevelGroup?.targetPercentage != null
      ? resource.managementLevelGroup.targetPercentage * 100
      : resource.chargeabilityTarget;

    return {
      id: `${resource.id}:${periodMonth}`,
      resourceId: resource.id,
      monthKey: periodMonth,
      periodStart: periodStart.toISOString(),
      periodEnd: periodEnd.toISOString(),
      eid: resource.eid,
      displayName: resource.displayName,
      email: resource.email,
      chapter: resource.chapter,
      resourceType: resource.resourceType,
      isActive: resource.isActive,
      chgResponsibility: resource.chgResponsibility,
      rolledOff: resource.rolledOff,
      departed: resource.departed,
      countryCode: resource.country?.code ?? null,
      countryName: resource.country?.name ?? null,
      federalState: resource.federalState,
      metroCityName: resource.metroCity?.name ?? null,
      orgUnitName: resource.orgUnit?.name ?? null,
      managementLevelGroupName: resource.managementLevelGroup?.name ?? null,
      managementLevelName: resource.managementLevel?.name ?? null,
      fte: roundMetric(resource.fte),
      lcrCents: resource.lcrCents,
      ucrCents: resource.ucrCents,
      currency: resource.currency,
      monthlyChargeabilityTargetPct: roundMetric(targetPct),
      monthlyTargetHours: roundMetric((sahHours * targetPct) / 100),
      monthlyBaseWorkingDays: roundMetric(baseWorkingDays),
      monthlyEffectiveWorkingDays: roundMetric(effectiveWorkingDays),
      monthlyBaseAvailableHours: roundMetric(baseAvailableHours),
      monthlySahHours: roundMetric(sahHours),
      monthlyPublicHolidayCount: holidayDates.length,
      monthlyPublicHolidayWorkdayCount: publicHolidayWorkdayCount,
      monthlyPublicHolidayHoursDeduction: roundMetric(publicHolidayHoursDeduction),
      monthlyAbsenceDayEquivalent: roundMetric(absenceDayEquivalent),
      monthlyAbsenceHoursDeduction: roundMetric(absenceHoursDeduction),
      monthlyActualBookedHours: roundMetric(actualBookedHours),
      monthlyExpectedBookedHours: roundMetric(expectedBookedHours),
      monthlyActualChargeabilityPct: roundMetric(sahHours > 0 ? (actualBookedHours / sahHours) * 100 : 0),
      monthlyExpectedChargeabilityPct: roundMetric(sahHours > 0 ? (expectedBookedHours / sahHours) * 100 : 0),
      monthlyUnassignedHours: roundMetric(Math.max(0, sahHours - actualBookedHours)),
    };
  });

  const filteredRows = rows.filter((row: Record<string, unknown>) => input.filters.every((filter) => matchesInMemoryFilter(
    row,
    filter,
    RESOURCE_MONTH_COLUMNS,
  )));
  const sortedRows = sortInMemoryRows(
    filteredRows,
    input.groupBy,
    input.sortBy,
    input.sortDir,
    RESOURCE_MONTH_COLUMNS,
  );
  const totalCount = sortedRows.length;
  const pagedRows = sortedRows.slice(input.offset, input.offset + input.limit);
  const outputColumns = ["id", ...input.columns.filter((column) => column !== "id")];
  const groups = buildReportGroups(pagedRows, input.groupBy);

  return {
    rows: pagedRows.map((row) => pickColumns(row, outputColumns)),
    columns: outputColumns,
    totalCount,
    groups,
  };
}

function roundMetric(value: number): number {
  return Math.round(value * 10) / 10;
}
