import { PreviewResolvedHolidaysSchema } from "@capakraken/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js";
import { protectedProcedure } from "../trpc.js";
import { type HolidayReadContext } from "./holiday-calendar-shared.js";
import { assertCanReadOwnedResource } from "./resource-owned-read-access.js";

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

function formatResolvedHolidayDetail(holiday: {
  date: string;
  name: string;
  scopeType: string;
  calendarName: string;
  sourceType: string;
}) {
  return {
    date: holiday.date,
    name: holiday.name,
    scope: holiday.scopeType,
    calendarName: holiday.calendarName,
    sourceType: holiday.sourceType,
  };
}

function summarizeResolvedHolidaysDetail(holidays: Array<{
  date: string;
  name: string;
  scope: string;
  calendarName: string;
  sourceType: string;
}>) {
  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 })),
  };
}

const ResolveHolidaysInputSchema = z.object({
  periodStart: z.coerce.date(),
  periodEnd: z.coerce.date(),
  countryId: z.string().optional(),
  countryCode: z.string().trim().min(1).optional(),
  stateCode: z.string().trim().min(1).optional(),
  metroCityId: z.string().optional(),
  metroCityName: z.string().trim().min(1).optional(),
}).superRefine((input, issueCtx) => {
  if (!input.countryId && !input.countryCode) {
    issueCtx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Either countryId or countryCode is required.",
      path: ["countryId"],
    });
  }
  if (input.periodEnd < input.periodStart) {
    issueCtx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "periodEnd must be on or after periodStart.",
      path: ["periodEnd"],
    });
  }
});

const ResolveResourceHolidaysInputSchema = z.object({
  resourceId: z.string(),
  periodStart: z.coerce.date(),
  periodEnd: z.coerce.date(),
}).superRefine((input, issueCtx) => {
  if (input.periodEnd < input.periodStart) {
    issueCtx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "periodEnd must be on or after periodStart.",
      path: ["periodEnd"],
    });
  }
});

async function readPreviewResolvedHolidaysSnapshot(
  ctx: HolidayReadContext,
  input: z.infer<typeof PreviewResolvedHolidaysSchema>,
) {
  const country = await findUniqueOrThrow(
    ctx.db.country.findUnique({
      where: { id: input.countryId },
      select: { id: true, code: true, name: true },
    }),
    "Country",
  );

  const metroCity = input.metroCityId
    ? await ctx.db.metroCity.findUnique({
        where: { id: input.metroCityId },
        select: { id: true, name: true, countryId: true },
      })
    : null;

  const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
    periodStart: new Date(`${input.year}-01-01T00:00:00.000Z`),
    periodEnd: new Date(`${input.year}-12-31T00:00:00.000Z`),
    countryId: input.countryId,
    countryCode: country.code,
    federalState: input.stateCode?.trim().toUpperCase() ?? null,
    metroCityId: input.metroCityId ?? null,
    metroCityName: metroCity?.name ?? null,
  });

  return {
    locationContext: {
      countryId: input.countryId,
      countryCode: country.code,
      stateCode: input.stateCode?.trim().toUpperCase() ?? null,
      metroCityId: input.metroCityId ?? null,
      metroCity: metroCity?.name ?? null,
      year: input.year,
    },
    holidays: resolved.map((holiday) => ({
      date: holiday.date,
      name: holiday.name,
      scopeType: holiday.scope,
      calendarName: holiday.calendarName,
      sourceType: holiday.sourceType,
    })),
  };
}

async function readResolvedHolidaysSnapshot(
  ctx: HolidayReadContext,
  input: z.infer<typeof ResolveHolidaysInputSchema>,
) {
  let resolvedCountryCode = input.countryCode?.trim().toUpperCase() ?? null;

  if (!resolvedCountryCode && input.countryId) {
    const country = await findUniqueOrThrow(
      ctx.db.country.findUnique({
        where: { id: input.countryId },
        select: { code: true },
      }),
      "Country",
    );
    resolvedCountryCode = country.code;
  }

  const metroCityName = input.metroCityId
    ? (await ctx.db.metroCity.findUnique({
        where: { id: input.metroCityId },
        select: { name: true },
      }))?.name ?? null
    : input.metroCityName?.trim() ?? null;

  const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
    periodStart: input.periodStart,
    periodEnd: input.periodEnd,
    countryId: input.countryId ?? null,
    countryCode: resolvedCountryCode,
    federalState: input.stateCode?.trim().toUpperCase() ?? null,
    metroCityId: input.metroCityId ?? null,
    metroCityName,
  });

  return {
    periodStart: input.periodStart.toISOString().slice(0, 10),
    periodEnd: input.periodEnd.toISOString().slice(0, 10),
    locationContext: {
      countryId: input.countryId ?? null,
      countryCode: resolvedCountryCode,
      federalState: input.stateCode?.trim().toUpperCase() ?? null,
      metroCityId: input.metroCityId ?? null,
      metroCity: metroCityName,
    },
    holidays: resolved.map((holiday) => ({
      date: holiday.date,
      name: holiday.name,
      scopeType: holiday.scope,
      calendarName: holiday.calendarName,
      sourceType: holiday.sourceType,
    })),
  };
}

async function readResolvedResourceHolidaysSnapshot(
  ctx: HolidayReadContext,
  input: z.infer<typeof ResolveResourceHolidaysInputSchema>,
) {
  await assertCanReadHolidayResource(ctx, input.resourceId);

  const resource = await findUniqueOrThrow(
    ctx.db.resource.findUnique({
      where: { id: input.resourceId },
      select: {
        id: true,
        eid: true,
        displayName: true,
        federalState: true,
        countryId: true,
        metroCityId: true,
        country: { select: { code: true, name: true } },
        metroCity: { select: { name: true } },
      },
    }),
    "Resource",
  );

  const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
    periodStart: input.periodStart,
    periodEnd: input.periodEnd,
    countryId: resource.countryId ?? null,
    countryCode: resource.country?.code ?? null,
    federalState: resource.federalState ?? null,
    metroCityId: resource.metroCityId ?? null,
    metroCityName: resource.metroCity?.name ?? null,
  });

  return {
    periodStart: input.periodStart.toISOString().slice(0, 10),
    periodEnd: input.periodEnd.toISOString().slice(0, 10),
    resource: {
      id: resource.id,
      eid: resource.eid,
      name: resource.displayName,
      country: resource.country?.name ?? resource.country?.code ?? null,
      countryCode: resource.country?.code ?? null,
      federalState: resource.federalState ?? null,
      metroCity: resource.metroCity?.name ?? null,
    },
    holidays: resolved.map((holiday) => ({
      date: holiday.date,
      name: holiday.name,
      scopeType: holiday.scope,
      calendarName: holiday.calendarName,
      sourceType: holiday.sourceType,
    })),
  };
}

export const holidayCalendarResolutionReadProcedures = {
  previewResolvedHolidays: protectedProcedure
    .input(PreviewResolvedHolidaysSchema)
    .query(async ({ ctx, input }) => (await readPreviewResolvedHolidaysSnapshot(ctx, input)).holidays),

  previewResolvedHolidaysDetail: protectedProcedure
    .input(PreviewResolvedHolidaysSchema)
    .query(async ({ ctx, input }) => {
      const resolved = await readPreviewResolvedHolidaysSnapshot(ctx, input);
      const holidays = resolved.holidays.map(formatResolvedHolidayDetail);
      return {
        count: holidays.length,
        locationContext: resolved.locationContext,
        summary: summarizeResolvedHolidaysDetail(holidays),
        holidays,
      };
    }),

  resolveHolidays: protectedProcedure
    .input(ResolveHolidaysInputSchema)
    .query(async ({ ctx, input }) => readResolvedHolidaysSnapshot(ctx, input)),

  resolveHolidaysDetail: protectedProcedure
    .input(ResolveHolidaysInputSchema)
    .query(async ({ ctx, input }) => {
      const resolved = await readResolvedHolidaysSnapshot(ctx, input);
      const holidays = resolved.holidays.map(formatResolvedHolidayDetail);
      return {
        periodStart: resolved.periodStart,
        periodEnd: resolved.periodEnd,
        locationContext: resolved.locationContext,
        count: holidays.length,
        summary: summarizeResolvedHolidaysDetail(holidays),
        holidays,
      };
    }),

  resolveResourceHolidays: protectedProcedure
    .input(ResolveResourceHolidaysInputSchema)
    .query(async ({ ctx, input }) => readResolvedResourceHolidaysSnapshot(ctx, input)),

  resolveResourceHolidaysDetail: protectedProcedure
    .input(ResolveResourceHolidaysInputSchema)
    .query(async ({ ctx, input }) => {
      const resolved = await readResolvedResourceHolidaysSnapshot(ctx, input);
      const holidays = resolved.holidays.map(formatResolvedHolidayDetail);
      return {
        periodStart: resolved.periodStart,
        periodEnd: resolved.periodEnd,
        resource: resolved.resource,
        count: holidays.length,
        summary: summarizeResolvedHolidaysDetail(holidays),
        holidays,
      };
    }),
};
