import {
  calculateAllocation,
  deriveResourceForecast,
  getMonthRange,
  DEFAULT_CALCULATION_RULES,
  type AssignmentSlice,
} from "@capakraken/engine";
import { VacationStatus } from "@capakraken/db";
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
import { z } from "zod";
import {
  asHolidayResolverDb,
  collectHolidayAvailability,
  getResolvedCalendarHolidays,
} from "../lib/holiday-availability.js";
import {
  calculateEffectiveAvailableHours,
  countEffectiveWorkingDays,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { assertCanReadResource } from "../lib/resource-access.js";
import type { TRPCContext } from "../trpc.js";
import {
  ResourceDirectoryQuerySchema,
  ResourceListQuerySchema,
  listResourceDirectoryEntries,
  listStaffResources,
  readResourceSummariesSnapshot,
  readResourceSummaryDetailsSnapshot,
} from "./resource-read-shared.js";
import { mapResourceSummary, mapResourceSummaryDetail } from "./resource-read-models.js";

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

export const ResolveResponsiblePersonNameInputSchema = z.object({
  name: z.string(),
});

export const ResourceChargeabilitySummaryInputSchema = z.object({
  resourceId: z.string(),
  month: z.string().regex(/^\d{4}-\d{2}$/),
});

export const ResourceSummarySnapshotInputSchema = z.object({
  search: z.string().optional(),
  country: z.string().optional(),
  metroCity: z.string().optional(),
  orgUnit: z.string().optional(),
  roleName: z.string().optional(),
  isActive: z.boolean().optional().default(true),
  limit: z.number().int().min(1).max(100).default(50),
});

function getAvailabilityHoursForDate(
  availability: WeekdayAvailability,
  date: Date,
): number {
  const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability;
  return availability[dayKey] ?? 0;
}

function sumAvailabilityHoursForDates(
  availability: WeekdayAvailability,
  dates: Date[],
): number {
  return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
}

function formatResolvedHolidaySummary(holiday: {
  date: string;
  name: string;
  scope: string;
  calendarName: string | null;
  sourceType?: string | null;
}) {
  return {
    date: holiday.date,
    name: holiday.name,
    scope: holiday.scope,
    calendarName: holiday.calendarName ?? "Built-in",
    sourceType: holiday.sourceType ?? "system",
  };
}

function summarizeResolvedHolidaySummary(holidays: Array<ReturnType<typeof formatResolvedHolidaySummary>>) {
  const byScope = new Map<string, number>();
  const bySourceType = new Map<string, number>();
  const byCalendar = new Map<string, number>();

  for (const holiday of holidays) {
    byScope.set(holiday.scope, (byScope.get(holiday.scope) ?? 0) + 1);
    bySourceType.set(holiday.sourceType, (bySourceType.get(holiday.sourceType) ?? 0) + 1);
    byCalendar.set(holiday.calendarName, (byCalendar.get(holiday.calendarName) ?? 0) + 1);
  }

  return {
    byScope: [...byScope.entries()]
      .sort(([left], [right]) => left.localeCompare(right))
      .map(([scope, count]) => ({ scope, count })),
    bySourceType: [...bySourceType.entries()]
      .sort(([left], [right]) => left.localeCompare(right))
      .map(([sourceType, count]) => ({ sourceType, count })),
    byCalendar: [...byCalendar.entries()]
      .sort(([left], [right]) => left.localeCompare(right))
      .map(([calendarName, count]) => ({ calendarName, count })),
  };
}

function round1(value: number): number {
  return Math.round(value * 10) / 10;
}

function averagePerWorkingDay(totalHours: number, workingDays: number): number {
  return workingDays > 0 ? round1(totalHours / workingDays) : 0;
}

export async function resolveResponsiblePersonName(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResolveResponsiblePersonNameInputSchema>,
) {
  const exact = await ctx.db.resource.findFirst({
    where: { displayName: { equals: input.name, mode: "insensitive" }, isActive: true },
    select: { displayName: true },
  });
  if (exact) {
    return {
      status: "resolved" as const,
      displayName: exact.displayName,
    };
  }

  const candidates = await ctx.db.resource.findMany({
    where: { displayName: { contains: input.name, mode: "insensitive" }, isActive: true },
    select: { displayName: true, eid: true },
    take: 5,
  });
  if (candidates.length === 1) {
    return {
      status: "resolved" as const,
      displayName: candidates[0]!.displayName,
    };
  }
  if (candidates.length > 1) {
    return {
      status: "ambiguous" as const,
      message: `Multiple resources match "${input.name}": ${candidates.map((candidate) => `${candidate.displayName} (${candidate.eid})`).join(", ")}. Please specify the exact name.`,
      candidates,
    };
  }

  return {
    status: "missing" as const,
    message: `No active resource found matching "${input.name}". The responsible person must be an existing resource.`,
    candidates: [],
  };
}

export async function getChargeabilitySummary(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResourceChargeabilitySummaryInputSchema>,
) {
  await assertCanReadResource(
    ctx,
    input.resourceId,
    "You can only view chargeability details for your own resource unless you have staff access",
  );

  const [year, month] = input.month.split("-").map(Number) as [number, number];
  const { start: monthStart, end: monthEnd } = getMonthRange(year, month);

  const resource = await ctx.db.resource.findUniqueOrThrow({
    where: { id: input.resourceId },
    select: {
      id: true,
      displayName: true,
      eid: true,
      fte: true,
      lcrCents: true,
      chargeabilityTarget: true,
      countryId: true,
      federalState: true,
      metroCityId: true,
      availability: true,
      country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
      metroCity: { select: { id: true, name: true } },
      managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
    },
  });

  const dailyHours = resource.country?.dailyWorkingHours ?? 8;
  const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
  const targetRatio = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);

  const availability = resource.availability as WeekdayAvailability | null;
  const weeklyAvailability: WeekdayAvailability = availability ?? {
    monday: dailyHours,
    tuesday: dailyHours,
    wednesday: dailyHours,
    thursday: dailyHours,
    friday: dailyHours,
    saturday: 0,
    sunday: 0,
  };

  const assignments = await ctx.db.assignment.findMany({
    where: {
      resourceId: input.resourceId,
      startDate: { lte: monthEnd },
      endDate: { gte: monthStart },
      status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
    },
    select: {
      id: true,
      hoursPerDay: true,
      startDate: true,
      endDate: true,
      dailyCostCents: true,
      status: true,
      project: {
        select: {
          id: true,
          name: true,
          shortCode: true,
          orderType: true,
          utilizationCategory: { select: { code: true } },
        },
      },
    },
  });

  const vacations = await ctx.db.vacation.findMany({
    where: {
      resourceId: input.resourceId,
      status: VacationStatus.APPROVED,
      startDate: { lte: monthEnd },
      endDate: { gte: monthStart },
    },
    select: { startDate: true, endDate: true, type: true, isHalfDay: true },
  });

  const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
    periodStart: monthStart,
    periodEnd: monthEnd,
    countryId: resource.countryId,
    countryCode: resource.country?.code,
    federalState: resource.federalState,
    metroCityId: resource.metroCityId,
    metroCityName: resource.metroCity?.name,
  });
  const holidayAvailability = collectHolidayAvailability({
    vacations,
    periodStart: monthStart,
    periodEnd: monthEnd,
    countryCode: resource.country?.code,
    federalState: resource.federalState,
    metroCityName: resource.metroCity?.name,
    resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date),
  });
  const absenceDays = holidayAvailability.absenceDays;

  const contexts = await loadResourceDailyAvailabilityContexts(
    ctx.db,
    [{
      id: resource.id,
      availability: weeklyAvailability,
      countryId: resource.countryId,
      countryCode: resource.country?.code,
      federalState: resource.federalState,
      metroCityId: resource.metroCityId,
      metroCityName: resource.metroCity?.name,
    }],
    monthStart,
    monthEnd,
  );
  const availabilityContext = contexts.get(resource.id);

  let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
  try {
    const dbRules = await ctx.db.calculationRule.findMany({
      where: { isActive: true },
      orderBy: [{ priority: "desc" }],
    });
    if (dbRules.length > 0) {
      calcRules = dbRules as unknown as CalculationRule[];
    }
  } catch {
    // table may not exist yet
  }

  const baseWorkingDays = countEffectiveWorkingDays({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: undefined,
  });
  const effectiveWorkingDays = countEffectiveWorkingDays({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: availabilityContext,
  });
  const baseAvailableHours = calculateEffectiveAvailableHours({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: undefined,
  });
  const effectiveAvailableHours = calculateEffectiveAvailableHours({
    availability: weeklyAvailability,
    periodStart: monthStart,
    periodEnd: monthEnd,
    context: availabilityContext,
  });

  const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`));
  const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => (
    count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0)
  ), 0);
  const publicHolidayHoursDeduction = sumAvailabilityHoursForDates(
    weeklyAvailability,
    publicHolidayDates,
  );
  const absenceDayEquivalent = absenceDays.reduce((sum, absence) => {
    if (absence.type === "PUBLIC_HOLIDAY") {
      return sum;
    }
    return sum + (absence.isHalfDay ? 0.5 : 1);
  }, 0);
  const absenceHoursDeduction = absenceDays.reduce((sum, absence) => {
    if (absence.type === "PUBLIC_HOLIDAY") {
      return sum;
    }
    const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date);
    return sum + baseHours * (absence.isHalfDay ? 0.5 : 1);
  }, 0);

  const slices: AssignmentSlice[] = [];
  const assignmentBreakdown: Array<{
    project: string;
    code: string;
    hours: number;
    status: string;
  }> = [];
  let totalBookedHours = 0;

  for (const assignment of assignments) {
    const overlapStart = new Date(Math.max(monthStart.getTime(), assignment.startDate.getTime()));
    const overlapEnd = new Date(Math.min(monthEnd.getTime(), assignment.endDate.getTime()));
    const categoryCode = assignment.project.utilizationCategory?.code ?? "Chg";

    const calcResult = calculateAllocation({
      lcrCents: resource.lcrCents,
      hoursPerDay: assignment.hoursPerDay,
      startDate: overlapStart,
      endDate: overlapEnd,
      availability: weeklyAvailability,
      absenceDays,
      calculationRules: calcRules,
      orderType: assignment.project.orderType,
      projectId: assignment.project.id,
    });
    if (calcResult.workingDays <= 0 && calcResult.totalHours <= 0) {
      continue;
    }

    totalBookedHours += calcResult.totalHours;
    assignmentBreakdown.push({
      project: assignment.project.name,
      code: assignment.project.shortCode,
      hours: round1(calcResult.totalHours),
      status: assignment.status,
    });
    slices.push({
      hoursPerDay: assignment.hoursPerDay,
      workingDays: calcResult.workingDays,
      categoryCode,
      ...(calcResult.totalChargeableHours !== undefined
        ? { totalChargeableHours: calcResult.totalChargeableHours }
        : {}),
    });
  }

  const forecast = deriveResourceForecast({
    fte: resource.fte,
    targetPercentage: targetRatio,
    assignments: slices,
    sah: effectiveAvailableHours,
  });

  const formattedHolidays = resolvedHolidays.map((holiday) => formatResolvedHolidaySummary({
    ...holiday,
    calendarName: holiday.calendarName ?? "Built-in",
    sourceType: holiday.sourceType ?? "system",
  }));
  const workingDays = round1(effectiveWorkingDays);
  const baseWorkingDaysRounded = round1(baseWorkingDays);
  const baseAvailableHoursRounded = round1(baseAvailableHours);
  const availableHours = round1(effectiveAvailableHours);
  const bookedHours = round1(totalBookedHours);
  const targetPct = round1(targetRatio * 100);
  const targetHours = availableHours > 0 ? round1((availableHours * targetPct) / 100) : 0;
  const chargeabilityPct = round1(forecast.chg * 100);
  const unassignedHours = round1(Math.max(0, availableHours - bookedHours));

  return {
    resource: resource.displayName,
    eid: resource.eid,
    month: input.month,
    periodStart: monthStart.toISOString().slice(0, 10),
    periodEnd: monthEnd.toISOString().slice(0, 10),
    fte: round1(resource.fte),
    target: `${targetPct}%`,
    targetPct,
    targetHours,
    workingDays,
    baseWorkingDays: baseWorkingDaysRounded,
    locationContext: {
      countryCode: resource.country?.code ?? null,
      country: resource.country?.name ?? resource.country?.code ?? null,
      federalState: resource.federalState ?? null,
      metroCity: resource.metroCity?.name ?? null,
    },
    baseAvailableHours: baseAvailableHoursRounded,
    availableHours,
    bookedHours,
    unassignedHours,
    chargeability: `${chargeabilityPct}%`,
    chargeabilityPct,
    onTarget: chargeabilityPct >= targetPct,
    holidaySummary: {
      count: formattedHolidays.length,
      workdayCount: round1(publicHolidayWorkdayCount),
      hoursDeduction: round1(publicHolidayHoursDeduction),
      holidays: formattedHolidays,
      breakdown: summarizeResolvedHolidaySummary(formattedHolidays),
    },
    absenceSummary: {
      dayEquivalent: round1(absenceDayEquivalent),
      hoursDeduction: round1(absenceHoursDeduction),
    },
    capacityBreakdown: {
      formula: "baseAvailableHours - holidayHoursDeduction - absenceHoursDeduction = availableHours",
      baseAvailableHours: baseAvailableHoursRounded,
      holidayHoursDeduction: round1(publicHolidayHoursDeduction),
      absenceHoursDeduction: round1(absenceHoursDeduction),
      availableHours,
    },
    averages: {
      availableHoursPerWorkingDay: averagePerWorkingDay(availableHours, workingDays),
      bookedHoursPerWorkingDay: averagePerWorkingDay(bookedHours, workingDays),
      remainingHoursPerWorkingDay: averagePerWorkingDay(Math.max(0, availableHours - bookedHours), workingDays),
    },
    allocations: assignmentBreakdown,
    scheduleContext: {
      dailyWorkingHours: dailyHours,
      hasScheduleRules: Boolean(scheduleRules),
    },
  };
}

export async function listResourceSummaries(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResourceSummarySnapshotInputSchema>,
) {
  const resources = await readResourceSummariesSnapshot(ctx, {
    isActive: input.isActive,
    limit: input.limit,
    ...(input.search ? { search: input.search } : {}),
    ...(input.country ? { country: input.country } : {}),
    ...(input.metroCity ? { metroCity: input.metroCity } : {}),
    ...(input.orgUnit ? { orgUnit: input.orgUnit } : {}),
    ...(input.roleName ? { roleName: input.roleName } : {}),
  });
  return resources.map(mapResourceSummary);
}

export async function listResourceSummaryDetails(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResourceSummarySnapshotInputSchema>,
) {
  const resources = await readResourceSummaryDetailsSnapshot(ctx, {
    isActive: input.isActive,
    limit: input.limit,
    ...(input.search ? { search: input.search } : {}),
    ...(input.country ? { country: input.country } : {}),
    ...(input.metroCity ? { metroCity: input.metroCity } : {}),
    ...(input.orgUnit ? { orgUnit: input.orgUnit } : {}),
    ...(input.roleName ? { roleName: input.roleName } : {}),
  });
  return resources.map(mapResourceSummaryDetail);
}

export async function listResourceDirectory(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResourceDirectoryQuerySchema>,
) {
  return listResourceDirectoryEntries(ctx, input);
}

export async function listResourceChapters(ctx: ResourceSummaryReadContext) {
  const resources = await ctx.db.resource.findMany({
    where: { isActive: true, chapter: { not: null } },
    select: { chapter: true },
    distinct: ["chapter"],
    orderBy: { chapter: "asc" },
  });
  return resources.map((resource) => resource.chapter as string);
}

export async function listStaffResourceEntries(
  ctx: ResourceSummaryReadContext,
  input: z.infer<typeof ResourceListQuerySchema>,
) {
  return listStaffResources(ctx, input);
}
