import {
  MILLISECONDS_PER_DAY,
  type Allocation,
  type CapacityWindow,
  type Resource,
  type UtilizationAnalysis,
  type UtilizationPeriod,
} from "@nexus/shared";

export interface CapacityAnalysisInput {
  resource: Pick<Resource, "id" | "displayName" | "chargeabilityTarget" | "availability">;
  allocations: (Pick<Allocation, "startDate" | "endDate" | "hoursPerDay" | "status"> & {
    projectName: string;
    isChargeable: boolean;
  })[];
  analysisStart: Date;
  analysisEnd: Date;
}

/** Returns the number of whole days from epoch (UTC midnight) for a Date. */
function toDayIndex(d: Date): number {
  return Math.floor(d.getTime() / MILLISECONDS_PER_DAY);
}

/**
 * Analyzes resource utilization over a given period.
 * Pure function — no DB access.
 */
export function analyzeUtilization(input: CapacityAnalysisInput): UtilizationAnalysis {
  const { resource, allocations, analysisStart, analysisEnd } = input;
  const activeStatuses = new Set(["PROPOSED", "CONFIRMED", "ACTIVE"]);

  const activeAllocs = allocations.filter((a) => activeStatuses.has(a.status));

  const periods: UtilizationPeriod[] = activeAllocs.map((a) => ({
    startDate: new Date(a.startDate),
    endDate: new Date(a.endDate),
    hoursPerDay: a.hoursPerDay,
    projectName: a.projectName,
    isChargeable: a.isChargeable,
  }));

  const startDay = toDayIndex(analysisStart);
  const endDay = toDayIndex(analysisEnd);
  const numDays = endDay - startDay + 1;

  // Pre-compute allocated and chargeable hours per day using a difference array (O(A + D)).
  const allocHours = new Float64Array(numDays);
  const chargeHours = new Float64Array(numDays);

  for (const alloc of activeAllocs) {
    const aStartDay = Math.max(toDayIndex(new Date(alloc.startDate)), startDay);
    const aEndDay = Math.min(toDayIndex(new Date(alloc.endDate)), endDay);
    if (aStartDay > aEndDay) continue;

    const lo = aStartDay - startDay;
    const hi = aEndDay - startDay;
    for (let i = lo; i <= hi; i++) {
      allocHours[i]! += alloc.hoursPerDay;
      if (alloc.isChargeable) chargeHours[i]! += alloc.hoursPerDay;
    }
  }

  const overallocatedDays: string[] = [];
  const underutilizedDays: string[] = [];
  let totalWorkingDays = 0;
  let totalChargeableHours = 0;
  let totalAvailableHours = 0;

  const DOW_KEYS = [
    "sunday",
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday",
  ] as const;

  const current = new Date(analysisStart);
  current.setHours(0, 0, 0, 0);

  for (let i = 0; i < numDays; i++) {
    const dow = current.getDay();
    const dowKey = DOW_KEYS[dow] as keyof typeof resource.availability;
    const availableHours = resource.availability[dowKey] ?? 0;

    if (availableHours > 0) {
      totalWorkingDays++;
      totalAvailableHours += availableHours;
      totalChargeableHours += chargeHours[i] ?? 0;

      const allocated = allocHours[i] ?? 0;
      const dateStr = current.toISOString().split("T")[0] ?? current.toISOString();

      if (allocated > availableHours) {
        overallocatedDays.push(dateStr);
      } else if (allocated < availableHours * 0.5) {
        underutilizedDays.push(dateStr);
      }
    }

    current.setDate(current.getDate() + 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: periods,
    overallocatedDays,
    underutilizedDays,
  };
}

/**
 * Finds capacity windows for a resource — periods where they have available hours.
 */
export function findCapacityWindows(
  resource: Pick<Resource, "id" | "displayName" | "availability">,
  existingAllocations: Pick<Allocation, "startDate" | "endDate" | "hoursPerDay" | "status">[],
  searchStart: Date,
  searchEnd: Date,
  minAvailableHoursPerDay = 4,
): CapacityWindow[] {
  const activeStatuses = new Set(["PROPOSED", "CONFIRMED", "ACTIVE"]);
  const windows: CapacityWindow[] = [];

  const startDay = toDayIndex(searchStart);
  const endDay = toDayIndex(searchEnd);
  const numDays = endDay - startDay + 1;

  // Pre-compute allocated hours per day using a difference array (O(A + D)).
  const allocHours = new Float64Array(numDays);

  for (const alloc of existingAllocations) {
    if (!activeStatuses.has(alloc.status)) continue;
    const aStartDay = Math.max(toDayIndex(new Date(alloc.startDate)), startDay);
    const aEndDay = Math.min(toDayIndex(new Date(alloc.endDate)), endDay);
    if (aStartDay > aEndDay) continue;

    const lo = aStartDay - startDay;
    const hi = aEndDay - startDay;
    for (let i = lo; i <= hi; i++) {
      allocHours[i]! += alloc.hoursPerDay;
    }
  }

  let windowStart: Date | null = null;
  let windowAvailableDays = 0;
  let windowTotalHours = 0;
  let windowMinHours = Infinity;

  const DOW_KEYS = [
    "sunday",
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday",
  ] as const;

  function closeWindow(closeDate: Date) {
    if (windowStart && windowAvailableDays > 0) {
      const prev = new Date(closeDate);
      prev.setDate(prev.getDate() - 1);
      windows.push({
        resourceId: resource.id,
        resourceName: resource.displayName,
        startDate: new Date(windowStart),
        endDate: prev,
        availableHoursPerDay: windowMinHours === Infinity ? 0 : windowMinHours,
        availableDays: windowAvailableDays,
        totalAvailableHours: windowTotalHours,
      });
    }
    windowStart = null;
    windowAvailableDays = 0;
    windowTotalHours = 0;
    windowMinHours = Infinity;
  }

  const current = new Date(searchStart);
  current.setHours(0, 0, 0, 0);

  for (let i = 0; i < numDays; i++) {
    const dow = current.getDay();
    const dowKey = DOW_KEYS[dow] as keyof typeof resource.availability;
    const maxHours = resource.availability[dowKey] ?? 0;

    if (maxHours === 0) {
      closeWindow(current);
      current.setDate(current.getDate() + 1);
      continue;
    }

    const freeHours = Math.max(0, maxHours - (allocHours[i] ?? 0));

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

    current.setDate(current.getDate() + 1);
  }

  const endDate = new Date(searchEnd);
  endDate.setHours(0, 0, 0, 0);
  closeWindow(new Date(endDate.getTime() + MILLISECONDS_PER_DAY));

  return windows;
}
