import type {
  AbsenceDay,
  AllocationCalculationInput,
  AllocationCalculationResult,
  DailyBreakdown,
  WeekdayAvailability,
} from "@capakraken/shared";
import type { AbsenceTrigger } from "@capakraken/shared";
import { getRecurringHoursForDay } from "./recurrence.js";
import { findMatchingRule, applyCostEffect } from "../rules/engine.js";

/** Day-of-week index → availability key */
const DOW_KEYS: (keyof WeekdayAvailability)[] = [
  "sunday",
  "monday",
  "tuesday",
  "wednesday",
  "thursday",
  "friday",
  "saturday",
];

/**
 * Returns the availability hours for a given date.
 * Returns 0 for days not in the availability map (treated as non-working).
 */
export function getAvailableHoursForDate(
  date: Date,
  availability: WeekdayAvailability,
): number {
  const key = DOW_KEYS[date.getDay()];
  if (!key) return 0;
  return availability[key] ?? 0;
}

/**
 * Checks whether a given date is a working day for this resource.
 */
export function isWorkday(date: Date, availability: WeekdayAvailability): boolean {
  return getAvailableHoursForDate(date, availability) > 0;
}

/**
 * Counts working days between startDate and endDate (inclusive).
 */
export function countWorkingDays(
  startDate: Date,
  endDate: Date,
  availability: WeekdayAvailability,
): number {
  let count = 0;
  const current = new Date(startDate);
  current.setHours(0, 0, 0, 0);
  const end = new Date(endDate);
  end.setHours(0, 0, 0, 0);

  while (current <= end) {
    if (isWorkday(current, availability)) {
      count++;
    }
    current.setDate(current.getDate() + 1);
  }
  return count;
}

/**
 * Core allocation calculator: given hours/day, LCR, and date range,
 * computes total hours, total cost, and daily breakdown.
 *
 * When calculationRules + absenceDays are provided, the rules engine
 * determines per-day cost attribution and chargeability effects.
 *
 * Monetary values always in integer cents.
 */
export function calculateAllocation(input: AllocationCalculationInput): AllocationCalculationResult {
  const {
    lcrCents, hoursPerDay, startDate, endDate, availability, includeSaturday,
    recurrence, vacationDates, absenceDays, calculationRules, orderType, projectId,
  } = input;

  // When includeSaturday is not explicitly true, zero out saturday availability
  const effectiveAvailability: WeekdayAvailability = includeSaturday
    ? availability
    : { ...availability, saturday: 0 };

  // Pre-compute vacation date set (YYYY-MM-DD strings for O(1) lookup)
  const vacationDateSet = new Set<string>(
    (vacationDates ?? []).map((d) => {
      const copy = new Date(d);
      copy.setHours(0, 0, 0, 0);
      return copy.toISOString().split("T")[0]!;
    }),
  );

  // Pre-compute typed absence day lookup (date key → AbsenceDay)
  const absenceDayMap = new Map<string, AbsenceDay>();
  for (const ad of absenceDays ?? []) {
    const copy = new Date(ad.date);
    copy.setHours(0, 0, 0, 0);
    absenceDayMap.set(copy.toISOString().split("T")[0]!, ad);
  }

  const hasRules = calculationRules && calculationRules.length > 0;

  const allocationStart = new Date(startDate);
  allocationStart.setHours(0, 0, 0, 0);

  const breakdown: DailyBreakdown[] = [];
  const current = new Date(startDate);
  current.setHours(0, 0, 0, 0);
  const end = new Date(endDate);
  end.setHours(0, 0, 0, 0);

  let workingDays = 0;
  let totalHours = 0;
  let totalChargeableHours = 0;
  let totalProjectCostCents = 0;

  while (current <= end) {
    const dateKey = current.toISOString().split("T")[0]!;
    const isVacation = vacationDateSet.has(dateKey);
    const absenceDay = absenceDayMap.get(dateKey);

    let effectiveHours: number;
    let dayIsWorkday: boolean;
    let absenceType: AbsenceTrigger | undefined;
    let chargeableHours: number | undefined;
    let projectCostCents: number;

    // Determine if this is an absence day (from typed absenceDays or legacy vacationDates)
    const isAbsent = isVacation || !!absenceDay;
    if (absenceDay) {
      absenceType = absenceDay.type;
    } else if (isVacation) {
      absenceType = "VACATION";
    }

    if (isAbsent && hasRules && absenceType) {
      // ── Rules-based absence handling ──
      const availableHours = getAvailableHoursForDate(current, effectiveAvailability);
      dayIsWorkday = availableHours > 0;

      if (!dayIsWorkday) {
        // Weekend/non-working day — no effect regardless of absence
        effectiveHours = 0;
        chargeableHours = 0;
        projectCostCents = 0;
      } else {
        const normalHours = Math.min(hoursPerDay, availableHours);
        const halfDayFactor = absenceDay?.isHalfDay ? 0.5 : 1;
        const absentHours = normalHours * halfDayFactor;
        const workedHours = normalHours - absentHours;

        // The person does NOT work the absent portion
        effectiveHours = workedHours;

        const match = findMatchingRule(calculationRules!, absenceType, projectId, orderType);
        if (match) {
          // Cost effect: how much does the project pay?
          const normalCostCents = Math.round(absentHours * lcrCents);
          const absentProjectCost = applyCostEffect(normalCostCents, match.costEffect, match.costReductionPercent);
          const workedCostCents = Math.round(workedHours * lcrCents);
          projectCostCents = workedCostCents + absentProjectCost;

          // Chargeability effect: does the person count as chargeable?
          if (match.chargeabilityEffect === "COUNT") {
            chargeableHours = normalHours; // full hours count toward chargeability
          } else {
            chargeableHours = workedHours; // only worked portion counts
          }
        } else {
          // No matching rule — legacy behavior: block absent hours
          effectiveHours = workedHours;
          projectCostCents = Math.round(workedHours * lcrCents);
          chargeableHours = workedHours;
        }

        workingDays++;
        totalHours += effectiveHours;
      }
    } else if (isVacation && !hasRules) {
      // ── Legacy behavior: vacation blocks the day entirely ──
      effectiveHours = 0;
      dayIsWorkday = false;
      projectCostCents = 0;
    } else if (recurrence) {
      // Recurrence pattern — may override hoursPerDay or skip the day entirely
      const recurHours = getRecurringHoursForDay(current, recurrence, hoursPerDay, allocationStart);
      if (recurHours === 0) {
        effectiveHours = 0;
        dayIsWorkday = false;
        projectCostCents = 0;
      } else {
        const availableHours = getAvailableHoursForDate(current, effectiveAvailability);
        dayIsWorkday = availableHours > 0;
        effectiveHours = dayIsWorkday ? Math.min(recurHours, availableHours) : 0;
        projectCostCents = Math.round(effectiveHours * lcrCents);
      }

      if (dayIsWorkday) {
        workingDays++;
        totalHours += effectiveHours;
      }
    } else {
      const availableHours = getAvailableHoursForDate(current, effectiveAvailability);
      dayIsWorkday = availableHours > 0;
      effectiveHours = dayIsWorkday ? Math.min(hoursPerDay, availableHours) : 0;
      projectCostCents = Math.round(effectiveHours * lcrCents);

      if (dayIsWorkday) {
        workingDays++;
        totalHours += effectiveHours;
      }
    }

    // costCents on DailyBreakdown = project cost (rule-adjusted)
    const dayCostCents = projectCostCents;

    breakdown.push({
      date: new Date(current),
      isWorkday: dayIsWorkday,
      hours: effectiveHours,
      costCents: dayCostCents,
      ...(absenceType ? { absenceType } : {}),
      ...(chargeableHours !== undefined ? { chargeableHours } : {}),
    });

    totalChargeableHours += chargeableHours ?? effectiveHours;
    totalProjectCostCents += dayCostCents;

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

  const totalCostCents = breakdown.reduce((sum, d) => sum + d.costCents, 0);
  const dailyCostCents = Math.round(hoursPerDay * lcrCents);

  return {
    workingDays,
    totalHours,
    totalCostCents,
    dailyCostCents,
    dailyBreakdown: breakdown,
    ...(hasRules ? { totalChargeableHours, totalProjectCostCents } : {}),
  };
}

/**
 * Calculates total allocation cost for a simple case (without full breakdown).
 * Useful for quick budget checks.
 */
export function calculateTotalCost(
  lcrCents: number,
  hoursPerDay: number,
  workingDays: number,
): number {
  return Math.round(lcrCents * hoursPerDay * workingDays);
}
