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 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[];
}

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,
  });
}

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>>();

  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 monthlyContribution = hasAvailability(assignment.resource)
        ? calculateEffectiveAllocationCostCents({
          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),
        })
        : (assignment.dailyCostCents ?? 0) * 22;
      monthlyBurnByProject.set(
        assignment.projectId,
        (monthlyBurnByProject.get(assignment.projectId) ?? 0) + monthlyContribution,
      );
      activeAssignmentCountByProject.set(
        assignment.projectId,
        (activeAssignmentCountByProject.get(assignment.projectId) ?? 0) + 1,
      );

      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);
    }

    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),
    };
  });

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