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,
  type DailyAvailabilityContext,
} 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;
  calendarContextCount: number;
  resourceCount: number;
  groupCount: number;
  baseAvailableHours: number;
  effectiveAvailableHours: number;
  publicHolidayHoursDeduction: number;
  absenceDayEquivalent: number;
  absenceHoursDeduction: number;
  calendarLocations: PeakTimesCalendarLocationSummary[];
  bookedHours: number;
  capacityHours: number;
  remainingCapacityHours: number;
  overbookedHours: number;
  utilizationPct: number;
}

export interface PeakTimesCalendarLocationSummary {
  countryCode: string | null;
  countryName: string | null;
  federalState: string | null;
  metroCityName: string | null;
  resourceCount: number;
  effectiveAvailableHours: 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;
}

type PeakTimesCapacityDerivationSummary = Pick<
  PeakTimesPeriodDerivation,
  | "baseAvailableHours"
  | "effectiveAvailableHours"
  | "publicHolidayHoursDeduction"
  | "absenceDayEquivalent"
  | "absenceHoursDeduction"
  | "calendarContextCount"
  | "calendarLocations"
>;

const DAY_KEYS: (keyof WeekdayAvailability)[] = [
  "sunday",
  "monday",
  "tuesday",
  "wednesday",
  "thursday",
  "friday",
  "saturday",
];

function toIsoDate(value: Date): string {
  return value.toISOString().slice(0, 10);
}

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

function buildLocationKey(input: {
  countryCode: string | null | undefined;
  countryName: string | null | undefined;
  federalState: string | null | undefined;
  metroCityName: string | null | undefined;
}): string {
  return JSON.stringify({
    countryCode: input.countryCode ?? null,
    countryName: input.countryName ?? null,
    federalState: input.federalState ?? null,
    metroCityName: input.metroCityName ?? null,
  });
}

function getDailyAvailabilityHours(
  availability: WeekdayAvailability,
  date: Date,
): number {
  const dayKey = DAY_KEYS[date.getUTCDay()];
  return dayKey ? (availability[dayKey] ?? 0) : 0;
}

function summarizeCapacityDerivation(
  availability: WeekdayAvailability,
  periodStart: Date,
  periodEnd: Date,
  context: DailyAvailabilityContext | undefined,
) {
  let publicHolidayHoursDeduction = 0;
  let absenceDayEquivalent = 0;
  let absenceHoursDeduction = 0;

  const baseAvailableHours = calculateEffectiveAvailableHours({
    availability,
    periodStart,
    periodEnd,
    context: undefined,
  });
  const effectiveAvailableHours = calculateEffectiveAvailableHours({
    availability,
    periodStart,
    periodEnd,
    context,
  });

  const cursor = new Date(periodStart);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(periodEnd);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    const isoDate = toIsoDate(cursor);
    const baseHours = getDailyAvailabilityHours(availability, cursor);
    const absenceFraction = Math.min(
      1,
      Math.max(0, context?.absenceFractionsByDate.get(isoDate) ?? 0),
    );
    const isHoliday = context?.holidayDates.has(isoDate) ?? false;

    if (baseHours > 0) {
      if (isHoliday) {
        publicHolidayHoursDeduction += baseHours;
      } else if (absenceFraction > 0) {
        absenceDayEquivalent += absenceFraction;
        absenceHoursDeduction += baseHours * absenceFraction;
      }
    }

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

  return {
    baseAvailableHours,
    effectiveAvailableHours,
    publicHolidayHoursDeduction,
    absenceDayEquivalent,
    absenceHoursDeduction,
  };
}

function summarizeCalendarLocations(
  resources: Array<{
    id: string;
    availability: WeekdayAvailability;
    countryCode: string | null | undefined;
    countryName: string | null | undefined;
    federalState: string | null | undefined;
    metroCityName: string | null | undefined;
  }>,
  contexts: Map<string, DailyAvailabilityContext>,
  periodStart: Date,
  periodEnd: Date,
): PeakTimesCalendarLocationSummary[] {
  const locationMap = new Map<string, PeakTimesCalendarLocationSummary & { resourceIds: Set<string> }>();

  for (const resource of resources) {
    const capacityDerivation = summarizeCapacityDerivation(
      resource.availability,
      periodStart,
      periodEnd,
      contexts.get(resource.id),
    );
    const locationKey = buildLocationKey({
      countryCode: resource.countryCode,
      countryName: resource.countryName,
      federalState: resource.federalState,
      metroCityName: resource.metroCityName,
    });
    const existing = locationMap.get(locationKey) ?? {
      countryCode: resource.countryCode ?? null,
      countryName: resource.countryName ?? null,
      federalState: resource.federalState ?? null,
      metroCityName: resource.metroCityName ?? null,
      resourceCount: 0,
      effectiveAvailableHours: 0,
      resourceIds: new Set<string>(),
    };

    existing.effectiveAvailableHours += capacityDerivation.effectiveAvailableHours;
    existing.resourceIds.add(resource.id);
    existing.resourceCount = existing.resourceIds.size;
    locationMap.set(locationKey, existing);
  }

  return [...locationMap.values()]
    .map(({ resourceIds: _resourceIds, ...summary }) => ({
      ...summary,
      effectiveAvailableHours: round1(summary.effectiveAvailableHours),
    }))
    .sort((left, right) => right.effectiveAvailableHours - left.effectiveAvailableHours);
}

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,
            name: 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,
      countryName: resource.country?.name,
      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>();
  const derivationByBucket = new Map<string, PeakTimesCapacityDerivationSummary>();
  for (const [bucketKey, bucketPeriod] of bucketPeriods.entries()) {
    let capacityHours = 0;
    const derivationTotals: PeakTimesCapacityDerivationSummary = {
      baseAvailableHours: 0,
      effectiveAvailableHours: 0,
      publicHolidayHoursDeduction: 0,
      absenceDayEquivalent: 0,
      absenceHoursDeduction: 0,
      calendarContextCount: 0,
      calendarLocations: [] as PeakTimesCalendarLocationSummary[],
    };
    for (const resource of resourceMap.values()) {
      const capacityDerivation = summarizeCapacityDerivation(
        resource.availability,
        bucketPeriod.start,
        bucketPeriod.end,
        contexts.get(resource.id),
      );
      const effectiveAvailableHours = capacityDerivation.effectiveAvailableHours;
      capacityHours += effectiveAvailableHours;
      derivationTotals.baseAvailableHours += capacityDerivation.baseAvailableHours;
      derivationTotals.effectiveAvailableHours += capacityDerivation.effectiveAvailableHours;
      derivationTotals.publicHolidayHoursDeduction += capacityDerivation.publicHolidayHoursDeduction;
      derivationTotals.absenceDayEquivalent += capacityDerivation.absenceDayEquivalent;
      derivationTotals.absenceHoursDeduction += capacityDerivation.absenceHoursDeduction;

      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,
        );
      }
    }
    derivationTotals.calendarLocations = summarizeCalendarLocations(
      [...resourceMap.values()].map((resource) => ({
        id: resource.id,
        availability: resource.availability,
        countryCode: resource.country?.code,
        countryName: resource.country?.name,
        federalState: resource.federalState,
        metroCityName: resource.metroCity?.name,
      })),
      contexts,
      bucketPeriod.start,
      bucketPeriod.end,
    );
    derivationTotals.calendarContextCount = derivationTotals.calendarLocations.length;
    capacityByBucket.set(bucketKey, capacityHours);
    derivationByBucket.set(bucketKey, derivationTotals);
  }

  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 capacityDerivation: PeakTimesCapacityDerivationSummary =
        derivationByBucket.get(period) ?? {
        baseAvailableHours: capacityHours,
        effectiveAvailableHours: capacityHours,
        publicHolidayHoursDeduction: 0,
        absenceDayEquivalent: 0,
        absenceHoursDeduction: 0,
        calendarContextCount: 0,
        calendarLocations: [] as PeakTimesCalendarLocationSummary[],
      };
      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),
          calendarContextCount: capacityDerivation.calendarContextCount,
          resourceCount: resourceMap.size,
          groupCount: groupRows.length,
          baseAvailableHours: capacityDerivation.baseAvailableHours,
          effectiveAvailableHours: capacityDerivation.effectiveAvailableHours,
          publicHolidayHoursDeduction: capacityDerivation.publicHolidayHoursDeduction,
          absenceDayEquivalent: Math.round(capacityDerivation.absenceDayEquivalent * 10) / 10,
          absenceHoursDeduction: capacityDerivation.absenceHoursDeduction,
          calendarLocations: capacityDerivation.calendarLocations,
          bookedHours: totalHours,
          capacityHours,
          remainingCapacityHours,
          overbookedHours,
          utilizationPct: capacityHours > 0
            ? Math.round((totalHours / capacityHours) * 100)
            : 0,
        },
      };
    });
}
