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

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

export interface GetDashboardChargeabilityOverviewInput {
  includeProposed?: boolean;
  topN: number;
  watchlistThreshold: number;
  countryIds?: string[];
  departed?: boolean;
  now?: Date;
}

export interface DashboardChargeabilityDerivation {
  weeklyAvailabilityHours: number;
  baseWorkingDays: number;
  effectiveWorkingDayEquivalent: number;
  baseAvailableHours: number;
  effectiveAvailableHours: number;
  publicHolidayCount: number;
  publicHolidayWorkdayCount: number;
  publicHolidayHoursDeduction: number;
  absenceDayEquivalent: number;
  absenceHoursDeduction: number;
  actualBookedHours: number;
  expectedBookedHours: number;
  targetBookedHours: number;
  unassignedHours: number;
}

export interface DashboardChargeabilityRow {
  id: string;
  eid: string;
  displayName: string;
  chapter: string | null;
  countryId?: string | null;
  countryCode?: string | null;
  countryName?: string | null;
  federalState?: string | null;
  metroCityName?: string | null;
  departed: boolean | null;
  chargeabilityTarget: number;
  actualChargeability: number;
  expectedChargeability: number;
  derivation?: DashboardChargeabilityDerivation;
}

export interface DashboardChargeabilityOverview {
  rows: DashboardChargeabilityRow[];
  top: DashboardChargeabilityRow[];
  watchlist: DashboardChargeabilityRow[];
  month: string;
}

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

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

function summarizeDerivation(
  availability: WeekdayAvailability,
  periodStart: Date,
  periodEnd: Date,
  context: DailyAvailabilityContext | undefined,
  actualBookedHours: number,
  expectedBookedHours: number,
  chargeabilityTarget: number,
): DashboardChargeabilityDerivation {
  let baseWorkingDays = 0;
  let effectiveWorkingDayEquivalent = 0;
  let publicHolidayWorkdayCount = 0;
  let publicHolidayHoursDeduction = 0;
  let absenceDayEquivalent = 0;
  let absenceHoursDeduction = 0;

  const weeklyAvailabilityHours = Object.values(availability).reduce(
    (sum, hours) => sum + (hours ?? 0),
    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) {
      baseWorkingDays += 1;
      if (isHoliday) {
        publicHolidayWorkdayCount += 1;
        publicHolidayHoursDeduction += baseHours;
      } else {
        absenceDayEquivalent += absenceFraction;
        absenceHoursDeduction += baseHours * absenceFraction;
        effectiveWorkingDayEquivalent += Math.max(0, 1 - absenceFraction);
      }
    }

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

  return {
    weeklyAvailabilityHours,
    baseWorkingDays,
    effectiveWorkingDayEquivalent,
    baseAvailableHours,
    effectiveAvailableHours,
    publicHolidayCount: context?.holidayDates.size ?? 0,
    publicHolidayWorkdayCount,
    publicHolidayHoursDeduction,
    absenceDayEquivalent,
    absenceHoursDeduction,
    actualBookedHours,
    expectedBookedHours,
    targetBookedHours: Math.round((effectiveAvailableHours * chargeabilityTarget) / 10) / 10,
    unassignedHours: Math.max(0, effectiveAvailableHours - expectedBookedHours),
  };
}

export async function getDashboardChargeabilityOverview(
  db: PrismaClient,
  input: GetDashboardChargeabilityOverviewInput,
): Promise<DashboardChargeabilityOverview> {
  const now = input.now ?? new Date();
  const start = new Date(now.getFullYear(), now.getMonth(), 1);
  const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);

  const resources = await db.resource.findMany({
    where: {
      isActive: true,
      ...(input.countryIds && input.countryIds.length > 0
        ? { countryId: { in: input.countryIds } }
        : {}),
      ...(input.departed !== undefined ? { departed: input.departed } : {}),
    },
    select: {
      id: true,
      eid: true,
      displayName: true,
      chapter: true,
      countryId: true,
      federalState: true,
      metroCityId: true,
      departed: true,
      chargeabilityTarget: true,
      country: {
        select: {
          id: true,
          code: true,
          name: true,
        },
      },
      metroCity: {
        select: {
          id: true,
          name: true,
        },
      },
      availability: true,
    },
  });
  const bookings = await listAssignmentBookings(db, {
    startDate: start,
    endDate: end,
    resourceIds: resources.map((resource) => resource.id),
  });
  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,
    })),
    start,
    end,
  );

  const stats: DashboardChargeabilityRow[] = resources.map((resource) => {
    const availability = resource.availability as unknown as WeekdayAvailability;
    const context = contexts.get(resource.id);
    const resourceBookings = bookings.filter((booking) => booking.resourceId === resource.id);
    const actualAllocations = resourceBookings.filter((booking) =>
      isChargeabilityActualBooking(booking, input.includeProposed === true),
    );
    const expectedAllocations = resourceBookings.filter(
      (booking) => isChargeabilityRelevantProject(booking.project, true),
    );
    const availableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: start,
      periodEnd: end,
      context,
    });
    const actualBookedHours = actualAllocations.reduce(
      (sum, allocation) => sum + calculateEffectiveAllocationHours({
        availability,
        startDate: allocation.startDate,
        endDate: allocation.endDate,
        hoursPerDay: allocation.hoursPerDay,
        periodStart: start,
        periodEnd: end,
        context,
      }),
      0,
    );
    const expectedBookedHours = expectedAllocations.reduce(
      (sum, allocation) => sum + calculateEffectiveAllocationHours({
        availability,
        startDate: allocation.startDate,
        endDate: allocation.endDate,
        hoursPerDay: allocation.hoursPerDay,
        periodStart: start,
        periodEnd: end,
        context,
      }),
      0,
    );
    const actualChargeability = availableHours > 0
      ? Math.min(100, Math.round((actualBookedHours / availableHours) * 100))
      : 0;
    const expectedChargeability = availableHours > 0
      ? Math.min(100, Math.round((expectedBookedHours / availableHours) * 100))
      : 0;
    const chargeabilityTarget = resource.chargeabilityTarget ?? 0;

    return {
      id: resource.id,
      eid: resource.eid,
      displayName: resource.displayName,
      chapter: resource.chapter,
      countryId: resource.countryId,
      countryCode: resource.country?.code ?? null,
      countryName: resource.country?.name ?? null,
      federalState: resource.federalState ?? null,
      metroCityName: resource.metroCity?.name ?? null,
      departed: resource.departed,
      chargeabilityTarget,
      actualChargeability,
      expectedChargeability,
      derivation: summarizeDerivation(
        availability,
        start,
        end,
        context,
        actualBookedHours,
        expectedBookedHours,
        chargeabilityTarget,
      ),
    };
  });

  return {
    rows: stats,
    top: [...stats]
      .sort((left, right) => right.actualChargeability - left.actualChargeability),
    watchlist: [...stats]
      .filter(
        (resource) =>
          resource.actualChargeability <
          resource.chargeabilityTarget - input.watchlistThreshold,
      )
      .sort((left, right) => left.actualChargeability - right.actualChargeability),
    month: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`,
  };
}
