import type { PrismaClient } from "@capakraken/db";
import type { WeekdayAvailability } from "@capakraken/shared";
import { calculateInclusiveDays } from "./shared.js";
import {
  calculateEffectiveAllocationCostCents,
  loadDailyAvailabilityContexts,
} from "./holiday-capacity.js";

export interface ProjectHealthRow {
  id: string;
  projectName: string;
  shortCode: string;
  status: string;
  clientId: string | null;
  clientName: string | null;
  budgetHealth: number;
  staffingHealth: number;
  timelineHealth: number;
  compositeScore: number;
  budgetCents?: number | null;
  spentCents?: number;
  remainingBudgetCents?: number | null;
  budgetUtilizationPercent?: number | null;
  demandHeadcountTotal?: number;
  demandHeadcountFilled?: number;
  demandHeadcountOpen?: number;
  demandRequirementCount?: number;
  plannedEndDate?: Date | null;
  daysUntilEndDate?: number | null;
  timelineStatus?: "ON_TRACK" | "DUE_SOON" | "OVERDUE" | "UNSCHEDULED";
  calendarLocations?: Array<{
    countryCode: string | null;
    countryName: string | null;
    federalState: string | null;
    metroCityName: string | null;
    assignmentCount: number;
    spentCents: number;
  }>;
  derivation?: {
    periodStart: string;
    periodEnd: string;
    calendarContextCount: number;
    holidayAwareAssignmentCount: number;
    fallbackAssignmentCount: number;
    baseSpentCents: number;
    adjustedSpentCents: number;
    publicHolidayDayEquivalent: number;
    publicHolidayCostDeductionCents: number;
    absenceDayEquivalent: number;
    absenceCostDeductionCents: number;
  };
}

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

function hasAvailability<T extends { availability?: unknown }>(
  resource: T | null | undefined,
): resource is T & { availability: WeekdayAvailability } {
  return resource !== null && resource !== undefined && resource.availability !== null && resource.availability !== undefined;
}

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 toUtcDayStart(value: Date): Date {
  return new Date(Date.UTC(
    value.getUTCFullYear(),
    value.getUTCMonth(),
    value.getUTCDate(),
  ));
}

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 summarizeSpentDerivation(input: {
  availability: WeekdayAvailability;
  startDate: Date;
  endDate: Date;
  dailyCostCents: number;
  context: Awaited<ReturnType<typeof loadDailyAvailabilityContexts>> extends Map<string, infer TValue>
    ? TValue | undefined
    : never;
}) {
  const baseSpentCents = calculateEffectiveAllocationCostCents({
    availability: input.availability,
    startDate: input.startDate,
    endDate: input.endDate,
    dailyCostCents: input.dailyCostCents,
    periodStart: input.startDate,
    periodEnd: input.endDate,
    context: undefined,
  });
  const adjustedSpentCents = calculateEffectiveAllocationCostCents({
    availability: input.availability,
    startDate: input.startDate,
    endDate: input.endDate,
    dailyCostCents: input.dailyCostCents,
    periodStart: input.startDate,
    periodEnd: input.endDate,
    context: input.context,
  });

  let publicHolidayDayEquivalent = 0;
  let publicHolidayCostDeductionCents = 0;
  let absenceDayEquivalent = 0;
  let absenceCostDeductionCents = 0;

  const cursor = new Date(input.startDate);
  cursor.setUTCHours(0, 0, 0, 0);
  const end = new Date(input.endDate);
  end.setUTCHours(0, 0, 0, 0);

  while (cursor <= end) {
    const baseHours = getDailyAvailabilityHours(input.availability, cursor);
    if (baseHours > 0) {
      const isoDate = toIsoDate(cursor);
      if (input.context?.holidayDates.has(isoDate)) {
        publicHolidayDayEquivalent += 1;
        publicHolidayCostDeductionCents += input.dailyCostCents;
      } else {
        const vacationFraction = Math.min(
          1,
          Math.max(0, input.context?.vacationFractionsByDate.get(isoDate) ?? 0),
        );
        if (vacationFraction > 0) {
          absenceDayEquivalent += vacationFraction;
          absenceCostDeductionCents += input.dailyCostCents * vacationFraction;
        }
      }
    }
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }

  return {
    baseSpentCents,
    adjustedSpentCents,
    publicHolidayDayEquivalent,
    publicHolidayCostDeductionCents: Math.round(publicHolidayCostDeductionCents),
    absenceDayEquivalent,
    absenceCostDeductionCents: Math.round(absenceCostDeductionCents),
  };
}

export async function getDashboardProjectHealth(
  db: PrismaClient,
): Promise<ProjectHealthRow[]> {
  const projects = await db.project.findMany({
    where: { status: "ACTIVE" },
    select: {
      id: true,
      name: true,
      shortCode: true,
      status: true,
      budgetCents: true,
      endDate: true,
      clientId: true,
      client: { select: { name: true } },
      demandRequirements: {
        select: {
          id: true,
          headcount: true,
          status: true,
          assignments: {
            where: { status: { not: "CANCELLED" } },
            select: { id: true },
          },
        },
      },
    },
  });

  if (projects.length === 0) return [];

  const projectIds = projects.map((p) => p.id);

  // Fetch assignments for budget calculation
  const assignments = await db.assignment.findMany({
    where: {
      projectId: { in: projectIds },
      status: { not: "CANCELLED" },
    },
    select: {
      projectId: true,
      startDate: true,
      endDate: true,
      dailyCostCents: true,
        resource: {
          select: {
            id: true,
            availability: true,
            countryId: true,
            federalState: true,
            metroCityId: true,
            country: { select: { code: true, name: true } },
            metroCity: { select: { name: true } },
          },
        },
      },
  });

  const contextStart = assignments.length > 0
    ? new Date(Math.min(...assignments.map((assignment) => assignment.startDate.getTime())))
    : new Date();
  const contextEnd = assignments.length > 0
    ? new Date(Math.max(...assignments.map((assignment) => assignment.endDate.getTime())))
    : new Date();
  const contexts = await loadDailyAvailabilityContexts(
    db,
    assignments
      .map((assignment) => assignment.resource)
      .filter(hasAvailability)
      .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,
      })),
    contextStart,
    contextEnd,
  );
  const spentByProject = new Map<string, number>();
  const derivationByProject = new Map<string, {
    periodStart: string;
    periodEnd: string;
    calendarContextCount: number;
    holidayAwareAssignmentCount: number;
    fallbackAssignmentCount: number;
    baseSpentCents: number;
    adjustedSpentCents: number;
    publicHolidayDayEquivalent: number;
    publicHolidayCostDeductionCents: number;
    absenceDayEquivalent: number;
    absenceCostDeductionCents: number;
  }>();
  const calendarLocationsByProject = new Map<string, Map<string, NonNullable<ProjectHealthRow["calendarLocations"]>[number]>>();
  for (const a of assignments) {
    const derivation = hasAvailability(a.resource)
      ? summarizeSpentDerivation({
        availability: a.resource.availability as unknown as WeekdayAvailability,
        startDate: a.startDate,
        endDate: a.endDate,
        dailyCostCents: a.dailyCostCents ?? 0,
        context: contexts.get(a.resource.id),
      })
      : null;
    const cost = derivation?.adjustedSpentCents
      ?? (a.dailyCostCents ?? 0) * calculateInclusiveDays(a.startDate, a.endDate);
    spentByProject.set(a.projectId, (spentByProject.get(a.projectId) ?? 0) + cost);
    const existingDerivation = derivationByProject.get(a.projectId) ?? {
      periodStart: toIsoDate(a.startDate),
      periodEnd: toIsoDate(a.endDate),
      calendarContextCount: 0,
      holidayAwareAssignmentCount: 0,
      fallbackAssignmentCount: 0,
      baseSpentCents: 0,
      adjustedSpentCents: 0,
      publicHolidayDayEquivalent: 0,
      publicHolidayCostDeductionCents: 0,
      absenceDayEquivalent: 0,
      absenceCostDeductionCents: 0,
    };
    if (a.startDate < new Date(existingDerivation.periodStart)) {
      existingDerivation.periodStart = toIsoDate(a.startDate);
    }
    if (a.endDate > new Date(existingDerivation.periodEnd)) {
      existingDerivation.periodEnd = toIsoDate(a.endDate);
    }
    if (derivation) {
      existingDerivation.calendarContextCount += 1;
      existingDerivation.holidayAwareAssignmentCount += 1;
      existingDerivation.baseSpentCents += derivation.baseSpentCents;
      existingDerivation.adjustedSpentCents += derivation.adjustedSpentCents;
      existingDerivation.publicHolidayDayEquivalent += derivation.publicHolidayDayEquivalent;
      existingDerivation.publicHolidayCostDeductionCents += derivation.publicHolidayCostDeductionCents;
      existingDerivation.absenceDayEquivalent += derivation.absenceDayEquivalent;
      existingDerivation.absenceCostDeductionCents += derivation.absenceCostDeductionCents;
    } else {
      existingDerivation.fallbackAssignmentCount += 1;
      existingDerivation.baseSpentCents += cost;
      existingDerivation.adjustedSpentCents += cost;
    }
    derivationByProject.set(a.projectId, existingDerivation);
    if (a.resource) {
      const projectLocations = calendarLocationsByProject.get(a.projectId) ?? new Map();
      const locationKey = buildLocationKey({
        countryCode: a.resource.country?.code,
        countryName: a.resource.country?.name,
        federalState: a.resource.federalState,
        metroCityName: a.resource.metroCity?.name,
      });
      const summary = projectLocations.get(locationKey) ?? {
        countryCode: a.resource.country?.code ?? null,
        countryName: a.resource.country?.name ?? null,
        federalState: a.resource.federalState ?? null,
        metroCityName: a.resource.metroCity?.name ?? null,
        assignmentCount: 0,
        spentCents: 0,
      };
      summary.assignmentCount += 1;
      summary.spentCents += cost;
      projectLocations.set(locationKey, summary);
      calendarLocationsByProject.set(a.projectId, projectLocations);
    }
  }

  const now = new Date();

  const rows: ProjectHealthRow[] = projects.map((p) => {
    // Budget health: 100 - pctUsed (capped at 100)
    const spentCents = spentByProject.get(p.id) ?? 0;
    const pctUsed =
      (p.budgetCents ?? 0) > 0
        ? Math.round((spentCents / p.budgetCents) * 100)
        : 0;
    const budgetHealth = Math.max(0, 100 - Math.min(pctUsed, 100));

    // Staffing health: filledDemands / totalDemands * 100
    let totalDemands = 0;
    let filledDemands = 0;
    for (const dr of p.demandRequirements) {
      totalDemands += dr.headcount;
      filledDemands += Math.min(dr.assignments.length, dr.headcount);
    }
    const staffingHealth =
      totalDemands > 0 ? Math.round((filledDemands / totalDemands) * 100) : 100;

    // Timeline health: 100 if end date > today, else 0
    const endDate = p.endDate ? toUtcDayStart(p.endDate) : null;
    const today = toUtcDayStart(now);
    const daysUntilEndDate = endDate
      ? Math.round((endDate.getTime() - today.getTime()) / 86_400_000)
      : null;
    const timelineStatus = endDate === null
      ? "UNSCHEDULED"
      : daysUntilEndDate! < 0
        ? "OVERDUE"
        : daysUntilEndDate! <= 14
          ? "DUE_SOON"
          : "ON_TRACK";
    const timelineHealth = endDate === null
      ? 100
      : endDate > today
        ? 100
        : 0;

    // Composite = average of 3 dimensions
    const compositeScore = Math.round(
      (budgetHealth + staffingHealth + timelineHealth) / 3,
    );
    const remainingBudgetCents = p.budgetCents == null ? null : p.budgetCents - spentCents;
    const derivation = derivationByProject.get(p.id);

    return {
      id: p.id,
      projectName: p.name,
      shortCode: p.shortCode,
      status: p.status,
      clientId: p.clientId,
      clientName: p.client?.name ?? null,
      budgetHealth,
      staffingHealth,
      timelineHealth,
      compositeScore,
      budgetCents: p.budgetCents ?? null,
      spentCents,
      remainingBudgetCents,
      budgetUtilizationPercent: p.budgetCents && p.budgetCents > 0 ? pctUsed : null,
      demandHeadcountTotal: totalDemands,
      demandHeadcountFilled: filledDemands,
      demandHeadcountOpen: Math.max(totalDemands - filledDemands, 0),
      demandRequirementCount: p.demandRequirements.length,
      plannedEndDate: p.endDate ?? null,
      daysUntilEndDate,
      timelineStatus,
      calendarLocations: Array.from(calendarLocationsByProject.get(p.id)?.values() ?? [])
        .sort((left, right) => right.spentCents - left.spentCents),
      ...(derivation ? { derivation } : {}),
    };
  });

  rows.sort((a, b) => a.compositeScore - b.compositeScore);
  return rows;
}
