import { buildSplitAllocationReadModel } from "@capakraken/application";
import { Prisma } from "@capakraken/db";
import type { PrismaClient } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
import type { TRPCContext } from "../trpc.js";
import {
  PROJECT_PLANNING_DEMAND_INCLUDE,
  TIMELINE_ASSIGNMENT_INCLUDE,
} from "./project-planning-read-model.js";

export type ShiftDbClient = Pick<
  PrismaClient,
  "project" | "demandRequirement" | "assignment"
>;

export type TimelineEntriesDbClient = Pick<
  PrismaClient,
  "demandRequirement" | "assignment" | "resource" | "project" | "holidayCalendar" | "country" | "metroCity"
>;

export type TimelineEntriesFilters = {
  startDate: Date;
  endDate: Date;
  resourceIds?: string[] | undefined;
  projectIds?: string[] | undefined;
  clientIds?: string[] | undefined;
  chapters?: string[] | undefined;
  eids?: string[] | undefined;
  countryCodes?: string[] | undefined;
};

export const TimelineWindowFiltersSchema = z.object({
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
  resourceIds: z.array(z.string()).optional(),
  projectIds: z.array(z.string()).optional(),
  clientIds: z.array(z.string()).optional(),
  chapters: z.array(z.string()).optional(),
  eids: z.array(z.string()).optional(),
  countryCodes: z.array(z.string()).optional(),
});

type TimelineWindowFiltersInput = z.infer<typeof TimelineWindowFiltersSchema>;
type TimelineSelfServiceContext = Pick<TRPCContext, "db" | "dbUser">;

export function getAssignmentResourceIds(
  readModel: ReturnType<typeof buildSplitAllocationReadModel>,
): string[] {
  return [
    ...new Set(
      readModel.assignments
        .map((assignment) => assignment.resourceId)
        .filter((resourceId): resourceId is string => resourceId !== null),
    ),
  ];
}

export function fmtDate(value: Date | null | undefined): string | null {
  if (!value) {
    return null;
  }
  return value.toISOString().slice(0, 10);
}

function createUtcDate(year: number, month: number, day: number): Date {
  return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
}

export function createTimelineDateRange(input: {
  startDate?: string | undefined;
  endDate?: string | undefined;
  durationDays?: number | undefined;
}): { startDate: Date; endDate: Date } {
  const now = new Date();
  const startDate = input.startDate
    ? new Date(`${input.startDate}T00:00:00.000Z`)
    : createUtcDate(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());

  if (Number.isNaN(startDate.getTime())) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Invalid startDate: ${input.startDate}`,
    });
  }

  const endDate = input.endDate
    ? new Date(`${input.endDate}T00:00:00.000Z`)
    : createUtcDate(
        startDate.getUTCFullYear(),
        startDate.getUTCMonth(),
        startDate.getUTCDate() + Math.max((input.durationDays ?? 21) - 1, 0),
      );

  if (Number.isNaN(endDate.getTime())) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Invalid endDate: ${input.endDate}`,
    });
  }
  if (endDate < startDate) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "endDate must be on or after startDate.",
    });
  }

  return { startDate, endDate };
}

function normalizeStringList(values?: string[] | undefined): string[] | undefined {
  const normalized = values
    ?.map((value) => value.trim())
    .filter((value) => value.length > 0);

  return normalized && normalized.length > 0 ? normalized : undefined;
}

export function createTimelineFilters(input: {
  resourceIds?: string[] | undefined;
  projectIds?: string[] | undefined;
  clientIds?: string[] | undefined;
  chapters?: string[] | undefined;
  eids?: string[] | undefined;
  countryCodes?: string[] | undefined;
}): Omit<TimelineEntriesFilters, "startDate" | "endDate"> {
  return {
    resourceIds: normalizeStringList(input.resourceIds),
    projectIds: normalizeStringList(input.projectIds),
    clientIds: normalizeStringList(input.clientIds),
    chapters: normalizeStringList(input.chapters),
    eids: normalizeStringList(input.eids),
    countryCodes: normalizeStringList(input.countryCodes),
  };
}

export function createEmptyTimelineEntriesView() {
  return buildSplitAllocationReadModel({
    demandRequirements: [],
    assignments: [],
  });
}

async function findOwnedTimelineResourceId(
  ctx: TimelineSelfServiceContext,
): Promise<string | null> {
  if (!ctx.dbUser?.id) {
    return null;
  }

  if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
    return null;
  }

  const resource = await ctx.db.resource.findFirst({
    where: { userId: ctx.dbUser.id },
    select: { id: true },
  });

  return resource?.id ?? null;
}

export async function buildSelfServiceTimelineInput(
  ctx: TimelineSelfServiceContext,
  input: TimelineWindowFiltersInput,
): Promise<TimelineEntriesFilters | null> {
  const ownedResourceId = await findOwnedTimelineResourceId(ctx);
  if (!ownedResourceId) {
    return null;
  }

  return {
    startDate: input.startDate,
    endDate: input.endDate,
    resourceIds: [ownedResourceId],
    projectIds: normalizeStringList(input.projectIds),
    clientIds: normalizeStringList(input.clientIds),
  };
}

export function summarizeTimelineEntries(readModel: {
  allocations: Array<{ projectId: string | null; resourceId: string | null }>;
  demands: Array<{ projectId: string | null }>;
  assignments: Array<{ projectId: string | null; resourceId: string | null }>;
}) {
  const projectIds = new Set<string>();
  const resourceIds = new Set<string>();

  for (const entry of [...readModel.allocations, ...readModel.demands, ...readModel.assignments]) {
    if (entry.projectId) {
      projectIds.add(entry.projectId);
    }
  }

  for (const assignment of [...readModel.allocations, ...readModel.assignments]) {
    if (assignment.resourceId) {
      resourceIds.add(assignment.resourceId);
    }
  }

  return {
    allocationCount: readModel.allocations.length,
    demandCount: readModel.demands.length,
    assignmentCount: readModel.assignments.length,
    projectCount: projectIds.size,
    resourceCount: resourceIds.size,
  };
}

export function rangesOverlap(
  leftStart: Date,
  leftEnd: Date,
  rightStart: Date,
  rightEnd: Date,
): boolean {
  return leftStart <= rightEnd && rightStart <= leftEnd;
}

export function toDate(value: Date | string): Date {
  return value instanceof Date ? value : new Date(value);
}

export function anonymizeResourceOnEntry<T extends { resource?: { id: string } | null }>(
  entry: T,
  directory: Awaited<ReturnType<typeof getAnonymizationDirectory>>,
): T {
  if (!entry.resource) {
    return entry;
  }
  return {
    ...entry,
    resource: anonymizeResource(entry.resource, directory),
  };
}

export async function loadTimelineEntriesReadModel(
  db: TimelineEntriesDbClient,
  input: TimelineEntriesFilters,
) {
  const { startDate, endDate, resourceIds, projectIds, clientIds, chapters, eids, countryCodes } = input;

  const effectiveResourceIds = await (async () => {
    if (resourceIds && resourceIds.length > 0) return resourceIds;
    const hasChapters = chapters && chapters.length > 0;
    const hasEids = eids && eids.length > 0;
    const hasCountry = countryCodes && countryCodes.length > 0;
    if (!hasChapters && !hasEids && !hasCountry) return undefined;

    const andConditions: Record<string, unknown>[] = [];
    if (hasChapters) andConditions.push({ chapter: { in: chapters } });
    if (hasEids) andConditions.push({ eid: { in: eids } });
    if (hasCountry) andConditions.push({ country: { code: { in: countryCodes } } });

    const matching = await db.resource.findMany({
      where: (andConditions.length === 1 ? andConditions[0]! : { AND: andConditions }) as Prisma.ResourceWhereInput,
      select: { id: true },
    });
    return matching.map((resource) => resource.id);
  })();

  const effectiveProjectIds = await (async () => {
    if (!clientIds || clientIds.length === 0) return projectIds;

    const matchingProjects = await db.project.findMany({
      where: { clientId: { in: clientIds } },
      select: { id: true },
    });
    const clientProjectIds = matchingProjects.map((project) => project.id);

    if (!projectIds || projectIds.length === 0) {
      return clientProjectIds;
    }

    const allowedIds = new Set(clientProjectIds);
    return projectIds.filter((projectId) => allowedIds.has(projectId));
  })();

  const excludeDemands = effectiveResourceIds !== undefined;

  const [demandRequirements, assignments] = await Promise.all([
    excludeDemands
      ? Promise.resolve([])
      : db.demandRequirement.findMany({
          where: {
            status: { not: "CANCELLED" },
            startDate: { lte: endDate },
            endDate: { gte: startDate },
            ...(effectiveProjectIds ? { projectId: { in: effectiveProjectIds } } : {}),
          },
          include: PROJECT_PLANNING_DEMAND_INCLUDE,
          orderBy: [{ startDate: "asc" }, { projectId: "asc" }],
        }),
    db.assignment.findMany({
      where: {
        status: { not: "CANCELLED" },
        startDate: { lte: endDate },
        endDate: { gte: startDate },
        ...(effectiveResourceIds ? { resourceId: { in: effectiveResourceIds } } : {}),
        ...(effectiveProjectIds ? { projectId: { in: effectiveProjectIds } } : {}),
      },
      include: TIMELINE_ASSIGNMENT_INCLUDE,
      orderBy: [{ startDate: "asc" }, { resourceId: "asc" }],
    }),
  ]);

  return buildSplitAllocationReadModel({ demandRequirements, assignments });
}
