import type { PrismaClient } from "@capakraken/db";
import type { WeekdayAvailability } from "@capakraken/shared";
import { loadDashboardPlanningReadModel } from "./load-dashboard-planning-read-model.js";
import { calculateAllocationHours } from "./shared.js";
import {
  calculateEffectiveAllocationHours,
  calculateEffectiveAvailableHours,
  loadDailyAvailabilityContexts,
} from "./holiday-capacity.js";

export interface GetDashboardDemandInput {
  startDate: Date;
  endDate: Date;
  groupBy: "project" | "person" | "chapter";
}

export interface DemandCalendarLocationSummary {
  countryCode: string | null;
  countryName: string | null;
  federalState: string | null;
  metroCityName: string | null;
  resourceCount: number;
  allocatedHours: number;
}

export interface DemandRowDerivation {
  periodStart: string;
  periodEnd: string;
  periodWorkingHoursBase: number;
  requiredHours: number | null;
  requiredFTEs: number;
  fillPct: number | null;
  demandSource: "DEMAND_REQUIREMENTS" | "PROJECT_STAFFING_REQS" | "NONE";
  calendarLocations: DemandCalendarLocationSummary[];
}

export interface DashboardDemandRow {
  id: string;
  name: string;
  shortCode: string;
  allocatedHours: number;
  requiredFTEs: number;
  resourceCount: number;
  derivation?: DemandRowDerivation;
}

interface ProjectSummary {
  id: string;
  name: string;
  shortCode: string;
  staffingReqs: unknown;
}

function hasAvailability<T extends { availability?: unknown }>(
  resource: T | null | undefined,
): resource is T & { availability: WeekdayAvailability } {
  return resource !== null && resource !== undefined && resource.availability !== null && resource.availability !== undefined;
}

function getDemandFteFactor(hoursPerDay: number, percentage: number): number {
  const normalizedPercentage = percentage > 0 ? percentage : (hoursPerDay / 8) * 100;
  return normalizedPercentage / 100;
}

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

function toIsoDate(value: Date): string {
  return value.toISOString().slice(0, 10);
}

function buildLocationKey(input: {
  countryCode: string | null | undefined;
  countryName: string | null | undefined;
  federalState: string | null | undefined;
  metroCityName: string | null | undefined;
}): string {
  return JSON.stringify({
    countryCode: input.countryCode ?? null,
    countryName: input.countryName ?? null,
    federalState: input.federalState ?? null,
    metroCityName: input.metroCityName ?? null,
  });
}

function getProjectRequiredFTEs(staffingReqs: unknown): number {
  const requirements = Array.isArray(staffingReqs) ? staffingReqs : [];
  return requirements.reduce((sum, requirement) => {
    if (
      typeof requirement === "object" &&
      requirement !== null &&
      "fteCount" in requirement &&
      typeof requirement.fteCount === "number"
    ) {
      return sum + requirement.fteCount;
    }

    return sum;
  }, 0);
}

const FULL_TIME_AVAILABILITY: WeekdayAvailability = {
  sunday: 0,
  monday: 8,
  tuesday: 8,
  wednesday: 8,
  thursday: 8,
  friday: 8,
  saturday: 0,
};

function summarizeCalendarLocations(
  assignments: Array<{
    resource?: { id?: string | null } | null;
    startDate: Date | string;
    endDate: Date | string;
    hoursPerDay: number;
  }>,
  resourceInfoById: Map<string, {
    id: string;
    availability: WeekdayAvailability;
    countryCode: string | null | undefined;
    countryName: string | null | undefined;
    federalState: string | null | undefined;
    metroCityName: string | null | undefined;
  }>,
  contexts: Map<string, Awaited<ReturnType<typeof loadDailyAvailabilityContexts>> extends Map<string, infer T> ? T : never>,
  input: GetDashboardDemandInput,
): DemandCalendarLocationSummary[] {
  const locationMap = new Map<string, DemandCalendarLocationSummary & { resourceIds: Set<string> }>();

  for (const assignment of assignments) {
    const resourceId = assignment.resource?.id ?? undefined;
    const resource = resourceId ? resourceInfoById.get(resourceId) : undefined;
    if (!resource) {
      continue;
    }

    const hours = calculateEffectiveAllocationHours({
      availability: resource.availability,
      startDate: toDate(assignment.startDate),
      endDate: toDate(assignment.endDate),
      hoursPerDay: assignment.hoursPerDay,
      periodStart: input.startDate,
      periodEnd: input.endDate,
      context: contexts.get(resource.id),
    });

    const locationKey = buildLocationKey({
      countryCode: resource.countryCode,
      countryName: resource.countryName,
      federalState: resource.federalState,
      metroCityName: resource.metroCityName,
    });
    const existing = locationMap.get(locationKey) ?? {
      countryCode: resource.countryCode ?? null,
      countryName: resource.countryName ?? null,
      federalState: resource.federalState ?? null,
      metroCityName: resource.metroCityName ?? null,
      resourceCount: 0,
      allocatedHours: 0,
      resourceIds: new Set<string>(),
    };

    existing.allocatedHours += hours;
    existing.resourceIds.add(resource.id);
    existing.resourceCount = existing.resourceIds.size;
    locationMap.set(locationKey, existing);
  }

  return [...locationMap.values()]
    .map(({ resourceIds: _resourceIds, ...summary }) => ({
      ...summary,
      allocatedHours: Math.round(summary.allocatedHours * 10) / 10,
    }))
    .sort((left, right) => right.allocatedHours - left.allocatedHours);
}

export async function getDashboardDemand(
  db: PrismaClient,
  input: GetDashboardDemandInput,
): Promise<DashboardDemandRow[]> {
  const { demandRequirements, assignments, projects, readModel } =
    await loadDashboardPlanningReadModel(db, {
      startDate: input.startDate,
      endDate: input.endDate,
    });

  const demandRequirementById = new Map(
    demandRequirements.map((demandRequirement) => [
      demandRequirement.id,
      demandRequirement,
    ]),
  );
  const normalizedAssignments = readModel.assignments;
  const normalizedDemands = readModel.demands;
  const resourceProfiles = assignments
    .map((assignment) => assignment.resource)
    .filter(hasAvailability)
    .map((resource) => ({
      id: resource.id,
      availability: resource.availability as unknown as WeekdayAvailability,
      countryId: resource.countryId,
      countryCode: resource.country?.code,
      countryName: resource.country?.name,
      federalState: resource.federalState,
      metroCityId: resource.metroCityId,
      metroCityName: resource.metroCity?.name,
    }));
  const resourceInfoById = new Map(
    resourceProfiles.map((resource) => [resource.id, resource]),
  );
  const contexts = await loadDailyAvailabilityContexts(
    db,
    [...new Map(resourceProfiles.map((resource) => [resource.id, resource])).values()],
    input.startDate,
    input.endDate,
  );

  const projectMap = new Map<string, ProjectSummary>(
    projects.map((project) => [project.id, project]),
  );
  for (const allocation of readModel.allocations) {
    if (!allocation.project || projectMap.has(allocation.project.id)) {
      continue;
    }

    projectMap.set(allocation.project.id, {
      id: allocation.project.id,
      name: allocation.project.name,
      shortCode: allocation.project.shortCode,
      staffingReqs: allocation.project.staffingReqs,
    });
  }

  const assignmentCountByDemandRequirementId = new Map<string, number>();
  for (const assignment of assignments) {
    if (!assignment.demandRequirementId) {
      continue;
    }

    assignmentCountByDemandRequirementId.set(
      assignment.demandRequirementId,
      (assignmentCountByDemandRequirementId.get(assignment.demandRequirementId) ?? 0) + 1,
    );
  }

  const periodWorkingHoursBase = calculateEffectiveAvailableHours({
    availability: FULL_TIME_AVAILABILITY,
    periodStart: input.startDate,
    periodEnd: input.endDate,
    context: undefined,
  });

  if (input.groupBy === "project") {
    const projectIds = new Set<string>([
      ...projectMap.keys(),
      ...normalizedAssignments.map((assignment) => assignment.projectId),
      ...normalizedDemands.map((demand) => demand.projectId),
    ]);

    return [...projectIds].map((projectId) => {
      const project = projectMap.get(projectId) ?? {
        id: projectId,
        name: projectId,
        shortCode: projectId,
        staffingReqs: [],
      };

      const projectAssignments = normalizedAssignments.filter(
        (assignment) => assignment.projectId === projectId,
      );
      const projectDemands = normalizedDemands.filter(
        (demand) => demand.projectId === projectId,
      );

      const allocatedHours = projectAssignments.reduce(
        (sum, assignment) => {
          const resource = assignment.resource?.id
            ? resourceInfoById.get(assignment.resource.id)
            : undefined;
          return sum + (
            resource
              ? calculateEffectiveAllocationHours({
                availability: resource.availability,
                startDate: toDate(assignment.startDate),
                endDate: toDate(assignment.endDate),
                hoursPerDay: assignment.hoursPerDay,
                periodStart: input.startDate,
                periodEnd: input.endDate,
                context: contexts.get(resource.id),
              })
              : calculateAllocationHours({
                startDate: toDate(assignment.startDate),
                endDate: toDate(assignment.endDate),
                hoursPerDay: assignment.hoursPerDay,
              })
          );
        },
        0,
      );
      const requiredFTEs =
        projectDemands.length > 0
          ? projectDemands.reduce((sum, demand) => {
              const demandFteFactor = getDemandFteFactor(
                demand.hoursPerDay,
                demand.percentage,
              );
              const explicitDemand = demandRequirementById.get(demand.id);
              if (!explicitDemand) {
                return sum + demand.requestedHeadcount * demandFteFactor;
              }

              const linkedAssignmentCount =
                assignmentCountByDemandRequirementId.get(explicitDemand.id) ?? 0;
              const plannedHeadcount =
                linkedAssignmentCount +
                (explicitDemand.status === "COMPLETED" ? 0 : explicitDemand.headcount);
              return sum + plannedHeadcount * demandFteFactor;
            }, 0)
          : getProjectRequiredFTEs(project.staffingReqs);
      const requiredHours = requiredFTEs > 0
        ? Math.round(requiredFTEs * periodWorkingHoursBase * 10) / 10
        : null;
      const fillPct = requiredHours && requiredHours > 0
        ? Math.round((allocatedHours / requiredHours) * 100)
        : null;
      const calendarLocations = summarizeCalendarLocations(
        projectAssignments,
        resourceInfoById,
        contexts,
        input,
      );

      return {
        id: project.id,
        name: project.name,
        shortCode: project.shortCode,
        allocatedHours: Math.round(allocatedHours),
        requiredFTEs: Math.round(requiredFTEs * 100) / 100,
        resourceCount: new Set(
          projectAssignments.map((assignment) => assignment.resource?.id).filter(Boolean),
        ).size,
        derivation: {
          periodStart: toIsoDate(input.startDate),
          periodEnd: toIsoDate(input.endDate),
          periodWorkingHoursBase,
          requiredHours,
          requiredFTEs: Math.round(requiredFTEs * 100) / 100,
          fillPct,
          demandSource: projectDemands.length > 0
            ? "DEMAND_REQUIREMENTS"
            : "PROJECT_STAFFING_REQS",
          calendarLocations,
        },
      };
    });
  }

  if (input.groupBy === "chapter") {
    const chapterMap = new Map<
      string,
      { allocatedHours: number; resourceIds: Set<string> }
    >();

    for (const assignment of normalizedAssignments) {
      const chapter = assignment.resource?.chapter ?? "Unassigned";
      const resource = assignment.resource?.id
        ? resourceInfoById.get(assignment.resource.id)
        : undefined;
      const existing = chapterMap.get(chapter) ?? {
        allocatedHours: 0,
        resourceIds: new Set<string>(),
      };

      if (assignment.resource?.id) {
        existing.resourceIds.add(assignment.resource.id);
      }

      existing.allocatedHours += resource
        ? calculateEffectiveAllocationHours({
          availability: resource.availability,
          startDate: toDate(assignment.startDate),
          endDate: toDate(assignment.endDate),
          hoursPerDay: assignment.hoursPerDay,
          periodStart: input.startDate,
          periodEnd: input.endDate,
          context: contexts.get(resource.id),
        })
        : calculateAllocationHours({
          startDate: toDate(assignment.startDate),
          endDate: toDate(assignment.endDate),
          hoursPerDay: assignment.hoursPerDay,
        });

      chapterMap.set(chapter, existing);
    }

    return [...chapterMap.entries()].map(([chapter, data]) => {
      const chapterAssignments = normalizedAssignments.filter(
        (assignment) => (assignment.resource?.chapter ?? "Unassigned") === chapter,
      );

      return {
        id: chapter,
        name: chapter,
        shortCode: chapter,
        allocatedHours: Math.round(data.allocatedHours),
        requiredFTEs: 0,
        resourceCount: data.resourceIds.size,
        derivation: {
          periodStart: toIsoDate(input.startDate),
          periodEnd: toIsoDate(input.endDate),
          periodWorkingHoursBase,
          requiredHours: null,
          requiredFTEs: 0,
          fillPct: null,
          demandSource: "NONE",
          calendarLocations: summarizeCalendarLocations(
            chapterAssignments,
            resourceInfoById,
            contexts,
            input,
          ),
        },
      };
    });
  }

  const personMap = new Map<
    string,
    {
      name: string;
      chapter: string | null;
      allocatedHours: number;
      projectIds: Set<string>;
    }
  >();

  for (const assignment of normalizedAssignments) {
    if (!assignment.resource) {
      continue;
    }

    const existing = personMap.get(assignment.resource.id) ?? {
      name: assignment.resource.displayName,
      chapter: assignment.resource.chapter ?? null,
      allocatedHours: 0,
      projectIds: new Set<string>(),
    };
    const resource = resourceInfoById.get(assignment.resource.id);

    existing.allocatedHours += resource
      ? calculateEffectiveAllocationHours({
        availability: resource.availability,
        startDate: toDate(assignment.startDate),
        endDate: toDate(assignment.endDate),
        hoursPerDay: assignment.hoursPerDay,
        periodStart: input.startDate,
        periodEnd: input.endDate,
        context: contexts.get(resource.id),
      })
      : calculateAllocationHours({
        startDate: toDate(assignment.startDate),
        endDate: toDate(assignment.endDate),
        hoursPerDay: assignment.hoursPerDay,
      });
    existing.projectIds.add(assignment.projectId);

    personMap.set(assignment.resource.id, existing);
  }

  return [...personMap.entries()].map(([id, data]) => {
    const personAssignments = normalizedAssignments.filter(
      (assignment) => assignment.resource?.id === id,
    );

    return {
      id,
      name: data.name,
      shortCode: data.chapter ?? "",
      allocatedHours: Math.round(data.allocatedHours),
      requiredFTEs: 0,
      resourceCount: data.projectIds.size,
      derivation: {
        periodStart: toIsoDate(input.startDate),
        periodEnd: toIsoDate(input.endDate),
        periodWorkingHoursBase,
        requiredHours: null,
        requiredFTEs: 0,
        fillPct: null,
        demandSource: "NONE",
        calendarLocations: summarizeCalendarLocations(
          personAssignments,
          resourceInfoById,
          contexts,
          input,
        ),
      },
    };
  });
}
