import {
  deriveResourceForecast,
  calculateGroupChargeability,
  calculateGroupTarget,
  sumFte,
  getMonthRange,
  getMonthKeys,
  type AssignmentSlice,
} from "@capakraken/engine";
import type { PrismaClient } from "@capakraken/db";
import type { WeekdayAvailability } from "@capakraken/shared";
import { PermissionKey } from "@capakraken/shared";
import { isChargeabilityActualBooking, listAssignmentBookings } from "@capakraken/application";
import { z } from "zod";
import { createTRPCRouter, controllerProcedure, requirePermission } from "../trpc.js";
import { anonymizeResources, getAnonymizationDirectory } from "../lib/anonymization.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";

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

const reportInputSchema = z.object({
  startMonth: z.string().regex(/^\d{4}-\d{2}$/),
  endMonth: z.string().regex(/^\d{4}-\d{2}$/),
  orgUnitId: z.string().optional(),
  managementLevelGroupId: z.string().optional(),
  countryId: z.string().optional(),
  includeProposed: z.boolean().default(false),
});

const detailedReportInputSchema = reportInputSchema.extend({
  resourceQuery: z.string().optional(),
  resourceLimit: z.number().int().min(1).max(100).optional(),
});

type ChargeabilityReportDbClient = Pick<
  PrismaClient,
  "assignment" | "resource" | "project" | "vacation" | "holidayCalendar" | "systemSettings"
>;

async function queryChargeabilityReport(
  db: ChargeabilityReportDbClient,
  input: z.infer<typeof reportInputSchema>,
) {
  const { startMonth, endMonth, includeProposed } = input;

  const [startYear, startMo] = startMonth.split("-").map(Number) as [number, number];
  const [endYear, endMo] = endMonth.split("-").map(Number) as [number, number];
  const rangeStart = getMonthRange(startYear, startMo).start;
  const rangeEnd = getMonthRange(endYear, endMo).end;
  const monthKeys = getMonthKeys(rangeStart, rangeEnd);

  const resourceWhere = {
    isActive: true,
    chgResponsibility: true,
    departed: false,
    rolledOff: false,
    ...(input.orgUnitId ? { orgUnitId: input.orgUnitId } : {}),
    ...(input.managementLevelGroupId ? { managementLevelGroupId: input.managementLevelGroupId } : {}),
    ...(input.countryId ? { countryId: input.countryId } : {}),
  };

  const resources = await db.resource.findMany({
    where: resourceWhere,
    select: {
      id: true,
      eid: true,
      displayName: true,
      fte: true,
      availability: true,
      countryId: true,
      federalState: true,
      metroCityId: true,
      chargeabilityTarget: true,
      country: { select: { id: true, code: true, dailyWorkingHours: true, scheduleRules: true } },
      orgUnit: { select: { id: true, name: true } },
      managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
      managementLevel: { select: { id: true, name: true } },
      metroCity: { select: { id: true, name: true } },
    },
    orderBy: { displayName: "asc" },
  });

  if (resources.length === 0) {
    return {
      monthKeys,
      resources: [],
      groupTotals: monthKeys.map((key) => ({
        monthKey: key,
        totalFte: 0,
        chg: 0,
        target: 0,
        gap: 0,
      })),
    };
  }

  const resourceIds = resources.map((resource) => resource.id);
  const allBookings = await listAssignmentBookings(db, {
    startDate: rangeStart,
    endDate: rangeEnd,
    resourceIds,
  });
  const availabilityContexts = 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,
    })),
    rangeStart,
    rangeEnd,
  );

  const projectIds = [...new Set(allBookings.map((booking) => booking.projectId))];
  const projectUtilCats = projectIds.length > 0
    ? await db.project.findMany({
        where: { id: { in: projectIds } },
        select: { id: true, utilizationCategory: { select: { code: true } } },
      })
    : [];
  const projectUtilCatMap = new Map(
    projectUtilCats.map((project) => [project.id, project.utilizationCategory?.code ?? null]),
  );

  const assignments = allBookings
    .filter((booking) => booking.resourceId !== null)
    .filter((booking) => isChargeabilityActualBooking(booking, includeProposed))
    .map((booking) => ({
      resourceId: booking.resourceId!,
      startDate: booking.startDate,
      endDate: booking.endDate,
      hoursPerDay: booking.hoursPerDay,
      project: {
        status: booking.project.status,
        utilizationCategory: { code: projectUtilCatMap.get(booking.projectId) ?? null },
      },
    }));

  const resourceRows = await Promise.all(resources.map(async (resource) => {
    const resourceAssignments = assignments.filter((assignment) => assignment.resourceId === resource.id);
    const targetPct = resource.managementLevelGroup?.targetPercentage
      ?? (resource.chargeabilityTarget / 100);
    const availability = resource.availability as unknown as WeekdayAvailability;
    const context = availabilityContexts.get(resource.id);

    const months = await Promise.all(monthKeys.map(async (key) => {
      const [year, month] = key.split("-").map(Number) as [number, number];
      const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
      const availableHours = calculateEffectiveAvailableHours({
        availability,
        periodStart: monthStart,
        periodEnd: monthEnd,
        context,
      });
      const slices: AssignmentSlice[] = resourceAssignments.flatMap((assignment) => {
        const totalChargeableHours = calculateEffectiveBookedHours({
          availability,
          startDate: assignment.startDate,
          endDate: assignment.endDate,
          hoursPerDay: assignment.hoursPerDay,
          periodStart: monthStart,
          periodEnd: monthEnd,
          context,
        });
        if (totalChargeableHours <= 0) {
          return [];
        }

        const categoryCode = assignment.project.utilizationCategory?.code;

        return {
          hoursPerDay: assignment.hoursPerDay,
          workingDays: 0,
          categoryCode: typeof categoryCode === "string" && categoryCode.length > 0 ? categoryCode : "Chg",
          totalChargeableHours,
        };
      });

      const forecast = deriveResourceForecast({
        fte: resource.fte,
        targetPercentage: targetPct,
        assignments: slices,
        sah: availableHours,
      });

      return {
        monthKey: key,
        sah: availableHours,
        ...forecast,
      };
    }));

    return {
      id: resource.id,
      eid: resource.eid,
      displayName: resource.displayName,
      fte: resource.fte,
      country: resource.country?.code ?? null,
      city: resource.metroCity?.name ?? null,
      orgUnit: resource.orgUnit?.name ?? null,
      mgmtGroup: resource.managementLevelGroup?.name ?? null,
      mgmtLevel: resource.managementLevel?.name ?? null,
      targetPct,
      months,
    };
  }));

  const groupTotals = monthKeys.map((key, monthIdx) => {
    const groupInputs = resourceRows.map((resource) => ({
      fte: resource.fte,
      chargeability: resource.months[monthIdx]!.chg,
    }));
    const targetInputs = resourceRows.map((resource) => ({
      fte: resource.fte,
      targetPercentage: resource.targetPct,
    }));

    const chg = calculateGroupChargeability(groupInputs);
    const target = calculateGroupTarget(targetInputs);

    return {
      monthKey: key,
      totalFte: sumFte(resourceRows),
      chg,
      target,
      gap: chg - target,
    };
  });

  const directory = await getAnonymizationDirectory(db);

  return {
    monthKeys,
    resources: anonymizeResources(resourceRows, directory),
    groupTotals,
  };
}

function buildChargeabilityReportDetail(
  report: Awaited<ReturnType<typeof queryChargeabilityReport>>,
  input: z.infer<typeof detailedReportInputSchema>,
) {
  const resourceQuery = input.resourceQuery?.trim().toLowerCase();
  const matchingResources = resourceQuery
    ? report.resources.filter((resource) => (
        resource.displayName.toLowerCase().includes(resourceQuery)
        || resource.eid.toLowerCase().includes(resourceQuery)
      ))
    : report.resources;
  const resourceLimit = Math.min(Math.max(input.resourceLimit ?? 25, 1), 100);
  const resources = matchingResources.slice(0, resourceLimit).map((resource) => ({
    id: resource.id,
    eid: resource.eid,
    displayName: resource.displayName,
    fte: round1(resource.fte),
    country: resource.country,
    city: resource.city,
    orgUnit: resource.orgUnit,
    managementLevelGroup: resource.mgmtGroup,
    managementLevel: resource.mgmtLevel,
    targetPct: round1(resource.targetPct * 100),
    months: resource.months.map((month) => ({
      monthKey: month.monthKey,
      sah: round1(month.sah),
      chargeabilityPct: round1(month.chg * 100),
      targetPct: round1(resource.targetPct * 100),
      gapPct: round1((month.chg - resource.targetPct) * 100),
    })),
  }));

  return {
    filters: {
      startMonth: input.startMonth,
      endMonth: input.endMonth,
      orgUnitId: input.orgUnitId ?? null,
      managementLevelGroupId: input.managementLevelGroupId ?? null,
      countryId: input.countryId ?? null,
      includeProposed: input.includeProposed ?? false,
      resourceQuery: input.resourceQuery ?? null,
    },
    monthKeys: report.monthKeys,
    groupTotals: report.groupTotals.map((group) => ({
      monthKey: group.monthKey,
      totalFte: round1(group.totalFte),
      chargeabilityPct: round1(group.chg * 100),
      targetPct: round1(group.target * 100),
      gapPct: round1(group.gap * 100),
    })),
    resourceCount: matchingResources.length,
    returnedResourceCount: resources.length,
    truncated: resources.length < matchingResources.length,
    resources,
  };
}

export const chargeabilityReportRouter = createTRPCRouter({
  getReport: controllerProcedure
    .input(reportInputSchema)
    .query(async ({ ctx, input }) => queryChargeabilityReport(ctx.db, input)),

  getDetail: controllerProcedure
    .input(detailedReportInputSchema)
    .query(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.VIEW_COSTS);
      const report = await queryChargeabilityReport(ctx.db, input);
      return buildChargeabilityReportDetail(report, input);
    }),
});
