import { PermissionKey, type WeekdayAvailability } from "@capakraken/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  countEffectiveWorkingDays,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { fmtEur } from "../lib/format-utils.js";
import { planningReadProcedure, requirePermission } from "../trpc.js";
import {
  averagePerWorkingDay,
  createDateRange,
  getBaseDayAvailability,
  round1,
  toIsoDate,
} from "./staffing-shared.js";

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