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 { loadResourceDailyAvailabilityContexts } from "../lib/resource-capacity.js";
import { fmtEur } from "../lib/format-utils.js";
import { planningReadProcedure, requirePermission } from "../trpc.js";
import {
  ACTIVE_STATUSES,
  calculateAllocatedHoursForDay,
  createLocationLabel,
  getBaseDayAvailability,
  getEffectiveDayAvailability,
  round1,
  toIsoDate,
} from "./staffing-shared.js";
import { buildResourceCapacitySummary } from "./staffing-capacity-summary.js";

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

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[]>;
    };
    project: {
      findUnique: (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;
};

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 capacity = buildResourceCapacitySummary({
      availability,
      periodStart: startDate,
      periodEnd: endDate,
      context,
      bookings: activeBookings,
    });
    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 allocatedHours = capacity.bookedHours;
    const remainingHours = capacity.remainingHours;
    const remainingHoursPerDay = capacity.remainingHoursPerDay;
    const utilizationPercent =
      capacity.availableHours > 0
        ? Math.min(100, (allocatedHours / capacity.availableHours) * 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((skill) => skill.isMainSkill);
    }
    if (skillCategory) {
      skills = skills.filter((skill) => skill.category === skillCategory);
    }
    if (minProficiency) {
      skills = skills.filter((skill) => skill.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(capacity.workingDays * hoursPerDay),
          baseWorkingDays: capacity.baseWorkingDays,
          effectiveWorkingDays: capacity.workingDays,
          baseAvailableHours: capacity.baseAvailableHours,
          effectiveAvailableHours: capacity.availableHours,
          bookedHours: capacity.bookedHours,
          remainingHours: capacity.remainingHours,
          remainingHoursPerDay,
          holidayCount: capacity.holidaySummary.count,
          holidayWorkdayCount: capacity.holidaySummary.workdayCount,
          holidayHoursDeduction: capacity.holidaySummary.hoursDeduction,
          absenceDayEquivalent: capacity.absenceSummary.dayEquivalent,
          absenceHoursDeduction: capacity.absenceSummary.hoursDeduction,
        },
        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((left, right) => {
    if (Math.abs(left.score - right.score) <= 2) {
      const leftValue = enrichedResources.find((resource) => resource.id === left.resourceId)?.valueScore ?? 0;
      const rightValue = enrichedResources.find((resource) => resource.id === right.resourceId)?.valueScore ?? 0;
      return rightValue - leftValue;
    }
    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,
    };
  });
}
export const staffingSuggestionsReadProcedures = {
  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),
      };
    }),
};
