import { listAssignmentBookings } from "@capakraken/application";
import { computeBudgetStatus, validateShift } from "@capakraken/engine";
import { ShiftProjectSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { getAnonymizationDirectory } from "../lib/anonymization.js";
import { controllerProcedure } from "../trpc.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { buildTimelineShiftPlan } from "./timeline-shift-planning.js";
import { loadTimelineHolidayOverlays, formatHolidayOverlays, summarizeHolidayOverlays } from "./timeline-holiday-read.js";
import {
  anonymizeResourceOnEntry,
  createTimelineDateRange,
  fmtDate,
  getAssignmentResourceIds,
  rangesOverlap,
  ShiftDbClient,
  summarizeTimelineEntries,
  toDate,
} from "./timeline-read-shared.js";

export async function loadProjectShiftContext(db: ShiftDbClient, projectId: string) {
  const [project, planningRead] = await Promise.all([
    findUniqueOrThrow(
      db.project.findUnique({
        where: { id: projectId },
        select: {
          id: true,
          budgetCents: true,
          winProbability: true,
          startDate: true,
          endDate: true,
        },
      }),
      "Project",
    ),
    loadProjectPlanningReadModel(db, { projectId, activeOnly: true }),
  ]);

  const { demandRequirements, assignments, readModel: projectReadModel } = planningRead;

  const resourceIds = getAssignmentResourceIds(projectReadModel);
  const allAssignmentWindows =
    resourceIds.length === 0
      ? []
      : (
          await listAssignmentBookings(db, {
            resourceIds,
          })
        ).map((booking) => ({
          id: booking.id,
          resourceId: booking.resourceId!,
          projectId: booking.projectId,
          startDate: booking.startDate,
          endDate: booking.endDate,
          hoursPerDay: booking.hoursPerDay,
          status: booking.status,
        }));

  const shiftPlan = buildTimelineShiftPlan({
    demandRequirements,
    assignments,
    allAssignmentWindows,
  });

  return {
    project,
    demandRequirements,
    assignments,
    shiftPlan,
  };
}

export async function loadTimelineProjectContext(db: ShiftDbClient, projectId: string) {
  const [project, planningRead] = await Promise.all([
    findUniqueOrThrow(
      db.project.findUnique({
        where: { id: projectId },
        select: {
          id: true,
          name: true,
          shortCode: true,
          orderType: true,
          budgetCents: true,
          winProbability: true,
          status: true,
          startDate: true,
          endDate: true,
          staffingReqs: true,
        },
      }),
      "Project",
    ),
    loadProjectPlanningReadModel(db, {
      projectId,
      activeOnly: true,
    }),
  ]);

  const resourceIds = getAssignmentResourceIds(planningRead.readModel);
  const allResourceAllocations =
    resourceIds.length === 0
      ? []
      : await listAssignmentBookings(db, {
          resourceIds,
        });

  return {
    project,
    allocations: planningRead.readModel.allocations,
    demands: planningRead.readModel.demands,
    assignments: planningRead.readModel.assignments,
    allResourceAllocations,
    resourceIds,
  };
}

export async function previewTimelineProjectShift(
  db: ShiftDbClient,
  input: {
    projectId: string;
    newStartDate: Date;
    newEndDate: Date;
  },
) {
  const { project, shiftPlan } = await loadProjectShiftContext(db, input.projectId);

  return validateShift({
    project: {
      id: project.id,
      budgetCents: project.budgetCents,
      winProbability: project.winProbability,
      startDate: project.startDate,
      endDate: project.endDate,
    },
    newStartDate: input.newStartDate,
    newEndDate: input.newEndDate,
    allocations: shiftPlan.validationAllocations,
  });
}

export const timelineProjectReadProcedures = {
  getProjectContext: controllerProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const {
        project,
        allocations,
        demands,
        assignments,
        allResourceAllocations,
        resourceIds,
      } = await loadTimelineProjectContext(ctx.db, input.projectId);
      const directory = await getAnonymizationDirectory(ctx.db);

      return {
        project,
        allocations: allocations.map((allocation) =>
          anonymizeResourceOnEntry(allocation, directory),
        ),
        demands,
        assignments: assignments.map((assignment) =>
          anonymizeResourceOnEntry(assignment, directory),
        ),
        allResourceAllocations: allResourceAllocations.map((allocation) =>
          anonymizeResourceOnEntry(allocation, directory),
        ),
        resourceIds,
      };
    }),

  getProjectContextDetail: controllerProcedure
    .input(
      z.object({
        projectId: z.string(),
        startDate: z.string().optional(),
        endDate: z.string().optional(),
        durationDays: z.number().int().min(1).max(366).optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const projectContext = await loadTimelineProjectContext(ctx.db, input.projectId);
      const directory = await getAnonymizationDirectory(ctx.db);

      const derivedStartDate = input.startDate
        ? createTimelineDateRange({ startDate: input.startDate, durationDays: 1 }).startDate
        : projectContext.project.startDate
          ?? projectContext.assignments[0]?.startDate
          ?? projectContext.demands[0]?.startDate
          ?? createTimelineDateRange({ durationDays: 1 }).startDate;
      const derivedEndDate = input.endDate
        ? createTimelineDateRange({ startDate: fmtDate(derivedStartDate) ?? undefined, endDate: input.endDate }).endDate
        : projectContext.project.endDate
          ?? createTimelineDateRange({
              startDate: fmtDate(derivedStartDate) ?? undefined,
              durationDays: input.durationDays ?? 21,
            }).endDate;

      if (derivedEndDate < derivedStartDate) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "endDate must be on or after startDate.",
        });
      }

      const holidayOverlays = projectContext.resourceIds.length > 0
        ? await loadTimelineHolidayOverlays(ctx.db, {
            startDate: derivedStartDate,
            endDate: derivedEndDate,
            resourceIds: projectContext.resourceIds,
            projectIds: [input.projectId],
          })
        : [];
      const formattedHolidayOverlays = formatHolidayOverlays(holidayOverlays);

      const assignmentConflicts = projectContext.assignments
        .filter((assignment) => assignment.resourceId && assignment.resource)
        .map((assignment) => {
          const overlaps = projectContext.allResourceAllocations
            .filter((booking) => (
              booking.resourceId === assignment.resourceId
              && booking.id !== assignment.id
              && rangesOverlap(
                toDate(booking.startDate),
                toDate(booking.endDate),
                toDate(assignment.startDate),
                toDate(assignment.endDate),
              )
            ))
            .map((booking) => ({
              id: booking.id,
              projectId: booking.projectId,
              projectName: booking.project?.name ?? null,
              projectShortCode: booking.project?.shortCode ?? null,
              startDate: fmtDate(toDate(booking.startDate)),
              endDate: fmtDate(toDate(booking.endDate)),
              hoursPerDay: booking.hoursPerDay,
              status: booking.status,
              sameProject: booking.projectId === input.projectId,
            }));

          return {
            assignmentId: assignment.id,
            resourceId: assignment.resourceId!,
            resourceName: assignment.resource?.displayName ?? null,
            startDate: fmtDate(toDate(assignment.startDate)),
            endDate: fmtDate(toDate(assignment.endDate)),
            hoursPerDay: assignment.hoursPerDay,
            overlapCount: overlaps.length,
            crossProjectOverlapCount: overlaps.filter((booking) => !booking.sameProject).length,
            overlaps,
          };
        });

      return {
        project: projectContext.project,
        period: {
          startDate: fmtDate(derivedStartDate),
          endDate: fmtDate(derivedEndDate),
        },
        summary: {
          ...summarizeTimelineEntries({
            allocations: projectContext.allocations,
            demands: projectContext.demands,
            assignments: projectContext.assignments,
          }),
          resourceIds: projectContext.resourceIds.length,
          allResourceAllocationCount: projectContext.allResourceAllocations.length,
          conflictedAssignmentCount: assignmentConflicts.filter((item) => item.crossProjectOverlapCount > 0).length,
          ...summarizeHolidayOverlays(formattedHolidayOverlays),
        },
        allocations: projectContext.allocations.map((allocation) =>
          anonymizeResourceOnEntry(allocation, directory),
        ),
        demands: projectContext.demands,
        assignments: projectContext.assignments.map((assignment) =>
          anonymizeResourceOnEntry(assignment, directory),
        ),
        allResourceAllocations: projectContext.allResourceAllocations.map((allocation) =>
          anonymizeResourceOnEntry(allocation, directory),
        ),
        assignmentConflicts,
        holidayOverlays: formattedHolidayOverlays,
        resourceIds: projectContext.resourceIds,
      };
    }),

  previewShift: controllerProcedure
    .input(ShiftProjectSchema)
    .query(async ({ ctx, input }) => previewTimelineProjectShift(ctx.db, input)),

  getShiftPreviewDetail: controllerProcedure
    .input(ShiftProjectSchema)
    .query(async ({ ctx, input }) => {
      const [project, preview] = await Promise.all([
        findUniqueOrThrow(
          ctx.db.project.findUnique({
            where: { id: input.projectId },
            select: {
              id: true,
              name: true,
              shortCode: true,
              status: true,
              responsiblePerson: true,
              startDate: true,
              endDate: true,
            },
          }),
          "Project",
        ),
        previewTimelineProjectShift(ctx.db, input),
      ]);

      return {
        project: {
          id: project.id,
          name: project.name,
          shortCode: project.shortCode,
          status: project.status,
          responsiblePerson: project.responsiblePerson,
          startDate: fmtDate(project.startDate),
          endDate: fmtDate(project.endDate),
        },
        requestedShift: {
          newStartDate: fmtDate(input.newStartDate),
          newEndDate: fmtDate(input.newEndDate),
        },
        preview,
      };
    }),

  getBudgetStatus: controllerProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const project = await findUniqueOrThrow(
        ctx.db.project.findUnique({
          where: { id: input.projectId },
          select: {
            id: true,
            name: true,
            shortCode: true,
            budgetCents: true,
            winProbability: true,
            startDate: true,
            endDate: true,
          },
        }),
        "Project",
      );

      const bookings = await listAssignmentBookings(ctx.db, {
        projectIds: [project.id],
      });

      const budgetStatus = computeBudgetStatus(
        project.budgetCents,
        project.winProbability,
        bookings.map((booking) => ({
          status: booking.status,
          dailyCostCents: booking.dailyCostCents,
          startDate: booking.startDate,
          endDate: booking.endDate,
          hoursPerDay: booking.hoursPerDay,
        })) as unknown as Pick<
          import("@capakraken/shared").Allocation,
          "status" | "dailyCostCents" | "startDate" | "endDate" | "hoursPerDay"
        >[],
        project.startDate,
        project.endDate,
      );

      return {
        ...budgetStatus,
        projectName: project.name,
        projectCode: project.shortCode,
        totalAllocations: bookings.length,
        budgetCents: project.budgetCents,
      };
    }),
};
