import { rankResources } from "@capakraken/staffing";
import { listAssignmentBookings } from "@capakraken/application";
import { PermissionKey, type WeekdayAvailability } from "@capakraken/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  countEffectiveWorkingDays,
  loadResourceDailyAvailabilityContexts,
  type ResourceDailyAvailabilityContext,
} from "../lib/resource-capacity.js";
import { fmtEur } from "../lib/format-utils.js";
import { createTRPCRouter, planningReadProcedure, requirePermission } from "../trpc.js";

const DAY_KEYS: (keyof WeekdayAvailability)[] = [
  "sunday",
  "monday",
  "tuesday",
  "wednesday",
  "thursday",
  "friday",
  "saturday",
];
const ACTIVE_STATUSES = new Set(["PROPOSED", "CONFIRMED", "ACTIVE"]);

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

function fmtDate(value: Date | null | undefined): string | null {
  return value ? value.toISOString().slice(0, 10) : null;
}

function createUtcDate(year: number, monthIndex: number, day: number): Date {
  return new Date(Date.UTC(year, monthIndex, day));
}

function normalizeUtcDate(value: Date): Date {
  return createUtcDate(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate());
}

function createDateRange(input: {
  startDate?: Date | undefined;
  endDate?: Date | undefined;
  durationDays?: number | undefined;
}): { startDate: Date; endDate: Date } {
  const startDate = input.startDate
    ? normalizeUtcDate(input.startDate)
    : createUtcDate(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate());
  const endDate = input.endDate
    ? normalizeUtcDate(input.endDate)
    : createUtcDate(
        startDate.getUTCFullYear(),
        startDate.getUTCMonth(),
        startDate.getUTCDate() + Math.max((input.durationDays ?? 21) - 1, 0),
      );

  if (endDate < startDate) {
    throw new Error("endDate must be on or after startDate.");
  }

  return { startDate, endDate };
}

function round1(value: number): number {
  return Math.round(value * 10) / 10;
}

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

function getEffectiveDayAvailability(
  availability: WeekdayAvailability,
  date: Date,
  context: ResourceDailyAvailabilityContext | undefined,
): number {
  const key = DAY_KEYS[date.getUTCDay()];
  const baseHours = key ? (availability[key] ?? 0) : 0;
  if (baseHours <= 0) {
    return 0;
  }
  const fraction = context?.absenceFractionsByDate.get(toIsoDate(date)) ?? 0;
  return Math.max(0, baseHours * (1 - fraction));
}

function overlapsDateRange(startDate: Date, endDate: Date, date: Date): boolean {
  return date >= startDate && date <= endDate;
}

function averagePerWorkingDay(totalHours: number, workingDays: number): number {
  if (workingDays <= 0) {
    return 0;
  }
  return round1(totalHours / workingDays);
}

function createLocationLabel(input: {
  countryCode?: string | null;
  federalState?: string | null;
  metroCityName?: string | null;
}): string {
  return [
    input.countryCode ?? null,
    input.federalState ?? null,
    input.metroCityName ?? null,
  ].filter((value): value is string => Boolean(value && value.trim().length > 0)).join(" / ");
}

function calculateAllocatedHoursForDay(input: {
  bookings: Array<{ startDate: Date; endDate: Date; hoursPerDay: number; status: string; isChargeable?: boolean }>;
  date: Date;
  context: ResourceDailyAvailabilityContext | undefined;
}): { allocatedHours: number; chargeableHours: number } {
  const isoDate = toIsoDate(input.date);
  const dayFraction = Math.max(0, 1 - (input.context?.absenceFractionsByDate.get(isoDate) ?? 0));

  return input.bookings.reduce(
    (acc, booking) => {
      if (!ACTIVE_STATUSES.has(booking.status) || !overlapsDateRange(booking.startDate, booking.endDate, input.date)) {
        return acc;
      }

      const effectiveHours = booking.hoursPerDay * dayFraction;
      acc.allocatedHours += effectiveHours;
      if (booking.isChargeable) {
        acc.chargeableHours += effectiveHours;
      }
      return acc;
    },
    { allocatedHours: 0, chargeableHours: 0 },
  );
}

type StaffingSuggestionInput = {
  requiredSkills: string[];
  preferredSkills?: string[] | undefined;
  startDate: Date;
  endDate: Date;
  hoursPerDay: number;
  budgetLcrCentsPerHour?: number | undefined;
  chapter?: string | undefined;
  skillCategory?: string | undefined;
  mainSkillsOnly?: boolean | undefined;
  minProficiency?: number | undefined;
};

type StaffingSuggestionsDbClient =
  Parameters<typeof listAssignmentBookings>[0]
  & Parameters<typeof loadResourceDailyAvailabilityContexts>[0]
  & {
    resource: {
      findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
    };
  };

type StaffingResourceRecord = {
  id: string;
  displayName: string;
  eid: string;
  fte: number | null;
  chapter: string | null;
  skills: unknown;
  lcrCents: number | null;
  chargeabilityTarget: number | null;
  availability: unknown;
  valueScore: number | null;
  countryId: string | null;
  federalState: string | null;
  metroCityId: string | null;
  country: { code: string; name: string } | null;
  metroCity: { name: string } | null;
  areaRole: { name: string } | null;
};

type BestProjectResourceRankingMode =
  | "lowest_lcr"
  | "highest_remaining_hours_per_day"
  | "highest_remaining_hours";

type BestProjectResourceInput = {
  projectId: string;
  startDate: Date;
  endDate: Date;
  minHoursPerDay: number;
  rankingMode: BestProjectResourceRankingMode;
  chapter?: string | undefined;
  roleName?: string | undefined;
};

type BestProjectResourceDbClient =
  Parameters<typeof loadResourceDailyAvailabilityContexts>[0]
  & {
    assignment: {
      findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
    };
  };

type BestProjectResourceDetailDbClient = BestProjectResourceDbClient & {
  project: {
    findUnique: (args: Record<string, unknown>) => Promise<unknown>;
  };
};

type BestProjectResourceAssignmentRecord = {
  resourceId: string;
  hoursPerDay: number;
  startDate: Date;
  endDate: Date;
  status: string;
  resource: {
    id: string;
    eid: string;
    displayName: string;
    chapter: string | null;
    lcrCents: number | null;
    availability: unknown;
    countryId: string | null;
    federalState: string | null;
    metroCityId: string | null;
    country: { code: string; name: string } | null;
    metroCity: { name: string } | null;
    areaRole: { name: string } | null;
  };
};

type BestProjectResourceOverlapAssignmentRecord = {
  resourceId: string;
  projectId: string;
  hoursPerDay: number;
  startDate: Date;
  endDate: Date;
  status: string;
  project: { name: string; shortCode: string | null } | null;
};

async function queryStaffingSuggestions(
  db: StaffingSuggestionsDbClient,
  input: StaffingSuggestionInput,
) {
  const {
    requiredSkills,
    preferredSkills,
    startDate,
    endDate,
    hoursPerDay,
    budgetLcrCentsPerHour,
    chapter,
    skillCategory,
    mainSkillsOnly,
    minProficiency,
  } = input;

  const resources = await db.resource.findMany({
    where: {
      isActive: true,
      ...(chapter ? { chapter } : {}),
    },
    select: {
      id: true,
      displayName: true,
      eid: true,
      fte: true,
      chapter: true,
      skills: true,
      lcrCents: true,
      chargeabilityTarget: true,
      availability: true,
      valueScore: true,
      countryId: true,
      federalState: true,
      metroCityId: true,
      country: { select: { code: true, name: true } },
      metroCity: { select: { name: true } },
      areaRole: { select: { name: true } },
    },
  }) as StaffingResourceRecord[];
  const bookings = await listAssignmentBookings(db, {
    startDate,
    endDate,
    resourceIds: resources.map((resource) => resource.id),
  });
  const contexts = await loadResourceDailyAvailabilityContexts(
    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,
    })),
    startDate,
    endDate,
  );
  const bookingsByResourceId = new Map<string, typeof bookings>();
  for (const booking of bookings) {
    if (!booking.resourceId) {
      continue;
    }
    const items = bookingsByResourceId.get(booking.resourceId) ?? [];
    items.push(booking);
    bookingsByResourceId.set(booking.resourceId, items);
  }

  const enrichedResources = resources.map((resource) => {
    const availability = resource.availability as unknown as WeekdayAvailability;
    const context = contexts.get(resource.id);
    const resourceBookings = bookingsByResourceId.get(resource.id) ?? [];
    const activeBookings = resourceBookings.filter((booking) => ACTIVE_STATUSES.has(booking.status));
    const baseAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: startDate,
      periodEnd: endDate,
      context: undefined,
    });
    const totalAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: startDate,
      periodEnd: endDate,
      context,
    });
    const baseWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart: startDate,
      periodEnd: endDate,
      context: undefined,
    });
    const effectiveWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart: startDate,
      periodEnd: endDate,
      context,
    });
    const allocatedHours = activeBookings.reduce(
      (sum, booking) =>
        sum + calculateEffectiveBookedHours({
          availability,
          startDate: booking.startDate,
          endDate: booking.endDate,
          hoursPerDay: booking.hoursPerDay,
          periodStart: startDate,
          periodEnd: endDate,
          context,
        }),
      0,
    );
    const holidayDates = [...(context?.holidayDates ?? new Set<string>())].sort();
    const holidayWorkdayCount = holidayDates.reduce((count, isoDate) => (
      count + (getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
    ), 0);
    const holidayHoursDeduction = holidayDates.reduce((sum, isoDate) => (
      sum + getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`))
    ), 0);
    let absenceDayEquivalent = 0;
    let absenceHoursDeduction = 0;
    for (const [isoDate, fraction] of context?.vacationFractionsByDate ?? []) {
      const dayHours = getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`));
      if (dayHours <= 0 || context?.holidayDates.has(isoDate)) {
        continue;
      }
      absenceDayEquivalent += fraction;
      absenceHoursDeduction += dayHours * fraction;
    }
    const conflictDays: string[] = [];
    const conflictDetails: Array<{
      date: string;
      baseHours: number;
      effectiveHours: number;
      allocatedHours: number;
      remainingHours: number;
      requestedHours: number;
      shortageHours: number;
      absenceFraction: number;
      isHoliday: boolean;
    }> = [];
    const cursor = new Date(startDate);
    cursor.setUTCHours(0, 0, 0, 0);
    const periodEndAtMidnight = new Date(endDate);
    periodEndAtMidnight.setUTCHours(0, 0, 0, 0);

    while (cursor <= periodEndAtMidnight) {
      const isoDate = toIsoDate(cursor);
      const baseHoursForDay = getBaseDayAvailability(availability, cursor);
      const availableHoursForDay = getEffectiveDayAvailability(availability, cursor, context);
      const isHoliday = context?.holidayDates.has(isoDate) ?? false;
      const absenceFraction = Math.min(
        1,
        Math.max(0, context?.absenceFractionsByDate.get(isoDate) ?? 0),
      );
      if (availableHoursForDay > 0) {
        const { allocatedHours: allocatedHoursForDay } = calculateAllocatedHoursForDay({
          bookings: activeBookings,
          date: cursor,
          context,
        });
        if (allocatedHoursForDay + hoursPerDay > availableHoursForDay) {
          const remainingHoursForDay = Math.max(0, availableHoursForDay - allocatedHoursForDay);
          conflictDays.push(isoDate);
          conflictDetails.push({
            date: isoDate,
            baseHours: round1(baseHoursForDay),
            effectiveHours: round1(availableHoursForDay),
            allocatedHours: round1(allocatedHoursForDay),
            remainingHours: round1(remainingHoursForDay),
            requestedHours: round1(hoursPerDay),
            shortageHours: round1(Math.max(0, hoursPerDay - remainingHoursForDay)),
            absenceFraction: round1(absenceFraction),
            isHoliday,
          });
        }
      }
      cursor.setUTCDate(cursor.getUTCDate() + 1);
    }

    const remainingHours = Math.max(0, totalAvailableHours - allocatedHours);
    const remainingHoursPerDay = averagePerWorkingDay(remainingHours, effectiveWorkingDays);
    const utilizationPercent =
      totalAvailableHours > 0
        ? Math.min(100, (allocatedHours / totalAvailableHours) * 100)
        : 0;

    type SkillRow = { skill: string; category?: string; proficiency: number; isMainSkill?: boolean };
    let skills = resource.skills as unknown as SkillRow[];

    if (mainSkillsOnly) skills = skills.filter((s) => s.isMainSkill);
    if (skillCategory) skills = skills.filter((s) => s.category === skillCategory);
    if (minProficiency) skills = skills.filter((s) => s.proficiency >= minProficiency);

    return {
      id: resource.id,
      displayName: resource.displayName,
      eid: resource.eid,
      fte: resource.fte,
      chapter: resource.chapter,
      role: resource.areaRole?.name ?? null,
      skills: skills as unknown as import("@capakraken/shared").SkillEntry[],
      lcrCents: resource.lcrCents,
      chargeabilityTarget: resource.chargeabilityTarget,
      currentUtilizationPercent: utilizationPercent,
      hasAvailabilityConflicts: conflictDays.length > 0,
      conflictDays,
      valueScore: resource.valueScore ?? 0,
      transparency: {
        location: {
          countryCode: resource.country?.code ?? null,
          countryName: resource.country?.name ?? null,
          federalState: resource.federalState ?? null,
          metroCityName: resource.metroCity?.name ?? null,
          label: createLocationLabel({
            countryCode: resource.country?.code ?? null,
            federalState: resource.federalState,
            metroCityName: resource.metroCity?.name ?? null,
          }),
        },
        capacity: {
          requestedHoursPerDay: round1(hoursPerDay),
          requestedHoursTotal: round1(effectiveWorkingDays * hoursPerDay),
          baseWorkingDays: round1(baseWorkingDays),
          effectiveWorkingDays: round1(effectiveWorkingDays),
          baseAvailableHours: round1(baseAvailableHours),
          effectiveAvailableHours: round1(totalAvailableHours),
          bookedHours: round1(allocatedHours),
          remainingHours: round1(remainingHours),
          remainingHoursPerDay,
          holidayCount: holidayDates.length,
          holidayWorkdayCount,
          holidayHoursDeduction: round1(holidayHoursDeduction),
          absenceDayEquivalent: round1(absenceDayEquivalent),
          absenceHoursDeduction: round1(absenceHoursDeduction),
        },
        conflicts: {
          count: conflictDays.length,
          conflictDays,
          details: conflictDetails,
        },
      },
    };
  });

  const ranked = rankResources({
    requiredSkills,
    preferredSkills,
    resources: enrichedResources,
    budgetLcrCentsPerHour,
  } as unknown as Parameters<typeof rankResources>[0]);
  const baseRankIndex = new Map(ranked.map((suggestion, index) => [suggestion.resourceId, index]));

  return [...ranked].sort((a, b) => {
    if (Math.abs(a.score - b.score) <= 2) {
      const aVal = (enrichedResources.find((resource) => resource.id === a.resourceId)?.valueScore ?? 0);
      const bVal = (enrichedResources.find((resource) => resource.id === b.resourceId)?.valueScore ?? 0);
      return bVal - aVal;
    }
    return 0;
  }).map((suggestion, index) => {
    const resource = enrichedResources.find((entry) => entry.id === suggestion.resourceId);
    const fallbackBreakdown = "breakdown" in suggestion
      ? (suggestion as { breakdown?: { skillScore: number; availabilityScore: number; costScore: number; utilizationScore: number } }).breakdown
      : undefined;
    const scoreBreakdown = suggestion.scoreBreakdown ?? {
      skillScore: fallbackBreakdown?.skillScore ?? 0,
      availabilityScore: fallbackBreakdown?.availabilityScore ?? 0,
      costScore: fallbackBreakdown?.costScore ?? 0,
      utilizationScore: fallbackBreakdown?.utilizationScore ?? 0,
      total: suggestion.score,
    };
    const baseRank = (baseRankIndex.get(suggestion.resourceId) ?? index) + 1;
    const tieBreakerApplied = baseRank !== index + 1;

    return {
      ...suggestion,
      resourceName: suggestion.resourceName ?? resource?.displayName ?? "",
      eid: suggestion.eid ?? resource?.eid ?? "",
      fte: resource?.fte ?? 0,
      chapter: resource?.chapter ?? null,
      role: resource?.role ?? null,
      scoreBreakdown,
      matchedSkills: suggestion.matchedSkills ?? requiredSkills.filter((skill) =>
        resource?.skills.some((entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase()),
      ),
      missingSkills: suggestion.missingSkills ?? requiredSkills.filter((skill) =>
        !resource?.skills.some((entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase()),
      ),
      availabilityConflicts: suggestion.availabilityConflicts ?? resource?.conflictDays ?? [],
      estimatedDailyCostCents: suggestion.estimatedDailyCostCents ?? ((resource?.lcrCents ?? 0) * 8),
      currentUtilization: suggestion.currentUtilization ?? round1(resource?.currentUtilizationPercent ?? 0),
      valueScore: resource?.valueScore ?? 0,
      location: resource?.transparency.location ?? {
        countryCode: null,
        countryName: null,
        federalState: null,
        metroCityName: null,
        label: "",
      },
      capacity: resource?.transparency.capacity ?? {
        requestedHoursPerDay: round1(hoursPerDay),
        requestedHoursTotal: 0,
        baseWorkingDays: 0,
        effectiveWorkingDays: 0,
        baseAvailableHours: 0,
        effectiveAvailableHours: 0,
        bookedHours: 0,
        remainingHours: 0,
        remainingHoursPerDay: 0,
        holidayCount: 0,
        holidayWorkdayCount: 0,
        holidayHoursDeduction: 0,
        absenceDayEquivalent: 0,
        absenceHoursDeduction: 0,
      },
      conflicts: resource?.transparency.conflicts ?? {
        count: 0,
        conflictDays: [],
        details: [],
      },
      ranking: {
        rank: index + 1,
        baseRank,
        tieBreakerApplied,
        tieBreakerReason: tieBreakerApplied
          ? "Within 2 score points, higher value score moves the candidate up."
          : null,
        model: "Composite ranking across skill fit, availability, cost, and utilization.",
        components: [
          { key: "skillScore", label: "Skills", score: scoreBreakdown.skillScore },
          { key: "availabilityScore", label: "Availability", score: scoreBreakdown.availabilityScore },
          { key: "costScore", label: "Cost", score: scoreBreakdown.costScore },
          { key: "utilizationScore", label: "Utilization", score: scoreBreakdown.utilizationScore },
        ],
      },
      remainingHoursPerDay: resource?.transparency.capacity.remainingHoursPerDay ?? 0,
      remainingHours: resource?.transparency.capacity.remainingHours ?? 0,
      effectiveAvailableHours: resource?.transparency.capacity.effectiveAvailableHours ?? 0,
      baseAvailableHours: resource?.transparency.capacity.baseAvailableHours ?? 0,
      holidayHoursDeduction: resource?.transparency.capacity.holidayHoursDeduction ?? 0,
    };
  });
}

async function queryBestProjectResource(
  db: BestProjectResourceDbClient,
  input: BestProjectResourceInput,
) {
  const projectAssignmentsResult = await db.assignment.findMany({
    where: {
      projectId: input.projectId,
      status: { not: "CANCELLED" },
      startDate: { lte: input.endDate },
      endDate: { gte: input.startDate },
      resource: {
        isActive: true,
        ...(input.chapter ? { chapter: { contains: input.chapter, mode: "insensitive" } } : {}),
        ...(input.roleName ? { areaRole: { name: { contains: input.roleName, mode: "insensitive" } } } : {}),
      },
    },
    select: {
      resourceId: true,
      hoursPerDay: true,
      startDate: true,
      endDate: true,
      status: true,
      resource: {
        select: {
          id: true,
          eid: true,
          displayName: true,
          chapter: true,
          lcrCents: true,
          availability: true,
          countryId: true,
          federalState: true,
          metroCityId: true,
          country: { select: { code: true, name: true } },
          metroCity: { select: { name: true } },
          areaRole: { select: { name: true } },
        },
      },
    },
    orderBy: [{ resourceId: "asc" }, { startDate: "asc" }],
  });
  const projectAssignments = (Array.isArray(projectAssignmentsResult)
    ? projectAssignmentsResult
    : []) as BestProjectResourceAssignmentRecord[];

  if (projectAssignments.length === 0) {
    return {
      period: {
        startDate: toIsoDate(input.startDate),
        endDate: toIsoDate(input.endDate),
        minHoursPerDay: input.minHoursPerDay,
        rankingMode: input.rankingMode,
      },
      filters: {
        chapter: input.chapter ?? null,
        roleName: input.roleName ?? null,
      },
      candidateCount: 0,
      candidates: [],
      bestMatch: null,
      note: "No active project resources matched the requested filters in the selected period.",
    };
  }

  const resourcesById = new Map<string, (typeof projectAssignments)[number]["resource"]>();
  const assignmentsOnProjectByResourceId = new Map<string, typeof projectAssignments>();
  for (const assignment of projectAssignments) {
    resourcesById.set(assignment.resourceId, assignment.resource);
    const items = assignmentsOnProjectByResourceId.get(assignment.resourceId) ?? [];
    items.push(assignment);
    assignmentsOnProjectByResourceId.set(assignment.resourceId, items);
  }

  const resourceIds = [...resourcesById.keys()];
  const overlappingAssignmentsResult = await db.assignment.findMany({
    where: {
      resourceId: { in: resourceIds },
      status: { not: "CANCELLED" },
      startDate: { lte: input.endDate },
      endDate: { gte: input.startDate },
    },
    select: {
      resourceId: true,
      projectId: true,
      hoursPerDay: true,
      startDate: true,
      endDate: true,
      status: true,
      project: { select: { name: true, shortCode: true } },
    },
    orderBy: [{ resourceId: "asc" }, { startDate: "asc" }],
  });
  const overlappingAssignments = (Array.isArray(overlappingAssignmentsResult)
    ? overlappingAssignmentsResult
    : []) as BestProjectResourceOverlapAssignmentRecord[];

  const assignmentsByResourceId = new Map<string, typeof overlappingAssignments>();
  for (const assignment of overlappingAssignments) {
    const items = assignmentsByResourceId.get(assignment.resourceId) ?? [];
    items.push(assignment);
    assignmentsByResourceId.set(assignment.resourceId, items);
  }

  const resources = [...resourcesById.values()];
  const contexts = await loadResourceDailyAvailabilityContexts(
    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,
    })),
    input.startDate,
    input.endDate,
  );

  const candidates = resources.map((resource) => {
    const availability = resource.availability as unknown as WeekdayAvailability;
    const context = contexts.get(resource.id);
    const baseWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart: input.startDate,
      periodEnd: input.endDate,
      context: undefined,
    });
    const workingDays = countEffectiveWorkingDays({
      availability,
      periodStart: input.startDate,
      periodEnd: input.endDate,
      context,
    });
    const baseAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: input.startDate,
      periodEnd: input.endDate,
      context: undefined,
    });
    const availableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: input.startDate,
      periodEnd: input.endDate,
      context,
    });
    const assignments = assignmentsByResourceId.get(resource.id) ?? [];
    const bookedHours = assignments.reduce(
      (sum, assignment) =>
        sum + calculateEffectiveBookedHours({
          availability,
          startDate: assignment.startDate,
          endDate: assignment.endDate,
          hoursPerDay: assignment.hoursPerDay,
          periodStart: input.startDate,
          periodEnd: input.endDate,
          context,
        }),
      0,
    );
    const projectHours = (assignmentsOnProjectByResourceId.get(resource.id) ?? []).reduce(
      (sum, assignment) =>
        sum + calculateEffectiveBookedHours({
          availability,
          startDate: assignment.startDate,
          endDate: assignment.endDate,
          hoursPerDay: assignment.hoursPerDay,
          periodStart: input.startDate,
          periodEnd: input.endDate,
          context,
        }),
      0,
    );
    let excludedCapacityDays = 0;
    for (const fraction of context?.absenceFractionsByDate.values() ?? []) {
      excludedCapacityDays += fraction;
    }
    const holidayWorkdayCount = [...(context?.holidayDates ?? new Set<string>())].reduce((count, isoDate) => (
      count + (getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
    ), 0);
    const holidayHoursDeduction = [...(context?.holidayDates ?? new Set<string>())].reduce((sum, isoDate) => (
      sum + getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`))
    ), 0);
    let absenceDayEquivalent = 0;
    let absenceHoursDeduction = 0;
    for (const [isoDate, fraction] of context?.vacationFractionsByDate ?? []) {
      const dayHours = getBaseDayAvailability(availability, new Date(`${isoDate}T00:00:00.000Z`));
      if (dayHours <= 0 || context?.holidayDates.has(isoDate)) {
        continue;
      }
      absenceDayEquivalent += fraction;
      absenceHoursDeduction += dayHours * fraction;
    }

    const remainingHours = Math.max(0, availableHours - bookedHours);
    const remainingHoursPerDay = averagePerWorkingDay(remainingHours, workingDays);

    return {
      id: resource.id,
      eid: resource.eid,
      name: resource.displayName,
      role: resource.areaRole?.name ?? null,
      chapter: resource.chapter ?? null,
      country: resource.country?.name ?? resource.country?.code ?? null,
      countryCode: resource.country?.code ?? null,
      federalState: resource.federalState ?? null,
      metroCity: resource.metroCity?.name ?? null,
      lcrCents: resource.lcrCents ?? null,
      lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
      baseWorkingDays: round1(baseWorkingDays),
      workingDays,
      excludedCapacityDays: round1(excludedCapacityDays),
      baseAvailableHours: round1(baseAvailableHours),
      availableHours: round1(availableHours),
      bookedHours: round1(bookedHours),
      remainingHours: round1(remainingHours),
      remainingHoursPerDay,
      projectHours: round1(projectHours),
      assignmentCount: assignments.length,
      holidaySummary: {
        count: context?.holidayDates.size ?? 0,
        workdayCount: holidayWorkdayCount,
        hoursDeduction: round1(holidayHoursDeduction),
        holidayDates: [...(context?.holidayDates ?? new Set<string>())].sort(),
      },
      absenceSummary: {
        dayEquivalent: round1(absenceDayEquivalent),
        hoursDeduction: round1(absenceHoursDeduction),
      },
      capacityBreakdown: {
        formula: "baseAvailableHours - holidayHoursDeduction - absenceHoursDeduction = availableHours",
        baseAvailableHours: round1(baseAvailableHours),
        holidayHoursDeduction: round1(holidayHoursDeduction),
        absenceHoursDeduction: round1(absenceHoursDeduction),
        availableHours: round1(availableHours),
      },
    };
  }).filter((candidate) => candidate.remainingHoursPerDay >= input.minHoursPerDay);

  candidates.sort((left, right) => {
    if (input.rankingMode === "highest_remaining_hours_per_day") {
      return right.remainingHoursPerDay - left.remainingHoursPerDay
        || right.remainingHours - left.remainingHours
        || (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER);
    }
    if (input.rankingMode === "highest_remaining_hours") {
      return right.remainingHours - left.remainingHours
        || right.remainingHoursPerDay - left.remainingHoursPerDay
        || (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER);
    }
    return (left.lcrCents ?? Number.MAX_SAFE_INTEGER) - (right.lcrCents ?? Number.MAX_SAFE_INTEGER)
      || right.remainingHoursPerDay - left.remainingHoursPerDay
      || right.remainingHours - left.remainingHours;
  });

  return {
    period: {
      startDate: toIsoDate(input.startDate),
      endDate: toIsoDate(input.endDate),
      minHoursPerDay: input.minHoursPerDay,
      rankingMode: input.rankingMode,
    },
    filters: {
      chapter: input.chapter ?? null,
      roleName: input.roleName ?? null,
    },
    candidateCount: candidates.length,
    bestMatch: candidates[0] ?? null,
    candidates,
  };
}

export const staffingRouter = createTRPCRouter({
  /**
   * Get ranked resource suggestions for a staffing requirement.
   */
  getSuggestions: planningReadProcedure
    .input(
      z.object({
        requiredSkills: z.array(z.string()),
        preferredSkills: z.array(z.string()).optional(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        hoursPerDay: z.number().min(0).max(24),
        budgetLcrCentsPerHour: z.number().optional(),
        chapter: z.string().optional(),
        skillCategory: z.string().optional(),
        mainSkillsOnly: z.boolean().optional(),
        minProficiency: z.number().min(1).max(5).optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.VIEW_COSTS);
      return queryStaffingSuggestions(ctx.db as unknown as StaffingSuggestionsDbClient, input);
    }),

  getProjectStaffingSuggestions: planningReadProcedure
    .input(
      z.object({
        projectId: z.string().min(1),
        roleName: z.string().optional(),
        startDate: z.coerce.date().optional(),
        endDate: z.coerce.date().optional(),
        limit: z.number().int().min(1).max(50).optional().default(5),
      }),
    )
    .query(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.VIEW_COSTS);
      const project = await findUniqueOrThrow(ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: {
          id: true,
          shortCode: true,
          name: true,
          startDate: true,
          endDate: true,
        },
      }), "Project");
      const startDate = input.startDate ?? project.startDate ?? new Date();
      const endDate = input.endDate ?? project.endDate ?? new Date();
      const normalizedRoleFilter = input.roleName?.trim().toLowerCase();
      const suggestions = await queryStaffingSuggestions(ctx.db as unknown as StaffingSuggestionsDbClient, {
        requiredSkills: [],
        startDate,
        endDate,
        hoursPerDay: 8,
      });

      return {
        project: `${project.name} (${project.shortCode})`,
        period: `${fmtDate(startDate)} to ${fmtDate(endDate)}`,
        suggestions: suggestions
          .filter((suggestion) => {
            if (!normalizedRoleFilter) {
              return true;
            }
            return suggestion.role?.toLowerCase().includes(normalizedRoleFilter) ?? false;
          })
          .map((suggestion) => ({
            id: suggestion.resourceId,
            name: suggestion.resourceName,
            eid: suggestion.eid,
            role: suggestion.role,
            chapter: suggestion.chapter,
            fte: round1(suggestion.fte ?? 0),
            lcr: fmtEur(Math.round((suggestion.estimatedDailyCostCents ?? 0) / 8)),
            workingDays: round1(suggestion.capacity.effectiveWorkingDays),
            availableHours: round1(suggestion.capacity.remainingHours),
            bookedHours: round1(suggestion.capacity.bookedHours),
            availableHoursPerDay: round1(suggestion.capacity.remainingHoursPerDay),
            utilization: round1(suggestion.currentUtilization ?? 0),
          }))
          .filter((suggestion) => suggestion.availableHours > 0)
          .slice(0, input.limit),
      };
    }),

  searchCapacity: planningReadProcedure
    .input(
      z.object({
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        minHoursPerDay: z.number().optional().default(4),
        roleName: z.string().optional(),
        chapter: z.string().optional(),
        limit: z.number().int().min(1).max(100).optional().default(20),
      }),
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = { isActive: true };
      if (input.roleName) {
        where.areaRole = { name: { contains: input.roleName, mode: "insensitive" } };
      }
      if (input.chapter) {
        where.chapter = { contains: input.chapter, mode: "insensitive" };
      }

      const resources = await ctx.db.resource.findMany({
        where,
        select: {
          id: true,
          displayName: true,
          eid: true,
          fte: true,
          availability: true,
          countryId: true,
          federalState: true,
          metroCityId: true,
          country: { select: { code: true } },
          metroCity: { select: { name: true } },
          areaRole: { select: { name: true } },
          chapter: true,
        },
        take: 100,
      });

      const bookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: resources.map((resource) => resource.id),
      });

      const bookingsByResourceId = new Map<string, typeof bookings>();
      for (const booking of bookings) {
        if (!booking.resourceId) {
          continue;
        }
        const existing = bookingsByResourceId.get(booking.resourceId) ?? [];
        existing.push(booking);
        bookingsByResourceId.set(booking.resourceId, existing);
      }

      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.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,
        })),
        input.startDate,
        input.endDate,
      );

      const results = resources
        .map((resource) => {
          const availability = resource.availability as unknown as WeekdayAvailability;
          const context = contexts.get(resource.id);
          const workingDays = countEffectiveWorkingDays({
            availability,
            periodStart: input.startDate,
            periodEnd: input.endDate,
            context,
          });
          const availableHours = calculateEffectiveAvailableHours({
            availability,
            periodStart: input.startDate,
            periodEnd: input.endDate,
            context,
          });
          const bookedHours = (bookingsByResourceId.get(resource.id) ?? []).reduce(
            (sum, booking) =>
              sum + calculateEffectiveBookedHours({
                availability,
                startDate: booking.startDate,
                endDate: booking.endDate,
                hoursPerDay: booking.hoursPerDay,
                periodStart: input.startDate,
                periodEnd: input.endDate,
                context,
              }),
            0,
          );
          const remainingHours = Math.max(0, availableHours - bookedHours);

          return {
            id: resource.id,
            name: resource.displayName,
            eid: resource.eid,
            role: resource.areaRole?.name ?? null,
            chapter: resource.chapter,
            workingDays,
            availableHours: round1(remainingHours),
            availableHoursPerDay: averagePerWorkingDay(remainingHours, workingDays),
          };
        })
        .filter((resource) => resource.availableHoursPerDay >= input.minHoursPerDay)
        .sort((left, right) => right.availableHours - left.availableHours)
        .slice(0, input.limit);

      return {
        period: `${toIsoDate(input.startDate)} to ${toIsoDate(input.endDate)}`,
        minHoursFilter: input.minHoursPerDay,
        results,
        totalFound: results.length,
      };
    }),

  /**
   * Analyze utilization for a specific resource over a date range.
   */
  analyzeUtilization: planningReadProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const resource = await findUniqueOrThrow(
        ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: {
            id: true,
            displayName: true,
            chargeabilityTarget: true,
            availability: true,
            countryId: true,
            federalState: true,
            metroCityId: true,
            country: { select: { code: true } },
            metroCity: { select: { name: true } },
          },
        }),
        "Resource",
      );

      const resourceBookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: [resource.id],
      });

      const availability = resource.availability as unknown as WeekdayAvailability;
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        [{
          id: resource.id,
          availability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        }],
        input.startDate,
        input.endDate,
      );
      const context = contexts.get(resource.id);
      const activeBookings = resourceBookings.map((booking) => ({
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        status: booking.status,
        projectName: booking.project.name,
        isChargeable: booking.project.orderType === "CHARGEABLE",
      }));

      const overallocatedDays: string[] = [];
      const underutilizedDays: string[] = [];
      let totalAvailableHours = 0;
      let totalChargeableHours = 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 availableHoursForDay = getEffectiveDayAvailability(availability, cursor, context);
        if (availableHoursForDay > 0) {
          const { allocatedHours, chargeableHours } = calculateAllocatedHoursForDay({
            bookings: activeBookings,
            date: cursor,
            context,
          });
          totalAvailableHours += availableHoursForDay;
          totalChargeableHours += chargeableHours;

          if (allocatedHours > availableHoursForDay) {
            overallocatedDays.push(toIsoDate(cursor));
          } else if (allocatedHours < availableHoursForDay * 0.5) {
            underutilizedDays.push(toIsoDate(cursor));
          }
        }

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

      const currentChargeability = totalAvailableHours > 0
        ? (totalChargeableHours / totalAvailableHours) * 100
        : 0;

      return {
        resourceId: resource.id,
        resourceName: resource.displayName,
        chargeabilityTarget: resource.chargeabilityTarget,
        currentChargeability,
        chargeabilityGap: resource.chargeabilityTarget - currentChargeability,
        allocations: activeBookings
          .filter((booking) => ACTIVE_STATUSES.has(booking.status))
          .map((booking) => ({
            startDate: booking.startDate,
            endDate: booking.endDate,
            hoursPerDay: booking.hoursPerDay,
            projectName: booking.projectName,
            isChargeable: booking.isChargeable,
          })),
        overallocatedDays,
        underutilizedDays,
      };
    }),

  /**
   * Find capacity windows for a resource.
   */
  findCapacity: planningReadProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        minAvailableHoursPerDay: z.number().optional().default(4),
      }),
    )
    .query(async ({ ctx, input }) => {
      const resource = await findUniqueOrThrow(
        ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: {
            id: true,
            displayName: true,
            availability: true,
            countryId: true,
            federalState: true,
            metroCityId: true,
            country: { select: { code: true } },
            metroCity: { select: { name: true } },
          },
        }),
        "Resource",
      );

      const resourceBookings = await listAssignmentBookings(ctx.db, {
        startDate: input.startDate,
        endDate: input.endDate,
        resourceIds: [resource.id],
      });

      const availability = resource.availability as unknown as WeekdayAvailability;
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        [{
          id: resource.id,
          availability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        }],
        input.startDate,
        input.endDate,
      );
      const context = contexts.get(resource.id);
      const windows: Array<{
        resourceId: string;
        resourceName: string;
        startDate: Date;
        endDate: Date;
        availableHoursPerDay: number;
        availableDays: number;
        totalAvailableHours: number;
      }> = [];

      let windowStart: Date | null = null;
      let windowAvailableDays = 0;
      let windowTotalHours = 0;
      let windowMinHours = Number.POSITIVE_INFINITY;

      const closeWindow = (closeDate: Date) => {
        if (windowStart && windowAvailableDays > 0) {
          const previousDay = new Date(closeDate);
          previousDay.setUTCDate(previousDay.getUTCDate() - 1);
          windows.push({
            resourceId: resource.id,
            resourceName: resource.displayName,
            startDate: new Date(windowStart),
            endDate: previousDay,
            availableHoursPerDay: Number.isFinite(windowMinHours) ? windowMinHours : 0,
            availableDays: windowAvailableDays,
            totalAvailableHours: Math.round(windowTotalHours * 10) / 10,
          });
        }
        windowStart = null;
        windowAvailableDays = 0;
        windowTotalHours = 0;
        windowMinHours = Number.POSITIVE_INFINITY;
      };

      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 availableHoursForDay = getEffectiveDayAvailability(availability, cursor, context);
        if (availableHoursForDay <= 0) {
          closeWindow(cursor);
          cursor.setUTCDate(cursor.getUTCDate() + 1);
          continue;
        }

        const { allocatedHours } = calculateAllocatedHoursForDay({
          bookings: resourceBookings.map((booking) => ({
            startDate: booking.startDate,
            endDate: booking.endDate,
            hoursPerDay: booking.hoursPerDay,
            status: booking.status,
          })),
          date: cursor,
          context,
        });
        const freeHours = Math.max(0, availableHoursForDay - allocatedHours);

        if (freeHours >= input.minAvailableHoursPerDay) {
          if (!windowStart) {
            windowStart = new Date(cursor);
          }
          windowAvailableDays += 1;
          windowTotalHours += freeHours;
          windowMinHours = Math.min(windowMinHours, freeHours);
        } else {
          closeWindow(cursor);
        }

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

      closeWindow(new Date(end.getTime() + 86_400_000));

      return windows;
    }),

  findBestProjectResource: planningReadProcedure
    .input(
      z.object({
        projectId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        minHoursPerDay: z.number().min(0).default(3),
        rankingMode: z.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"]).default("lowest_lcr"),
        chapter: z.string().optional(),
        roleName: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.VIEW_COSTS);
      return queryBestProjectResource(ctx.db as unknown as BestProjectResourceDbClient, input);
    }),

  getBestProjectResourceDetail: planningReadProcedure
    .input(
      z.object({
        projectId: z.string().min(1),
        startDate: z.coerce.date().optional(),
        endDate: z.coerce.date().optional(),
        durationDays: z.number().int().min(1).optional(),
        minHoursPerDay: z.number().min(0).default(3),
        rankingMode: z.enum(["lowest_lcr", "highest_remaining_hours_per_day", "highest_remaining_hours"]).default("lowest_lcr"),
        chapter: z.string().optional(),
        roleName: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.VIEW_COSTS);
      const project = await findUniqueOrThrow(ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: {
          id: true,
          name: true,
          shortCode: true,
          status: true,
          responsiblePerson: true,
        },
      }), "Project");
      const { startDate, endDate } = createDateRange({
        startDate: input.startDate,
        endDate: input.endDate,
        durationDays: input.durationDays,
      });
      const result = await queryBestProjectResource(ctx.db as unknown as BestProjectResourceDbClient, {
        projectId: input.projectId,
        startDate,
        endDate,
        minHoursPerDay: input.minHoursPerDay,
        rankingMode: input.rankingMode,
        ...(input.chapter ? { chapter: input.chapter } : {}),
        ...(input.roleName ? { roleName: input.roleName } : {}),
      });

      return {
        ...result,
        project: {
          id: project.id,
          name: project.name,
          shortCode: project.shortCode,
          status: project.status,
          responsiblePerson: project.responsiblePerson,
        },
      };
    }),
});
