import {
  buildSplitAllocationReadModel,
  createAssignment,
  findAllocationEntry,
  loadAllocationEntry,
  updateAllocationEntry,
} from "@capakraken/application";
import { Prisma, VacationType } from "@capakraken/db";
import type { PrismaClient } from "@capakraken/db";
import { calculateAllocation, DEFAULT_CALCULATION_RULES } from "@capakraken/engine";
import type { AbsenceDay, CalculationRule } from "@capakraken/shared";
import { AllocationStatus, PermissionKey, UpdateAllocationHoursSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
  emitAllocationCreated,
  emitAllocationUpdated,
} from "../sse/event-bus.js";
import { logger } from "../lib/logger.js";
import { managerProcedure, requirePermission } from "../trpc.js";

function isMissingOptionalTableError(error: unknown, tableHints: string[]): boolean {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    if (error.code !== "P2021") {
      return false;
    }
    const table = typeof error.meta?.table === "string" ? error.meta.table.toLowerCase() : "";
    const message = error.message.toLowerCase();
    return tableHints.some((hint) => table.includes(hint) || message.includes(hint));
  }

  if (typeof error !== "object" || error === null || !("code" in error)) {
    return false;
  }

  const candidate = error as {
    code?: unknown;
    message?: unknown;
    meta?: { table?: unknown };
  };
  const code = typeof candidate.code === "string" ? candidate.code : "";
  if (code !== "P2021") {
    return false;
  }
  const table = typeof candidate.meta?.table === "string" ? candidate.meta.table.toLowerCase() : "";
  const message = typeof candidate.message === "string" ? candidate.message.toLowerCase() : "";
  return tableHints.some((hint) => table.includes(hint) || message.includes(hint));
}

export async function loadCalculationRules(db: PrismaClient): Promise<CalculationRule[]> {
  const calculationRuleModel = (db as PrismaClient & {
    calculationRule?: { findMany?: (args: unknown) => Promise<unknown[]> };
  }).calculationRule;

  if (!calculationRuleModel || typeof calculationRuleModel.findMany !== "function") {
    return DEFAULT_CALCULATION_RULES;
  }

  try {
    const rules = await calculationRuleModel.findMany({
      where: { isActive: true },
      orderBy: [{ priority: "desc" }],
    });
    if (rules.length > 0) {
      return rules as unknown as CalculationRule[];
    }
  } catch (error) {
    if (!isMissingOptionalTableError(error, ["calculationrule", "calculation_rule", "calculation_rules"])) {
      logger.error({ err: error }, "Failed to load active calculation rules for timeline");
      throw error;
    }
  }
  return DEFAULT_CALCULATION_RULES;
}

export async function buildAbsenceDays(
  db: PrismaClient,
  resourceId: string,
  startDate: Date,
  endDate: Date,
): Promise<{ absenceDays: AbsenceDay[]; legacyVacationDates: Date[] }> {
  const absenceDays: AbsenceDay[] = [];
  const legacyVacationDates: Date[] = [];

  try {
    const vacations = await db.vacation.findMany({
      where: {
        resourceId,
        status: "APPROVED",
        startDate: { lte: endDate },
        endDate: { gte: startDate },
      },
      select: { startDate: true, endDate: true, type: true, isHalfDay: true },
    });

    for (const vacation of vacations) {
      const cur = new Date(vacation.startDate);
      cur.setHours(0, 0, 0, 0);
      const vacationEnd = new Date(vacation.endDate);
      vacationEnd.setHours(0, 0, 0, 0);

      const triggerType = vacation.type === VacationType.SICK
        ? "SICK" as const
        : vacation.type === VacationType.PUBLIC_HOLIDAY
          ? "PUBLIC_HOLIDAY" as const
          : "VACATION" as const;

      while (cur <= vacationEnd) {
        absenceDays.push({
          date: new Date(cur),
          type: triggerType,
          ...(vacation.isHalfDay ? { isHalfDay: true } : {}),
        });
        if (triggerType === "VACATION") {
          legacyVacationDates.push(new Date(cur));
        }
        cur.setDate(cur.getDate() + 1);
      }
    }
  } catch (error) {
    if (!isMissingOptionalTableError(error, ["vacation", "vacations"])) {
      logger.error(
        { err: error, resourceId, startDate, endDate },
        "Failed to load timeline absence days",
      );
      throw error;
    }
  }

  return { absenceDays, legacyVacationDates };
}

export const timelineAllocationMutationProcedures = {
  updateAllocationInline: managerProcedure
    .input(UpdateAllocationHoursSchema)
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const resolved = await loadAllocationEntry(ctx.db, input.allocationId);
      const existing = resolved.entry;
      const existingResource = resolved.resourceId
        ? await ctx.db.resource.findUnique({
            where: { id: resolved.resourceId },
            select: { id: true, lcrCents: true, availability: true },
          })
        : null;

      const newHoursPerDay = input.hoursPerDay ?? existing.hoursPerDay;
      const newStartDate = input.startDate ?? existing.startDate;
      const newEndDate = input.endDate ?? existing.endDate;

      if (newEndDate < newStartDate) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "End date must be after start date",
        });
      }

      const existingMeta = (existing.metadata as Record<string, unknown>) ?? {};
      const newMeta: Record<string, unknown> = {
        ...existingMeta,
        ...(input.includeSaturday !== undefined
          ? { includeSaturday: input.includeSaturday }
          : {}),
      };
      const includeSaturday =
        input.includeSaturday ?? (existingMeta.includeSaturday as boolean | undefined) ?? false;

      let newDailyCostCents = 0;
      if (resolved.resourceId) {
        if (!existingResource) {
          throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
        }

        const availability =
          existingResource.availability as unknown as import("@capakraken/shared").WeekdayAvailability;
        const recurrence = newMeta.recurrence as import("@capakraken/shared").RecurrencePattern | undefined;

        const [absenceData, calculationRules] = await Promise.all([
          buildAbsenceDays(ctx.db as PrismaClient, resolved.resourceId, newStartDate, newEndDate),
          loadCalculationRules(ctx.db as PrismaClient),
        ]);

        newDailyCostCents = calculateAllocation({
          lcrCents: existingResource.lcrCents,
          hoursPerDay: newHoursPerDay,
          startDate: newStartDate,
          endDate: newEndDate,
          availability,
          includeSaturday,
          ...(recurrence ? { recurrence } : {}),
          vacationDates: absenceData.legacyVacationDates,
          absenceDays: absenceData.absenceDays,
          calculationRules,
        }).dailyCostCents;
      }

      const updated = await ctx.db.$transaction(async (tx) => {
        const { allocation: updatedAllocation } = await updateAllocationEntry(
          tx as unknown as Parameters<typeof updateAllocationEntry>[0],
          {
            id: input.allocationId,
            demandRequirementUpdate: {
              hoursPerDay: newHoursPerDay,
              startDate: newStartDate,
              endDate: newEndDate,
              metadata: newMeta,
              ...(input.role !== undefined ? { role: input.role } : {}),
            },
            assignmentUpdate: {
              hoursPerDay: newHoursPerDay,
              startDate: newStartDate,
              endDate: newEndDate,
              dailyCostCents: newDailyCostCents,
              metadata: newMeta,
              ...(input.role !== undefined ? { role: input.role } : {}),
            },
          },
        );

        await tx.auditLog.create({
          data: {
            entityType: "Allocation",
            entityId: input.allocationId,
            action: "UPDATE",
            changes: {
              before: {
                id: resolved.entry.id,
                hoursPerDay: existing.hoursPerDay,
                startDate: existing.startDate,
                endDate: existing.endDate,
              },
              after: {
                id: updatedAllocation.id,
                hoursPerDay: newHoursPerDay,
                startDate: newStartDate,
                endDate: newEndDate,
                includeSaturday,
              },
            },
          },
        });

        return updatedAllocation;
      });

      emitAllocationUpdated({
        id: updated.id,
        projectId: updated.projectId,
        resourceId: updated.resourceId,
      });

      return updated;
    }),

  quickAssign: managerProcedure
    .input(
      z.object({
        resourceId: z.string(),
        projectId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
        hoursPerDay: z.number().min(0.5).max(24).default(8),
        role: z.string().min(1).max(200).default("Team Member"),
        roleId: z.string().optional(),
        status: z.nativeEnum(AllocationStatus).default(AllocationStatus.PROPOSED),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      if (input.endDate < input.startDate) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "End date must be after start date" });
      }

      const percentage = Math.min(100, Math.round((input.hoursPerDay / 8) * 100));
      const metadata = { source: "quickAssign" } satisfies Record<string, unknown>;

      const allocation = await ctx.db.$transaction(async (tx) => {
        const assignment = await createAssignment(
          tx as unknown as Parameters<typeof createAssignment>[0],
          {
            resourceId: input.resourceId,
            projectId: input.projectId,
            startDate: input.startDate,
            endDate: input.endDate,
            hoursPerDay: input.hoursPerDay,
            percentage,
            role: input.role,
            roleId: input.roleId ?? undefined,
            status: input.status,
            metadata,
          },
        );

        return buildSplitAllocationReadModel({
          demandRequirements: [],
          assignments: [assignment],
        }).allocations[0]!;
      });

      emitAllocationCreated({
        id: allocation.id,
        projectId: allocation.projectId,
        resourceId: allocation.resourceId,
      });

      return allocation;
    }),

  batchQuickAssign: managerProcedure
    .input(
      z.object({
        assignments: z
          .array(
            z.object({
              resourceId: z.string(),
              projectId: z.string(),
              startDate: z.coerce.date(),
              endDate: z.coerce.date(),
              hoursPerDay: z.number().min(0.5).max(24).default(8),
              role: z.string().min(1).max(200).default("Team Member"),
              status: z
                .nativeEnum(AllocationStatus)
                .default(AllocationStatus.PROPOSED),
            }),
          )
          .min(1)
          .max(50),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      for (const assignment of input.assignments) {
        if (assignment.endDate < assignment.startDate) {
          throw new TRPCError({
            code: "BAD_REQUEST",
            message: "End date must be after start date",
          });
        }
      }

      const results = await ctx.db.$transaction(async (tx) => {
        const created = [];
        for (const assignment of input.assignments) {
          const percentage = Math.min(
            100,
            Math.round((assignment.hoursPerDay / 8) * 100),
          );
          const metadata = {
            source: "batchQuickAssign",
          } satisfies Record<string, unknown>;

          const createdAssignment = await createAssignment(
            tx as unknown as Parameters<typeof createAssignment>[0],
            {
              resourceId: assignment.resourceId,
              projectId: assignment.projectId,
              startDate: assignment.startDate,
              endDate: assignment.endDate,
              hoursPerDay: assignment.hoursPerDay,
              percentage,
              role: assignment.role,
              status: assignment.status,
              metadata,
            },
          );
          created.push(createdAssignment);
        }
        return created;
      });

      for (const assignment of results) {
        emitAllocationCreated({
          id: assignment.id,
          projectId: assignment.projectId,
          resourceId: assignment.resourceId,
        });
      }

      return { count: results.length };
    }),

  batchShiftAllocations: managerProcedure
    .input(
      z.object({
        allocationIds: z.array(z.string()).min(1).max(100),
        daysDelta: z.number().int().min(-3650).max(3650),
        mode: z.enum(["move", "resize-start", "resize-end"]).default("move"),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      if (input.daysDelta === 0) {
        return { count: 0 };
      }

      const entries = await Promise.all(
        input.allocationIds.map((allocationId) => findAllocationEntry(ctx.db, allocationId)),
      );
      const resolved = entries.filter(
        (entry): entry is NonNullable<typeof entry> => entry !== null,
      );

      if (resolved.length === 0) {
        throw new TRPCError({ code: "NOT_FOUND", message: "No allocations found" });
      }

      const results = await ctx.db.$transaction(async (tx) => {
        const updated = [];
        for (const entry of resolved) {
          const existing = entry.entry;
          const newStart = new Date(existing.startDate);
          const newEnd = new Date(existing.endDate);

          if (input.mode === "move") {
            newStart.setDate(newStart.getDate() + input.daysDelta);
            newEnd.setDate(newEnd.getDate() + input.daysDelta);
          } else if (input.mode === "resize-start") {
            newStart.setDate(newStart.getDate() + input.daysDelta);
            if (newStart > newEnd) {
              newStart.setTime(newEnd.getTime());
            }
          } else {
            newEnd.setDate(newEnd.getDate() + input.daysDelta);
            if (newEnd < newStart) {
              newEnd.setTime(newStart.getTime());
            }
          }

          const result = await updateAllocationEntry(
            tx as unknown as Parameters<typeof updateAllocationEntry>[0],
            {
              id: existing.id,
              demandRequirementUpdate: {
                startDate: newStart,
                endDate: newEnd,
              },
              assignmentUpdate: {
                startDate: newStart,
                endDate: newEnd,
              },
            },
          );
          updated.push(result.allocation);
        }

        await tx.auditLog.create({
          data: {
            entityType: "Allocation",
            entityId: input.allocationIds.join(","),
            action: "UPDATE",
            changes: {
              operation: "batchShift",
              mode: input.mode,
              daysDelta: input.daysDelta,
              count: resolved.length,
            } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          },
        });

        return updated;
      });

      for (const allocation of results) {
        emitAllocationUpdated({
          id: allocation.id,
          projectId: allocation.projectId,
          resourceId: allocation.resourceId,
        });
      }

      return { count: results.length };
    }),
};
