import {
  calculateSAH,
  calculateAllocation,
  deriveResourceForecast,
  getMonthRange,
  DEFAULT_CALCULATION_RULES,
  type AssignmentSlice,
} from "@capakraken/engine";
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
import { VacationStatus } from "@capakraken/db";
import type { TRPCContext } from "../trpc.js";
import { fmtEur } from "../lib/format-utils.js";
import {
  asHolidayResolverDb,
  collectHolidayAvailability,
  getResolvedCalendarHolidays,
} from "../lib/holiday-availability.js";
import {
  calculateEffectiveAvailableHours,
  countEffectiveWorkingDays,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js";
import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js";

type ResourceGraphInput = {
  resourceId: string;
  month: string;
};

function getAvailabilityHoursForDate(
  availability: WeekdayAvailability,
  date: Date,
): number {
  const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability;
  return availability[dayKey] ?? 0;
}

function sumAvailabilityHoursForDates(
  availability: WeekdayAvailability,
  dates: Date[],
): number {
  return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
}

export async function readResourceGraphSnapshot(
  ctx: { db: TRPCContext["db"] },
  input: ResourceGraphInput,
) {
  const [year, month] = input.month.split("-").map(Number) as [number, number];
  const { start: monthStart, end: monthEnd } = getMonthRange(year, month);

  const resource = await ctx.db.resource.findUniqueOrThrow({
    where: { id: input.resourceId },
    select: {
      id: true,
      displayName: true,
      eid: true,
      fte: true,
      lcrCents: true,
      chargeabilityTarget: true,
      countryId: true,
      federalState: true,
      metroCityId: true,
      availability: true,
      country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
      metroCity: { select: { id: true, name: true } },
      managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
    },
  });

  const dailyHours = resource.country?.dailyWorkingHours ?? 8;
  const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
  const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);

  const avail = resource.availability as WeekdayAvailability | null;
  const weeklyAvailability: WeekdayAvailability = avail ?? {
    monday: dailyHours, tuesday: dailyHours, wednesday: dailyHours,
    thursday: dailyHours, friday: dailyHours, saturday: 0, sunday: 0,
  };

  const assignments = await ctx.db.assignment.findMany({
    where: {
      resourceId: input.resourceId,
      startDate: { lte: monthEnd },
      endDate: { gte: monthStart },
      status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
    },
    select: {
      id: true,
      hoursPerDay: true,
      startDate: true,
      endDate: true,
      dailyCostCents: true,
      status: true,
      project: {
        select: {
          id: true,
          name: true,
          shortCode: true,
          budgetCents: true,
          winProbability: true,
          utilizationCategory: { select: { code: true } },
        },
      },
    },
  });

  const vacations = await ctx.db.vacation.findMany({
    where: {
      resourceId: input.resourceId,
      status: VacationStatus.APPROVED,
      startDate: { lte: monthEnd },
      endDate: { gte: monthStart },
    },
    select: { startDate: true, endDate: true, type: true, isHalfDay: true },
  });
  const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
    periodStart: monthStart,
    periodEnd: monthEnd,
    countryId: resource.countryId,
    countryCode: resource.country?.code,
    federalState: resource.federalState,
    metroCityId: resource.metroCityId,
    metroCityName: resource.metroCity?.name,
  });
  const holidayAvailability = collectHolidayAvailability({
    vacations,
    periodStart: monthStart,
    periodEnd: monthEnd,
    countryCode: resource.country?.code,
    federalState: resource.federalState,
    metroCityName: resource.metroCity?.name,
    resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date),
  });
  const publicHolidayStrings = holidayAvailability.publicHolidayStrings;
  const absenceDateStrings = holidayAvailability.absenceDateStrings;
  const absenceDays = holidayAvailability.absenceDays;
  const halfDayCount = absenceDays.filter((absence) => absence.isHalfDay).length;
  const vacationDayCount = absenceDays.filter((absence) => absence.type === "VACATION").length;
  const sickDayCount = absenceDays.filter((absence) => absence.type === "SICK").length;
  const publicHolidayCount = resolvedHolidays.length;
  const absenceDayEquivalent = absenceDays.reduce((sum, absence) => {
    if (absence.type === "PUBLIC_HOLIDAY") {
      return sum;
    }
    return sum + (absence.isHalfDay ? 0.5 : 1);
  }, 0);

  const contexts = await loadResourceDailyAvailabilityContexts(
    ctx.db,
    [{
      id: resource.id,
      availability: weeklyAvailability,
      countryId: resource.countryId,
      countryCode: resource.country?.code,
      federalState: resource.federalState,
      metroCityId: resource.metroCityId,
      metroCityName: resource.metroCity?.name,
    }],
    monthStart,
    monthEnd,
  );
  const availabilityContext = contexts.get(resource.id);

  let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
  try {
    const dbRules = await ctx.db.calculationRule.findMany({
      where: { isActive: true },
      orderBy: [{ priority: "desc" }],
    });
    if (dbRules.length > 0) {
      calcRules = dbRules as unknown as CalculationRule[];
    }
  } catch {
    // table may not exist yet
  }

  const sahResult = calculateSAH({
    dailyWorkingHours: dailyHours,
    scheduleRules,
    fte: resource.fte,
    periodStart: monthStart,
    periodEnd: monthEnd,
    publicHolidays: publicHolidayStrings,
    absenceDays: absenceDateStrings,
  });
  const baseWorkingDays = countEffectiveWorkingDays({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: undefined,
  });
  const effectiveWorkingDays = countEffectiveWorkingDays({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: availabilityContext,
  });
  const baseAvailableHours = calculateEffectiveAvailableHours({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: undefined,
  });
  const effectiveAvailableHours = calculateEffectiveAvailableHours({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: availabilityContext,
  });
  const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`));
  const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => (
    count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0)
  ), 0);
  const publicHolidayHoursDeduction = sumAvailabilityHoursForDates(
    weeklyAvailability,
    publicHolidayDates,
  );
  const absenceHoursDeduction = absenceDays.reduce((sum, absence) => {
    if (absence.type === "PUBLIC_HOLIDAY") {
      return sum;
    }
    const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date);
    return sum + baseHours * (absence.isHalfDay ? 0.5 : 1);
  }, 0);
  const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0
    ? effectiveAvailableHours / effectiveWorkingDays
    : 0;
  const holidayScopeSummary = [
    resource.country?.code ?? "—",
    resource.federalState ?? "—",
    resource.metroCity?.name ?? "—",
  ].join(" / ");
  const holidayExamples = resolvedHolidays.length > 0
    ? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ")
    : "none";
  const holidayScopeBreakdown = resolvedHolidays.reduce<Record<string, number>>((counts, holiday) => {
    counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1;
    return counts;
  }, {});

  const slices: AssignmentSlice[] = [];
  const assignmentBreakdown: Array<{
    id: string;
    projectId: string;
    projectName: string;
    projectCode: string;
    status: string;
    bookedHours: number;
  }> = [];
  let totalAllocHours = 0;
  let totalAllocCostCents = 0;
  let totalChargeableHours = 0;
  let totalProjectCostCents = 0;
  let hasRulesEffect = false;

  for (const a of assignments) {
    const overlapStart = new Date(Math.max(monthStart.getTime(), a.startDate.getTime()));
    const overlapEnd = new Date(Math.min(monthEnd.getTime(), a.endDate.getTime()));
    const categoryCode = a.project.utilizationCategory?.code ?? "Chg";

    const calcResult = calculateAllocation({
      lcrCents: resource.lcrCents,
      hoursPerDay: a.hoursPerDay,
      startDate: overlapStart,
      endDate: overlapEnd,
      availability: weeklyAvailability,
      absenceDays,
      calculationRules: calcRules,
    });
    if (calcResult.workingDays <= 0 && calcResult.totalHours <= 0) continue;

    totalAllocHours += calcResult.totalHours;
    totalAllocCostCents += calcResult.totalCostCents;
    assignmentBreakdown.push({
      id: a.id,
      projectId: a.project.id,
      projectName: a.project.name,
      projectCode: a.project.shortCode,
      status: a.status,
      bookedHours: calcResult.totalHours,
    });
    if (calcResult.totalChargeableHours !== undefined) {
      totalChargeableHours += calcResult.totalChargeableHours;
      totalProjectCostCents += calcResult.totalProjectCostCents ?? calcResult.totalCostCents;
      hasRulesEffect = true;
    } else {
      totalChargeableHours += calcResult.totalHours;
      totalProjectCostCents += calcResult.totalCostCents;
    }

    slices.push({
      hoursPerDay: a.hoursPerDay,
      workingDays: calcResult.workingDays,
      categoryCode,
      ...(calcResult.totalChargeableHours !== undefined
        ? { totalChargeableHours: calcResult.totalChargeableHours }
        : {}),
    });
  }

  const forecast = deriveResourceForecast({
    fte: resource.fte,
    targetPercentage: targetPct,
    assignments: slices,
    sah: effectiveAvailableHours,
  });
  const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph(
    ctx.db,
    assignments,
    monthStart,
    monthEnd,
  );

  const dailyCostCents = assignments.length > 0
    ? Math.round(assignments[0]!.hoursPerDay * resource.lcrCents)
    : 0;
  const avgHoursPerDay = assignments.length > 0
    ? assignments.reduce((sum, a) => sum + a.hoursPerDay, 0) / assignments.length
    : 0;
  const totalWorkingDaysInMonth = assignments.reduce((sum, a) => {
    const overlapStart = new Date(Math.max(monthStart.getTime(), a.startDate.getTime()));
    const overlapEnd = new Date(Math.min(monthEnd.getTime(), a.endDate.getTime()));
    const calcResult = calculateAllocation({
      lcrCents: resource.lcrCents,
      hoursPerDay: a.hoursPerDay,
      startDate: overlapStart,
      endDate: overlapEnd,
      availability: weeklyAvailability,
      absenceDays,
      calculationRules: calcRules,
    });
    return sum + calcResult.workingDays;
  }, 0);

  const utilizationPct = effectiveAvailableHours > 0
    ? (totalAllocHours / effectiveAvailableHours) * 100
    : 0;
  const chargeableHours = forecast.chg * effectiveAvailableHours;
  return buildResourceGraphSnapshot({
    month: input.month,
    resource,
    dailyHours,
    scheduleRules,
    targetPct,
    weeklyAvailability,
    holidayScopeSummary,
    holidayExamples,
    holidayScopeBreakdown,
    calcRulesCount: calcRules.length,
    assignmentCount: assignments.length,
    absenceCount: absenceDateStrings.length,
    vacationDayCount,
    sickDayCount,
    halfDayCount,
    publicHolidayCount,
    publicHolidayWorkdayCount,
    publicHolidayHoursDeduction,
    absenceHoursDeduction,
    sahCalendarDays: sahResult.calendarDays,
    sahWeekendDays: sahResult.weekendDays,
    baseWorkingDays,
    effectiveWorkingDays,
    baseAvailableHours,
    effectiveAvailableHours,
    effectiveHoursPerWorkingDay,
    totalWorkingDaysInMonth,
    totalAllocHours,
    totalAllocCostCents,
    totalChargeableHours,
    totalProjectCostCents,
    hasRulesEffect,
    dailyCostCents,
    avgHoursPerDay,
    utilizationPct,
    forecast,
    chargeableHours,
    budgetNodes,
    budgetLinks,
    resolvedHolidays: resolvedHolidays.map((holiday) => ({
      date: holiday.date,
      name: holiday.name,
      scope: holiday.scope,
      calendarName: holiday.calendarName,
      sourceType: holiday.sourceType ?? null,
    })),
    assignmentBreakdown,
    absenceDayEquivalent,
  });
}
