/**
 * Vacation entitlement & balance router.
 * Tracks annual leave quotas per resource per year.
 * Balance is computed lazily: carryover from previous year is applied on first access.
 */
import { VacationType, VacationStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure } from "../trpc.js";
import { createAuditEntry } from "../lib/audit.js";
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";

/** Types that consume from annual leave balance */
const BALANCE_TYPES: VacationType[] = [VacationType.ANNUAL, VacationType.OTHER];

type EntitlementSnapshot = {
  id: string;
  entitledDays: number;
  carryoverDays: number;
  usedDays: number;
  pendingDays: number;
};

function mapBalanceDetail(resource: {
  displayName: string;
  eid: string;
}, balance: {
  year: number;
  entitledDays: number;
  carryoverDays: number;
  usedDays: number;
  pendingDays: number;
  remainingDays: number;
  sickDays: number;
}) {
  return {
    resource: resource.displayName,
    eid: resource.eid,
    year: balance.year,
    entitlement: balance.entitledDays,
    carryOver: balance.carryoverDays,
    taken: balance.usedDays,
    pending: balance.pendingDays,
    remaining: balance.remainingDays,
    sickDays: balance.sickDays,
  };
}

function mapYearSummaryDetail(
  year: number,
  summaries: Array<{
    displayName: string;
    eid: string;
    chapter: string | null;
    entitledDays: number;
    carryoverDays: number;
    usedDays: number;
    pendingDays: number;
    remainingDays: number;
  }>,
  resourceName?: string,
) {
  const needle = resourceName?.toLowerCase();

  return summaries
    .filter((summary) => {
      if (!needle) {
        return true;
      }
      return summary.displayName.toLowerCase().includes(needle)
        || summary.eid.toLowerCase().includes(needle);
    })
    .slice(0, 50)
    .map((summary) => ({
      resource: summary.displayName,
      eid: summary.eid,
      chapter: summary.chapter ?? null,
      year,
      entitled: summary.entitledDays,
      carryover: summary.carryoverDays,
      used: summary.usedDays,
      pending: summary.pendingDays,
      remaining: summary.remainingDays,
    }));
}

type EntitlementReadContext = Parameters<Parameters<typeof protectedProcedure["query"]>[0]>[0]["ctx"];

async function readBalanceSnapshot(
  ctx: Pick<EntitlementReadContext, "db" | "dbUser">,
  input: { resourceId: string; year: number },
) {
  if (ctx.dbUser) {
    const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"];
    if (!allowedRoles.includes(ctx.dbUser.systemRole)) {
      const resource = await ctx.db.resource.findUnique({
        where: { id: input.resourceId },
        select: { userId: true },
      });
      if (!resource || resource.userId !== ctx.dbUser.id) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can only view your own vacation balance",
        });
      }
    }
  }

  const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;
  const entitlement = await syncEntitlement(ctx.db, input.resourceId, input.year, defaultDays);

  const sickVacationsResult = await ctx.db.vacation.findMany({
    where: {
      resourceId: input.resourceId,
      type: VacationType.SICK,
      status: VacationStatus.APPROVED,
      startDate: { lte: new Date(`${input.year}-12-31T00:00:00.000Z`) },
      endDate: { gte: new Date(`${input.year}-01-01T00:00:00.000Z`) },
    },
    select: { startDate: true, endDate: true, isHalfDay: true },
  });
  const sickVacations = Array.isArray(sickVacationsResult) ? sickVacationsResult : [];
  const sickDays = sickVacations.reduce(
    (sum, vacation) => sum + countCalendarDaysInPeriod(
      vacation,
      new Date(`${input.year}-01-01T00:00:00.000Z`),
      new Date(`${input.year}-12-31T00:00:00.000Z`),
    ),
    0,
  );

  return {
    year: input.year,
    resourceId: input.resourceId,
    entitledDays: entitlement.entitledDays,
    carryoverDays: entitlement.carryoverDays,
    usedDays: entitlement.usedDays,
    pendingDays: entitlement.pendingDays,
    remainingDays: Math.max(
      0,
      entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays,
    ),
    sickDays,
  };
}

async function readYearSummarySnapshot(
  ctx: Pick<EntitlementReadContext, "db">,
  input: { year: number; chapter?: string },
) {
  const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
  const defaultDays = settings?.vacationDefaultDays ?? 28;

  const resources = await ctx.db.resource.findMany({
    where: {
      isActive: true,
      ...(input.chapter ? { chapter: input.chapter } : {}),
    },
    select: { ...RESOURCE_BRIEF_SELECT, chapter: true },
    orderBy: [{ chapter: "asc" }, { displayName: "asc" }],
  });

  return Promise.all(
    resources.map(async (resource) => {
      const entitlement = await syncEntitlement(ctx.db, resource.id, input.year, defaultDays);
      return {
        resourceId: resource.id,
        displayName: resource.displayName,
        eid: resource.eid,
        chapter: resource.chapter,
        entitledDays: entitlement.entitledDays,
        carryoverDays: entitlement.carryoverDays,
        usedDays: entitlement.usedDays,
        pendingDays: entitlement.pendingDays,
        remainingDays: Math.max(
          0,
          entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays,
        ),
      };
    }),
  );
}

/**
 * Get or create an entitlement record, applying carryover from previous year if needed.
 */
async function getOrCreateEntitlement(
  db: Parameters<Parameters<typeof protectedProcedure["query"]>[0]>[0]["ctx"]["db"],
  resourceId: string,
  year: number,
  defaultDays: number,
) {
  let entitlement = await db.vacationEntitlement.findUnique({
    where: { resourceId_year: { resourceId, year } },
  });

  if (!entitlement) {
    // Check previous year for carryover
    const prevYear = await db.vacationEntitlement.findUnique({
      where: { resourceId_year: { resourceId, year: year - 1 } },
    });

    const carryover = prevYear
      ? Math.max(0, prevYear.entitledDays - prevYear.usedDays - prevYear.pendingDays)
      : 0;

    entitlement = await db.vacationEntitlement.create({
      data: {
        resourceId,
        year,
        entitledDays: defaultDays + carryover,
        carryoverDays: carryover,
        usedDays: 0,
        pendingDays: 0,
      },
    });
  }

  return entitlement;
}

function calculateCarryoverDays(entitlement: {
  entitledDays: number;
  usedDays: number;
  pendingDays: number;
}): number {
  return Math.max(0, entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays);
}

/**
 * Recompute used/pending days from actual vacation records and update the cached values.
 */
async function syncEntitlement(
  db: Parameters<Parameters<typeof protectedProcedure["query"]>[0]>[0]["ctx"]["db"],
  resourceId: string,
  year: number,
  defaultDays: number,
  visitedYears: Set<number> = new Set(),
): Promise<EntitlementSnapshot> {
  if (visitedYears.has(year)) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: `Detected recursive entitlement sync for year ${year}`,
    });
  }
  visitedYears.add(year);

  let previousYearEntitlement: EntitlementSnapshot | null = await db.vacationEntitlement.findUnique({
    where: { resourceId_year: { resourceId, year: year - 1 } },
  });

  if (previousYearEntitlement) {
    previousYearEntitlement = await syncEntitlement(
      db,
      resourceId,
      year - 1,
      defaultDays,
      visitedYears,
    );
  }

  const entitlement = await getOrCreateEntitlement(db, resourceId, year, defaultDays);
  const carryoverDays = previousYearEntitlement
    ? calculateCarryoverDays(previousYearEntitlement)
    : 0;
  const expectedEntitledDays = defaultDays + carryoverDays;
  const entitlementWithCarryover = (
    entitlement.carryoverDays !== carryoverDays
    || entitlement.entitledDays !== expectedEntitledDays
  )
    ? await db.vacationEntitlement.update({
        where: { id: entitlement.id },
        data: {
          carryoverDays,
          entitledDays: expectedEntitledDays,
        },
      })
    : entitlement;
  const yearStart = new Date(`${year}-01-01T00:00:00.000Z`);
  const yearEnd = new Date(`${year}-12-31T00:00:00.000Z`);
  const holidayContext = await loadResourceHolidayContext(db, resourceId, yearStart, yearEnd);

  const vacations = await db.vacation.findMany({
    where: {
      resourceId,
      type: { in: BALANCE_TYPES },
      startDate: { lte: yearEnd },
      endDate: { gte: yearStart },
      status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
    },
    select: { startDate: true, endDate: true, status: true, isHalfDay: true },
  });

  let usedDays = 0;
  let pendingDays = 0;

  for (const v of vacations) {
    const days = countVacationChargeableDays({
      vacation: v,
      periodStart: yearStart,
      periodEnd: yearEnd,
      countryCode: holidayContext.countryCode,
      federalState: holidayContext.federalState,
      metroCityName: holidayContext.metroCityName,
      calendarHolidayStrings: holidayContext.calendarHolidayStrings,
      publicHolidayStrings: holidayContext.publicHolidayStrings,
    });
    if (v.status === VacationStatus.APPROVED) usedDays += days;
    else pendingDays += days;
  }

  return db.vacationEntitlement.update({
    where: { id: entitlementWithCarryover.id },
    data: { usedDays, pendingDays },
  });
}

export const entitlementRouter = createTRPCRouter({
  /**
   * Get vacation balance for a resource in a year.
   * Creates the entitlement record if it doesn't exist (with carryover).
   */
  getBalance: protectedProcedure
    .input(
      z.object({
        resourceId: z.string(),
        year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
      }),
    )
    .query(async ({ ctx, input }) => readBalanceSnapshot(ctx, input)),

  getBalanceDetail: protectedProcedure
    .input(
      z.object({
        resourceId: z.string(),
        year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
      }),
    )
    .query(async ({ ctx, input }) => {
      const balance = await readBalanceSnapshot(ctx, input);
      const resource = await ctx.db.resource.findUnique({
        where: { id: input.resourceId },
        select: { displayName: true, eid: true },
      });

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

      return mapBalanceDetail(resource, balance);
    }),

  /**
   * Get entitlement record for a resource/year (admin/manager only).
   */
  get: managerProcedure
    .input(z.object({ resourceId: z.string(), year: z.number().int() }))
    .query(async ({ ctx, input }) => {
      const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
      const defaultDays = settings?.vacationDefaultDays ?? 28;
      return syncEntitlement(ctx.db, input.resourceId, input.year, defaultDays);
    }),

  /**
   * Set entitlement for a resource/year (admin/manager only).
   */
  set: managerProcedure
    .input(
      z.object({
        resourceId: z.string(),
        year: z.number().int(),
        entitledDays: z.number().min(0).max(365),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.db.vacationEntitlement.findUnique({
        where: { resourceId_year: { resourceId: input.resourceId, year: input.year } },
      });
      if (existing) {
        const updated = await ctx.db.vacationEntitlement.update({
          where: { id: existing.id },
          data: { entitledDays: input.entitledDays },
        });

        void createAuditEntry({
          db: ctx.db,
          entityType: "VacationEntitlement",
          entityId: updated.id,
          entityName: `Entitlement ${input.resourceId} / ${input.year}`,
          action: "UPDATE",
          ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
          before: existing as unknown as Record<string, unknown>,
          after: updated as unknown as Record<string, unknown>,
          source: "ui",
          summary: `Updated entitlement from ${existing.entitledDays} to ${input.entitledDays} days (${input.year})`,
        });

        return updated;
      }
      const created = await ctx.db.vacationEntitlement.create({
        data: {
          resourceId: input.resourceId,
          year: input.year,
          entitledDays: input.entitledDays,
          carryoverDays: 0,
          usedDays: 0,
          pendingDays: 0,
        },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "VacationEntitlement",
        entityId: created.id,
        entityName: `Entitlement ${input.resourceId} / ${input.year}`,
        action: "CREATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        after: created as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Set entitlement to ${input.entitledDays} days (${input.year})`,
      });

      return created;
    }),

  /**
   * Bulk-set entitlements for multiple resources (admin only).
   * Useful for setting the default entitlement for a new year.
   */
  bulkSet: adminProcedure
    .input(
      z.object({
        year: z.number().int(),
        entitledDays: z.number().min(0).max(365),
        resourceIds: z.array(z.string()).optional(), // if omitted, applies to all active resources
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const resources = await ctx.db.resource.findMany({
        where: {
          isActive: true,
          ...(input.resourceIds ? { id: { in: input.resourceIds } } : {}),
        },
        select: { id: true },
      });

      let updated = 0;
      for (const r of resources) {
        await ctx.db.vacationEntitlement.upsert({
          where: { resourceId_year: { resourceId: r.id, year: input.year } },
          create: {
            resourceId: r.id,
            year: input.year,
            entitledDays: input.entitledDays,
            carryoverDays: 0,
            usedDays: 0,
            pendingDays: 0,
          },
          update: { entitledDays: input.entitledDays },
        });
        updated++;
      }

      void createAuditEntry({
        db: ctx.db,
        entityType: "VacationEntitlement",
        entityId: `bulk-${input.year}`,
        entityName: `Bulk Entitlement ${input.year}`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        after: { year: input.year, entitledDays: input.entitledDays, resourceCount: updated } as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Bulk set entitlement to ${input.entitledDays} days for ${updated} resources (${input.year})`,
      });

      return { updated };
    }),

  /**
   * Get year summary: all resources with their balance for a given year.
   * Manager/admin only.
   */
  getYearSummary: managerProcedure
    .input(
      z.object({
        year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
        chapter: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => readYearSummarySnapshot(ctx, {
      year: input.year,
      ...(input.chapter ? { chapter: input.chapter } : {}),
    })),

  getYearSummaryDetail: managerProcedure
    .input(
      z.object({
        year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()),
        chapter: z.string().optional(),
        resourceName: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const summaries = await readYearSummarySnapshot(ctx, {
        year: input.year,
        ...(input.chapter ? { chapter: input.chapter } : {}),
      });

      return mapYearSummaryDetail(input.year, summaries, input.resourceName);
    }),
});
