import { UpdateVacationStatusSchema } from "@capakraken/shared";
import { VacationStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { createAuditEntry } from "../lib/audit.js";
import { checkBatchVacationConflicts, checkVacationConflicts } from "../lib/vacation-conflicts.js";
import { emitVacationUpdated } from "../sse/event-bus.js";
import { adminProcedure, managerProcedure, protectedProcedure } from "../trpc.js";
import {
  assertVacationStillChargeable,
  buildVacationApprovalWriteData,
} from "./vacation-chargeability.js";
import { batchCreatePublicHolidayVacations } from "./vacation-public-holidays.js";
import {
  completeVacationApprovalTasks,
  dispatchVacationWebhookInBackground,
  notifyVacationStatusInBackground,
} from "./vacation-side-effects.js";
import {
  assertVacationApprovable,
  assertVacationCancelable,
  assertVacationRejectable,
  buildApprovedVacationUpdateData,
  buildRejectedVacationUpdateData,
  buildVacationStatusUpdateData,
  canActorCancelVacation,
  isVacationManagerRole,
} from "./vacation-management-support.js";

const BatchCreatePublicHolidaysSchema = z.object({
  year: z.number().int().min(2000).max(2100),
  federalState: z.string().optional(),
  chapter: z.string().optional(),
  replaceExisting: z.boolean().default(false),
});

async function findVacationActor(
  db: Parameters<Parameters<typeof protectedProcedure["query"]>[0]>[0]["ctx"]["db"],
  email: string | null | undefined,
) {
  return db.user.findUnique({
    where: { email: email ?? "" },
    select: { id: true, systemRole: true },
  });
}

export const vacationManagementProcedures = {
  approve: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await findUniqueOrThrow(
        ctx.db.vacation.findUnique({ where: { id: input.id } }),
        "Vacation",
      );
      assertVacationApprovable(existing.status);

      await assertVacationStillChargeable(ctx.db, {
        resourceId: existing.resourceId,
        type: existing.type,
        startDate: existing.startDate,
        endDate: existing.endDate,
        isHalfDay: existing.isHalfDay,
      });
      const deductionSnapshotWriteData = await buildVacationApprovalWriteData(ctx.db, {
        resourceId: existing.resourceId,
        type: existing.type,
        startDate: existing.startDate,
        endDate: existing.endDate,
        isHalfDay: existing.isHalfDay,
      });

      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);
      const conflictResult = await checkVacationConflicts(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ctx.db as any,
        input.id,
        userRecord?.id,
      );

      const updated = await ctx.db.vacation.update({
        where: { id: input.id },
        data: buildApprovedVacationUpdateData({
          deductionSnapshotWriteData,
          approvedById: userRecord?.id,
          approvedAt: new Date(),
        }),
      });

      emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });

      void createAuditEntry({
        db: ctx.db,
        entityType: "Vacation",
        entityId: updated.id,
        entityName: `Vacation ${updated.id}`,
        action: "UPDATE",
        ...(userRecord?.id ? { userId: userRecord.id } : {}),
        before: existing as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Approved vacation (was ${existing.status})`,
      });

      dispatchVacationWebhookInBackground(ctx.db, "vacation.approved", {
        id: updated.id,
        resourceId: updated.resourceId,
        startDate: updated.startDate.toISOString(),
        endDate: updated.endDate.toISOString(),
      });

      await completeVacationApprovalTasks(ctx.db, input.id, userRecord?.id);

      if (existing.status === VacationStatus.PENDING) {
        notifyVacationStatusInBackground(ctx.db, updated.id, updated.resourceId, VacationStatus.APPROVED);
      }

      return { ...updated, warnings: conflictResult.warnings };
    }),

  reject: managerProcedure
    .input(z.object({ id: z.string(), rejectionReason: z.string().max(500).optional() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await findUniqueOrThrow(
        ctx.db.vacation.findUnique({ where: { id: input.id } }),
        "Vacation",
      );
      assertVacationRejectable(existing.status);

      const updated = await ctx.db.vacation.update({
        where: { id: input.id },
        data: buildRejectedVacationUpdateData({
          rejectionReason: input.rejectionReason,
        }),
      });

      emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });

      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);
      await completeVacationApprovalTasks(ctx.db, input.id, userRecord?.id);

      void createAuditEntry({
        db: ctx.db,
        entityType: "Vacation",
        entityId: updated.id,
        entityName: `Vacation ${updated.id}`,
        action: "UPDATE",
        ...(userRecord?.id ? { userId: userRecord.id } : {}),
        before: existing as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Rejected vacation${input.rejectionReason ? `: ${input.rejectionReason}` : ""}`,
      });

      notifyVacationStatusInBackground(
        ctx.db,
        updated.id,
        updated.resourceId,
        VacationStatus.REJECTED,
        input.rejectionReason,
      );

      return updated;
    }),

  batchApprove: managerProcedure
    .input(z.object({ ids: z.array(z.string()).min(1).max(100) }))
    .mutation(async ({ ctx, input }) => {
      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);

      const vacations = await ctx.db.vacation.findMany({
        where: { id: { in: input.ids }, status: VacationStatus.PENDING },
        select: {
          id: true,
          resourceId: true,
          type: true,
          startDate: true,
          endDate: true,
          isHalfDay: true,
        },
      });

      for (const vacation of vacations) {
        await assertVacationStillChargeable(ctx.db, vacation);
      }

      const conflictMap = await checkBatchVacationConflicts(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ctx.db as any,
        vacations.map((vacation) => vacation.id),
        userRecord?.id,
      );

      for (const vacation of vacations) {
        const deductionSnapshotWriteData = await buildVacationApprovalWriteData(ctx.db, {
          resourceId: vacation.resourceId,
          type: vacation.type,
          startDate: vacation.startDate,
          endDate: vacation.endDate,
          isHalfDay: vacation.isHalfDay,
        });
        const updated = await ctx.db.vacation.update({
          where: { id: vacation.id },
          data: buildApprovedVacationUpdateData({
            deductionSnapshotWriteData,
            approvedById: userRecord?.id,
            approvedAt: new Date(),
          }),
        });

        emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
        notifyVacationStatusInBackground(ctx.db, updated.id, updated.resourceId, VacationStatus.APPROVED);

        void createAuditEntry({
          db: ctx.db,
          entityType: "Vacation",
          entityId: updated.id,
          entityName: `Vacation ${updated.id}`,
          action: "UPDATE",
          ...(userRecord?.id ? { userId: userRecord.id } : {}),
          after: updated as unknown as Record<string, unknown>,
          source: "ui",
          summary: "Batch approved vacation",
        });

        await completeVacationApprovalTasks(ctx.db, updated.id, userRecord?.id);
      }

      const warnings: string[] = [];
      for (const [, vacationWarnings] of conflictMap) {
        warnings.push(...vacationWarnings);
      }

      return { approved: vacations.length, warnings };
    }),

  batchReject: managerProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(100),
        rejectionReason: z.string().max(500).optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);

      const vacations = await ctx.db.vacation.findMany({
        where: { id: { in: input.ids }, status: VacationStatus.PENDING },
        select: { id: true, resourceId: true },
      });

      await ctx.db.vacation.updateMany({
        where: { id: { in: vacations.map((vacation) => vacation.id) } },
        data: buildRejectedVacationUpdateData({
          rejectionReason: input.rejectionReason,
        }),
      });

      for (const vacation of vacations) {
        emitVacationUpdated({ id: vacation.id, resourceId: vacation.resourceId, status: VacationStatus.REJECTED });
        notifyVacationStatusInBackground(
          ctx.db,
          vacation.id,
          vacation.resourceId,
          VacationStatus.REJECTED,
          input.rejectionReason,
        );

        void createAuditEntry({
          db: ctx.db,
          entityType: "Vacation",
          entityId: vacation.id,
          entityName: `Vacation ${vacation.id}`,
          action: "UPDATE",
          ...(userRecord?.id ? { userId: userRecord.id } : {}),
          after: { status: VacationStatus.REJECTED, rejectionReason: input.rejectionReason } as unknown as Record<string, unknown>,
          source: "ui",
          summary: `Batch rejected vacation${input.rejectionReason ? `: ${input.rejectionReason}` : ""}`,
        });

        await completeVacationApprovalTasks(ctx.db, vacation.id, userRecord?.id);
      }

      return { rejected: vacations.length };
    }),

  cancel: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await findUniqueOrThrow(
        ctx.db.vacation.findUnique({ where: { id: input.id } }),
        "Vacation",
      );
      assertVacationCancelable(existing.status);

      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);
      if (!userRecord) {
        throw new TRPCError({ code: "UNAUTHORIZED" });
      }
      const resource = isVacationManagerRole(userRecord.systemRole) || existing.requestedById === userRecord.id
        ? null
        : await ctx.db.resource.findUnique({
            where: { id: existing.resourceId },
            select: { userId: true },
          });

      if (!canActorCancelVacation({
        actorId: userRecord.id,
        actorRole: userRecord.systemRole,
        requestedById: existing.requestedById,
        resourceUserId: resource?.userId,
      })) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can only cancel your own vacation requests",
        });
      }

      const updated = await ctx.db.vacation.update({
        where: { id: input.id },
        data: { status: VacationStatus.CANCELLED },
      });

      emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });

      void createAuditEntry({
        db: ctx.db,
        entityType: "Vacation",
        entityId: updated.id,
        entityName: `Vacation ${updated.id}`,
        action: "UPDATE",
        userId: userRecord.id,
        before: existing as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Cancelled vacation (was ${existing.status})`,
      });

      return updated;
    }),

  getPendingApprovals: managerProcedure.query(async ({ ctx }) => {
    return ctx.db.vacation.findMany({
      where: { status: VacationStatus.PENDING },
      include: {
        resource: { select: { ...RESOURCE_BRIEF_SELECT, chapter: true } },
        requestedBy: { select: { id: true, name: true, email: true } },
      },
      orderBy: { startDate: "asc" },
    });
  }),

  batchCreatePublicHolidays: adminProcedure
    .input(BatchCreatePublicHolidaysSchema)
    .mutation(async ({ ctx, input }) => {
      const adminUser = await ctx.db.user.findUnique({
        where: { email: ctx.session.user?.email ?? "" },
        select: { id: true },
      });
      if (!adminUser) {
        throw new TRPCError({ code: "UNAUTHORIZED" });
      }

      const { created, holidays, resources } = await batchCreatePublicHolidayVacations(
        ctx.db,
        input,
        adminUser.id,
      );

      void createAuditEntry({
        db: ctx.db,
        entityType: "Vacation",
        entityId: `public-holidays-${input.year}`,
        entityName: `Public Holidays ${input.year}${input.federalState ? ` (${input.federalState})` : ""}`,
        action: "CREATE",
        userId: adminUser.id,
        after: { created, holidays, resources, year: input.year, federalState: input.federalState } as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Batch created ${created} public holidays for ${resources} resources (${input.year})`,
      });

      return { created, holidays, resources };
    }),

  updateStatus: protectedProcedure
    .input(UpdateVacationStatusSchema)
    .mutation(async ({ ctx, input }) => {
      const existing = await findUniqueOrThrow(
        ctx.db.vacation.findUnique({ where: { id: input.id } }),
        "Vacation",
      );

      const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);
      if (!userRecord) {
        throw new TRPCError({ code: "UNAUTHORIZED" });
      }

      if (input.status !== "CANCELLED" && !isVacationManagerRole(userRecord.systemRole)) {
        throw new TRPCError({ code: "FORBIDDEN", message: "Manager role required to approve/reject" });
      }

      const updated = await ctx.db.vacation.update({
        where: { id: input.id },
        data: buildVacationStatusUpdateData({
          status: input.status,
          note: input.note,
          approvedById: userRecord.id,
          approvedAt: new Date(),
        }),
      });

      emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });

      void createAuditEntry({
        db: ctx.db,
        entityType: "Vacation",
        entityId: updated.id,
        entityName: `Vacation ${updated.id}`,
        action: "UPDATE",
        userId: userRecord.id,
        before: existing as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Updated vacation status to ${input.status}`,
      });

      return updated;
    }),
};
