import { VacationStatus, VacationType } 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 { anonymizeResource, anonymizeUser, getAnonymizationDirectory } from "../lib/anonymization.js";
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
import { protectedProcedure, type TRPCContext } from "../trpc.js";
import {
  buildVacationPreview,
  findVacationResourceChapter,
  listChapterVacationOverlaps,
} from "./vacation-read-support.js";
import {
  assertCanReadOwnedResource,
  canManageOwnedResourceReads,
  resolveOwnedResourceReadFilter,
} from "./resource-owned-read-access.js";

type VacationReadContext = Pick<TRPCContext, "db" | "dbUser">;

export function canManageVacationReads(ctx: { dbUser: { systemRole: string } | null }): boolean {
  return canManageOwnedResourceReads(ctx);
}

export async function assertCanReadVacationResource(
  ctx: VacationReadContext,
  resourceId: string,
): Promise<void> {
  await assertCanReadOwnedResource(
    ctx,
    resourceId,
    "You can only view vacation data for your own resource",
  );
}

export function isSameUtcDay(left: Date, right: Date): boolean {
  return left.toISOString().slice(0, 10) === right.toISOString().slice(0, 10);
}

function mapTeamOverlapDetail(params: {
  resource: { displayName: string; chapter: string | null };
  startDate: Date;
  endDate: Date;
  overlaps: Array<{
    type: VacationType;
    status: VacationStatus;
    startDate: Date;
    endDate: Date;
    resource: { displayName: string };
  }>;
}) {
  return {
    resource: params.resource.displayName,
    chapter: params.resource.chapter,
    period: `${params.startDate.toISOString().slice(0, 10)} to ${params.endDate.toISOString().slice(0, 10)}`,
    overlappingVacations: params.overlaps.map((vacation) => ({
      resource: vacation.resource.displayName,
      type: vacation.type,
      status: vacation.status,
      start: vacation.startDate.toISOString().slice(0, 10),
      end: vacation.endDate.toISOString().slice(0, 10),
    })),
    overlapCount: params.overlaps.length,
  };
}

export const PreviewVacationRequestSchema = z.object({
  resourceId: z.string(),
  type: z.nativeEnum(VacationType),
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
  isHalfDay: z.boolean().optional(),
}).superRefine((data, ctx) => {
  if (data.endDate < data.startDate) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "End date must be after start date",
      path: ["endDate"],
    });
  }

  if (data.isHalfDay && !isSameUtcDay(data.startDate, data.endDate)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Half-day requests must start and end on the same day",
      path: ["isHalfDay"],
    });
  }
});

export function anonymizeVacationRecord<T extends {
  resource?: { id: string } | null;
  requestedBy?: { id?: string | null; name?: string | null; email?: string | null } | null;
  approvedBy?: { id?: string | null; name?: string | null; email?: string | null } | null;
}>(
  vacation: T,
  directory: Awaited<ReturnType<typeof getAnonymizationDirectory>>,
): T {
  return {
    ...vacation,
    ...(vacation.resource ? { resource: anonymizeResource(vacation.resource, directory) } : {}),
    ...(vacation.requestedBy ? { requestedBy: anonymizeUser(vacation.requestedBy, directory) } : {}),
    ...(vacation.approvedBy ? { approvedBy: anonymizeUser(vacation.approvedBy, directory) } : {}),
  };
}

export const vacationReadProcedures = {
  previewRequest: protectedProcedure
    .input(PreviewVacationRequestSchema)
    .query(async ({ ctx, input }) => {
      await assertCanReadVacationResource(ctx, input.resourceId);

      const holidayContext = await loadResourceHolidayContext(
        ctx.db,
        input.resourceId,
        input.startDate,
        input.endDate,
      );
      return buildVacationPreview({
        type: input.type,
        startDate: input.startDate,
        endDate: input.endDate,
        isHalfDay: input.isHalfDay ?? false,
        holidayContext,
      });
    }),

  list: protectedProcedure
    .input(
      z.object({
        resourceId: z.string().optional(),
        status: z.union([z.nativeEnum(VacationStatus), z.array(z.nativeEnum(VacationStatus))]).optional(),
        type: z.nativeEnum(VacationType).optional(),
        startDate: z.coerce.date().optional(),
        endDate: z.coerce.date().optional(),
        limit: z.number().min(1).max(500).default(100),
      }),
    )
    .query(async ({ ctx, input }) => {
      const resourceIdFilter = await resolveOwnedResourceReadFilter(
        ctx,
        input.resourceId,
        "You can only view vacation data for your own resource",
      );

      if (!canManageVacationReads(ctx) && !resourceIdFilter) {
        return [];
      }

      const vacations = await ctx.db.vacation.findMany({
        where: {
          ...(resourceIdFilter ? { resourceId: resourceIdFilter } : {}),
          ...(input.status ? { status: Array.isArray(input.status) ? { in: input.status } : input.status } : {}),
          ...(input.type ? { type: input.type } : {}),
          ...(input.startDate ? { endDate: { gte: input.startDate } } : {}),
          ...(input.endDate ? { startDate: { lte: input.endDate } } : {}),
        },
        include: {
          resource: { select: RESOURCE_BRIEF_SELECT },
          requestedBy: { select: { id: true, name: true, email: true } },
          approvedBy: { select: { id: true, name: true, email: true } },
        },
        orderBy: { startDate: "asc" },
        take: input.limit,
      });
      const directory = await getAnonymizationDirectory(ctx.db);
      return vacations.map((vacation) => anonymizeVacationRecord(vacation, directory));
    }),

  getById: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const vacation = await findUniqueOrThrow(
        ctx.db.vacation.findUnique({
          where: { id: input.id },
          include: {
            resource: { select: { ...RESOURCE_BRIEF_SELECT, userId: true } },
            requestedBy: { select: { id: true, name: true, email: true } },
            approvedBy: { select: { id: true, name: true, email: true } },
          },
        }),
        "Vacation",
      );

      if (!canManageVacationReads(ctx)) {
        const isOwnVacation = vacation.resource?.userId === ctx.dbUser?.id || vacation.requestedById === ctx.dbUser?.id;
        if (!isOwnVacation) {
          throw new TRPCError({
            code: "FORBIDDEN",
            message: "You can only view your own vacation data",
          });
        }
      }

      const directory = await getAnonymizationDirectory(ctx.db);
      const anonymized = anonymizeVacationRecord(vacation, directory);
      return {
        ...anonymized,
        resource: anonymized.resource
          ? {
            id: anonymized.resource.id,
            displayName: anonymized.resource.displayName,
            eid: anonymized.resource.eid,
            lcrCents: anonymized.resource.lcrCents,
            chapter: anonymized.resource.chapter,
          }
          : null,
      };
    }),

  getForResource: protectedProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
      }),
    )
    .query(async ({ ctx, input }) => {
      await assertCanReadVacationResource(ctx, input.resourceId);

      return ctx.db.vacation.findMany({
        where: {
          resourceId: input.resourceId,
          status: VacationStatus.APPROVED,
          startDate: { lte: input.endDate },
          endDate: { gte: input.startDate },
        },
        select: {
          id: true,
          startDate: true,
          endDate: true,
          type: true,
          status: true,
        },
        orderBy: { startDate: "asc" },
      });
    }),

  getTeamOverlap: protectedProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
      }),
    )
    .query(async ({ ctx, input }) => {
      await assertCanReadVacationResource(ctx, input.resourceId);

      const chapter = await findVacationResourceChapter(ctx.db, input.resourceId);
      if (!chapter) {
        return [];
      }

      return listChapterVacationOverlaps(ctx.db, {
        chapter,
        resourceId: input.resourceId,
        startDate: input.startDate,
        endDate: input.endDate,
      });
    }),

  getTeamOverlapDetail: protectedProcedure
    .input(
      z.object({
        resourceId: z.string(),
        startDate: z.coerce.date(),
        endDate: z.coerce.date(),
      }),
    )
    .query(async ({ ctx, input }) => {
      await assertCanReadVacationResource(ctx, input.resourceId);

      const resource = await ctx.db.resource.findUnique({
        where: { id: input.resourceId },
        select: { displayName: true, chapter: true },
      });

      if (!resource) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "Resource not found",
        });
      }

      if (!resource.chapter) {
        return mapTeamOverlapDetail({
          resource,
          startDate: input.startDate,
          endDate: input.endDate,
          overlaps: [],
        });
      }

      const overlaps = await listChapterVacationOverlaps(ctx.db, {
        chapter: resource.chapter,
        resourceId: input.resourceId,
        startDate: input.startDate,
        endDate: input.endDate,
      });

      return mapTeamOverlapDetail({
        resource,
        startDate: input.startDate,
        endDate: input.endDate,
        overlaps,
      });
    }),
};
