import type { PrismaClient } from "@capakraken/db";
import type { WeekdayAvailability } from "@capakraken/shared";
import { listAssignmentBookings } from "../allocation/list-assignment-bookings.js";
import { getMonthBucketKey, getWeekBucketKey } from "./shared.js";
import {
  calculateEffectiveAllocationHours,
  calculateEffectiveAvailableHours,
  enumerateIsoDates,
  loadDailyAvailabilityContexts,
} from "./holiday-capacity.js";

export interface GetDashboardPeakTimesInput {
  startDate: Date;
  endDate: Date;
  granularity: "week" | "month";
  groupBy: "project" | "chapter" | "resource";
}

export interface PeakTimesPeriodDerivation {
  periodStart: string;
  periodEnd: string;
  resourceCount: number;
  groupCount: number;
  bookedHours: number;
  capacityHours: number;
  remainingCapacityHours: number;
  overbookedHours: number;
  utilizationPct: number;
}

export interface PeakTimesGroupRow {
  name: string;
  hours: number;
  capacityHours: number | undefined;
  remainingHours: number | undefined;
  overbookedHours: number | undefined;
  utilizationPct: number | undefined;
}

export interface PeakTimesPeriodRow {
  period: string;
  groups: PeakTimesGroupRow[];
  totalHours: number;
  capacityHours: number;
  periodStart?: string;
  periodEnd?: string;
  bookedHours?: number;
  remainingHours?: number;
  overbookedHours?: number;
  utilizationPct?: number;
  groupCount?: number;
  resourceCount?: number;
  derivation: PeakTimesPeriodDerivation;
}

export async function getDashboardPeakTimes(
  db: PrismaClient,
  input: GetDashboardPeakTimesInput,
): Promise<PeakTimesPeriodRow[]> {
  const [allocations, resources] = await Promise.all([
    listAssignmentBookings(db, {
      startDate: input.startDate,
      endDate: input.endDate,
    }),
    db.resource.findMany({
      where: { isActive: true },
      select: {
        id: true,
        displayName: true,
        chapter: true,
        availability: true,
        countryId: true,
        federalState: true,
        metroCityId: true,
        country: {
          select: {
            code: true,
          },
        },
        metroCity: {
          select: {
            name: true,
          },
        },
      },
    }),
  ]);

  const buckets = new Map<string, Map<string, number>>();
  const groupCapacityBuckets = new Map<string, Map<string, number>>();
  const getBucketKey = input.granularity === "week" ? getWeekBucketKey : getMonthBucketKey;
  const resourceMap = new Map(
    resources.map((resource) => [
      resource.id,
      {
        ...resource,
        availability: resource.availability as unknown as WeekdayAvailability,
      },
    ]),
  );
  const contexts = await loadDailyAvailabilityContexts(
    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 bucketPeriods = new Map<string, { start: Date; end: Date }>();

  for (const isoDate of enumerateIsoDates(input.startDate, input.endDate)) {
    const date = new Date(`${isoDate}T00:00:00.000Z`);
    const bucketKey = getBucketKey(date);
    const existing = bucketPeriods.get(bucketKey);
    if (!existing) {
      bucketPeriods.set(bucketKey, { start: date, end: date });
      continue;
    }
    if (date < existing.start) {
      existing.start = date;
    }
    if (date > existing.end) {
      existing.end = date;
    }
  }
  for (const bucketKey of bucketPeriods.keys()) {
    buckets.set(bucketKey, new Map());
    groupCapacityBuckets.set(bucketKey, new Map());
  }

  for (const allocation of allocations) {
    const resource = allocation.resourceId ? resourceMap.get(allocation.resourceId) : undefined;
    const group =
      input.groupBy === "project"
        ? allocation.project.shortCode
        : input.groupBy === "chapter"
          ? allocation.resource?.chapter ?? "Unassigned"
          : allocation.resource?.displayName ?? "Unknown";
    for (const [bucketKey, bucketPeriod] of bucketPeriods.entries()) {
      const hours = resource
        ? calculateEffectiveAllocationHours({
          availability: resource.availability,
          startDate: allocation.startDate,
          endDate: allocation.endDate,
          hoursPerDay: allocation.hoursPerDay,
          periodStart: bucketPeriod.start,
          periodEnd: bucketPeriod.end,
          context: contexts.get(resource.id),
        })
        : 0;
      if (hours <= 0) {
        continue;
      }
      const bucket = buckets.get(bucketKey)!;
      bucket.set(group, (bucket.get(group) ?? 0) + hours);
    }
  }
  const capacityByBucket = new Map<string, number>();
  for (const [bucketKey, bucketPeriod] of bucketPeriods.entries()) {
    let capacityHours = 0;
    for (const resource of resourceMap.values()) {
      const effectiveAvailableHours = calculateEffectiveAvailableHours({
        availability: resource.availability,
        periodStart: bucketPeriod.start,
        periodEnd: bucketPeriod.end,
        context: contexts.get(resource.id),
      });
      capacityHours += effectiveAvailableHours;

      if (input.groupBy !== "project" && effectiveAvailableHours > 0) {
        const group =
          input.groupBy === "chapter"
            ? resource.chapter ?? "Unassigned"
            : resource.displayName ?? "Unknown";
        const groupCapacityBucket = groupCapacityBuckets.get(bucketKey)!;
        groupCapacityBucket.set(
          group,
          (groupCapacityBucket.get(group) ?? 0) + effectiveAvailableHours,
        );
      }
    }
    capacityByBucket.set(bucketKey, capacityHours);
  }

  return [...bucketPeriods.entries()]
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([period, bucketPeriod]) => {
      const groups = buckets.get(period) ?? new Map<string, number>();
      const groupCapacities = groupCapacityBuckets.get(period) ?? new Map<string, number>();
      const groupNames = new Set<string>([
        ...groups.keys(),
        ...(input.groupBy === "project" ? [] : groupCapacities.keys()),
      ]);
      const groupRows = [...groupNames]
        .map((name) => {
          const hours = groups.get(name) ?? 0;
          const groupCapacityHours =
            input.groupBy === "project" ? undefined : groupCapacities.get(name) ?? 0;
          const remainingHours =
            groupCapacityHours === undefined
              ? undefined
              : Math.max(0, groupCapacityHours - hours);
          const overbookedHours =
            groupCapacityHours === undefined
              ? undefined
              : Math.max(0, hours - groupCapacityHours);
          return {
            name,
            hours,
            capacityHours: groupCapacityHours,
            remainingHours,
            overbookedHours,
            utilizationPct:
              groupCapacityHours && groupCapacityHours > 0
                ? Math.round((hours / groupCapacityHours) * 100)
                : groupCapacityHours === 0
                  ? 0
                  : undefined,
          };
        })
        .sort(
          (left, right) =>
            (right.utilizationPct ?? -1) - (left.utilizationPct ?? -1) ||
            right.hours - left.hours ||
            left.name.localeCompare(right.name),
        );
      const totalHours = [...groups.values()].reduce((sum, hours) => sum + hours, 0);
      const capacityHours = capacityByBucket.get(period) ?? 0;
      const remainingCapacityHours = Math.max(0, capacityHours - totalHours);
      const overbookedHours = Math.max(0, totalHours - capacityHours);

      return {
        period,
        groups: groupRows,
        totalHours,
        capacityHours,
        periodStart: bucketPeriod.start.toISOString().slice(0, 10),
        periodEnd: bucketPeriod.end.toISOString().slice(0, 10),
        bookedHours: totalHours,
        remainingHours: remainingCapacityHours,
        overbookedHours,
        utilizationPct: capacityHours > 0
          ? Math.round((totalHours / capacityHours) * 100)
          : 0,
        groupCount: groupRows.length,
        resourceCount: resourceMap.size,
        derivation: {
          periodStart: bucketPeriod.start.toISOString().slice(0, 10),
          periodEnd: bucketPeriod.end.toISOString().slice(0, 10),
          resourceCount: resourceMap.size,
          groupCount: groupRows.length,
          bookedHours: totalHours,
          capacityHours,
          remainingCapacityHours,
          overbookedHours,
          utilizationPct: capacityHours > 0
            ? Math.round((totalHours / capacityHours) * 100)
            : 0,
        },
      };
    });
}
