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

export interface BudgetForecastLocationSummary {
  countryCode: string | null;
  countryName: string | null;
  federalState: string | null;
  metroCityName: string | null;
  activeAssignmentCount: number;
  burnRateCents: number;
}

export interface BudgetForecastDerivationSummary {
  periodStart: string;
  periodEnd: string;
  calendarContextCount: number;
  holidayAwareAssignmentCount: number;
  fallbackAssignmentCount: number;
  baseBurnRateCents: number;
  adjustedBurnRateCents: number;
  publicHolidayDayEquivalent: number;
  publicHolidayCostDeductionCents: number;
  absenceDayEquivalent: number;
  absenceCostDeductionCents: number;
}

export interface BudgetForecastRow {
  projectId?: string;
  projectName: string;
  shortCode: string;
  clientId: string | null;
  clientName: string | null;
  budgetCents: number;
  spentCents: number;
  remainingCents?: number;
  burnRate: number;
  estimatedExhaustionDate: string | null;
  pctUsed: number;
  activeAssignmentCount?: number;
  calendarLocations?: BudgetForecastLocationSummary[];
  derivation?: BudgetForecastDerivationSummary;
}

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

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

  const overlapStart = new Date(
    Math.max(input.startDate.getTime(), input.periodStart.getTime()),
  );
  const overlapEnd = new Date(
    Math.min(input.endDate.getTime(), input.periodEnd.getTime()),
  );

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

  if (overlapStart <= overlapEnd) {
    const cursor = new Date(overlapStart);
    cursor.setUTCHours(0, 0, 0, 0);
    const end = new Date(overlapEnd);
    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 {
    baseBurnRateCents,
    adjustedBurnRateCents,
    publicHolidayDayEquivalent,
    publicHolidayCostDeductionCents: Math.round(publicHolidayCostDeductionCents),
    absenceDayEquivalent,
    absenceCostDeductionCents: Math.round(absenceCostDeductionCents),
  };
}

export async function getDashboardBudgetForecast(
  db: PrismaClient,
): Promise<BudgetForecastRow[]> {
  const projects = await db.project.findMany({
    where: { status: "ACTIVE" },
    select: {
      id: true,
      name: true,
      shortCode: true,
      budgetCents: true,
      startDate: true,
      endDate: true,
      clientId: true,
      client: { select: { name: true } },
    },
  });

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

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

  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 now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
  const contextStart = assignments.length > 0
    ? new Date(
      Math.min(
        ...assignments.map((assignment) => assignment.startDate.getTime()),
        monthStart.getTime(),
      ),
    )
    : monthStart;
  const contextEnd = assignments.length > 0
    ? new Date(
      Math.max(
        ...assignments.map((assignment) => assignment.endDate.getTime()),
        monthEnd.getTime(),
      ),
    )
    : monthEnd;
  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 monthlyBurnByProject = new Map<string, number>();
  const activeAssignmentCountByProject = new Map<string, number>();
  const activeLocationsByProject = new Map<string, Map<string, BudgetForecastLocationSummary>>();
  const derivationByProject = new Map<string, Omit<BudgetForecastDerivationSummary, "periodStart" | "periodEnd" | "calendarContextCount">>();

  for (const assignment of assignments) {
    const totalCost = hasAvailability(assignment.resource)
      ? calculateEffectiveAllocationCostCents({
        availability: assignment.resource.availability as unknown as WeekdayAvailability,
        startDate: assignment.startDate,
        endDate: assignment.endDate,
        dailyCostCents: assignment.dailyCostCents ?? 0,
        periodStart: assignment.startDate,
        periodEnd: assignment.endDate,
        context: contexts.get(assignment.resource.id),
      })
      : (assignment.dailyCostCents ?? 0)
        * calculateInclusiveDays(assignment.startDate, assignment.endDate);

    spentByProject.set(
      assignment.projectId,
      (spentByProject.get(assignment.projectId) ?? 0) + totalCost,
    );

    if (assignment.startDate <= now && assignment.endDate >= now) {
      const derivation = hasAvailability(assignment.resource)
        ? summarizeBurnDerivation({
          availability: assignment.resource.availability as unknown as WeekdayAvailability,
          startDate: assignment.startDate,
          endDate: assignment.endDate,
          dailyCostCents: assignment.dailyCostCents ?? 0,
          periodStart: monthStart,
          periodEnd: monthEnd,
          context: contexts.get(assignment.resource.id),
        })
        : null;
      const monthlyContribution = derivation?.adjustedBurnRateCents ?? (assignment.dailyCostCents ?? 0) * 22;
      const baseMonthlyContribution = derivation?.baseBurnRateCents ?? monthlyContribution;
      monthlyBurnByProject.set(
        assignment.projectId,
        (monthlyBurnByProject.get(assignment.projectId) ?? 0) + monthlyContribution,
      );
      activeAssignmentCountByProject.set(
        assignment.projectId,
        (activeAssignmentCountByProject.get(assignment.projectId) ?? 0) + 1,
      );
      const existingDerivation = derivationByProject.get(assignment.projectId) ?? {
        holidayAwareAssignmentCount: 0,
        fallbackAssignmentCount: 0,
        baseBurnRateCents: 0,
        adjustedBurnRateCents: 0,
        publicHolidayDayEquivalent: 0,
        publicHolidayCostDeductionCents: 0,
        absenceDayEquivalent: 0,
        absenceCostDeductionCents: 0,
      };
      existingDerivation.baseBurnRateCents += baseMonthlyContribution;
      existingDerivation.adjustedBurnRateCents += monthlyContribution;
      if (derivation) {
        existingDerivation.holidayAwareAssignmentCount += 1;
        existingDerivation.publicHolidayDayEquivalent += derivation.publicHolidayDayEquivalent;
        existingDerivation.publicHolidayCostDeductionCents += derivation.publicHolidayCostDeductionCents;
        existingDerivation.absenceDayEquivalent += derivation.absenceDayEquivalent;
        existingDerivation.absenceCostDeductionCents += derivation.absenceCostDeductionCents;
      } else {
        existingDerivation.fallbackAssignmentCount += 1;
      }
      derivationByProject.set(assignment.projectId, existingDerivation);

      const locationSummaries = activeLocationsByProject.get(assignment.projectId) ?? new Map();
      const locationKey = buildLocationKey({
        countryCode: assignment.resource.country?.code,
        countryName: assignment.resource.country?.name,
        federalState: assignment.resource.federalState,
        metroCityName: assignment.resource.metroCity?.name,
      });
      const summary = locationSummaries.get(locationKey) ?? {
        countryCode: assignment.resource.country?.code ?? null,
        countryName: assignment.resource.country?.name ?? null,
        federalState: assignment.resource.federalState ?? null,
        metroCityName: assignment.resource.metroCity?.name ?? null,
        activeAssignmentCount: 0,
        burnRateCents: 0,
      };
      summary.activeAssignmentCount += 1;
      summary.burnRateCents += monthlyContribution;
      locationSummaries.set(locationKey, summary);
      activeLocationsByProject.set(assignment.projectId, locationSummaries);
    }
  }

  const rows: BudgetForecastRow[] = projects.map((project) => {
    const spentCents = spentByProject.get(project.id) ?? 0;
    const burnRate = monthlyBurnByProject.get(project.id) ?? 0;
    const remainingCents = Math.max(0, project.budgetCents - spentCents);
    const pctUsed = project.budgetCents > 0
      ? Math.round((spentCents / project.budgetCents) * 100)
      : 0;

    let estimatedExhaustionDate: string | null = null;
    if (burnRate > 0 && project.budgetCents > spentCents) {
      const monthsRemaining = remainingCents / burnRate;
      const exhaustionDate = new Date(
        now.getTime() + monthsRemaining * 30 * MILLISECONDS_PER_DAY,
      );
      estimatedExhaustionDate = exhaustionDate.toISOString().slice(0, 10);
    }

    const derivation = derivationByProject.get(project.id);

    return {
      projectId: project.id,
      projectName: project.name,
      shortCode: project.shortCode,
      clientId: project.clientId,
      clientName: project.client?.name ?? null,
      budgetCents: project.budgetCents,
      spentCents,
      remainingCents,
      burnRate,
      estimatedExhaustionDate,
      pctUsed,
      activeAssignmentCount: activeAssignmentCountByProject.get(project.id) ?? 0,
      calendarLocations: Array.from(activeLocationsByProject.get(project.id)?.values() ?? [])
        .sort((left, right) => right.burnRateCents - left.burnRateCents),
      ...(derivation
        ? {
          derivation: {
            periodStart: toIsoDate(monthStart),
            periodEnd: toIsoDate(monthEnd),
            calendarContextCount: activeLocationsByProject.get(project.id)?.size ?? 0,
            ...derivation,
          },
        }
        : {}),
    };
  });

  rows.sort((left, right) => right.pctUsed - left.pctUsed);
  return rows;
}
