import type { WeekdayAvailability } from "@capakraken/shared";
import {
  DISPO_INTERNAL_PROJECT_BUCKETS,
  normalizeDispoRoleToken,
} from "@capakraken/shared";
import type { TxClient, AggregatedAssignment } from "./commit-dispo-batch-types.js";
import { deriveTbdDispoProjectIdentity } from "./tbd-projects.js";

function asObject(value: unknown): Record<string, unknown> {
  return value && typeof value === "object" && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : {};
}

function normalizeDate(date: Date): Date {
  return new Date(`${date.toISOString().slice(0, 10)}T00:00:00.000Z`);
}

function getDateKey(date: Date): string {
  return normalizeDate(date).toISOString().slice(0, 10);
}

function addDays(date: Date, days: number): Date {
  const next = normalizeDate(date);
  next.setUTCDate(next.getUTCDate() + days);
  return next;
}

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

const WEEKDAY_KEYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] as const;
const WORKDAY_KEYS = ["monday", "tuesday", "wednesday", "thursday", "friday"] as const;

function resolveInternalProjectShortCode(utilizationCategoryCode: string | null): string | null {
  return (
    DISPO_INTERNAL_PROJECT_BUCKETS.find(
      (bucket) => bucket.utilizationCategoryCode === utilizationCategoryCode,
    )?.shortCode ?? null
  );
}

export function aggregateAssignments(
  rows: Awaited<ReturnType<TxClient["stagedAssignment"]["findMany"]>>,
  resourceIdByKey: ReadonlyMap<string, string>,
  projectIdByShortCode: ReadonlyMap<string, string>,
  roleIdByName: ReadonlyMap<string, string>,
  resourceRoleNameByKey: ReadonlyMap<string, string>,
  importTbdProjects: boolean,
): AggregatedAssignment[] {
  const resolvedRows = rows
    .filter((row) => !row.isUnassigned && (!row.isTbd || importTbdProjects))
    .map((row) => {
      const projectShortCode = row.isInternal
        ? resolveInternalProjectShortCode(row.utilizationCategoryCode)
        : row.isTbd
          ? deriveTbdDispoProjectIdentity(
            String(asObject(row.rawPayload).rawToken ?? ""),
            row.utilizationCategoryCode ?? null,
          ).shortCode
          : (row.projectKey ?? null);
      const roleName =
        row.roleName ??
        normalizeDispoRoleToken(row.roleToken) ??
        resourceRoleNameByKey.get(row.resourceExternalId) ??
        null;
      const resourceId = resourceIdByKey.get(row.resourceExternalId);
      const projectId = projectShortCode ? projectIdByShortCode.get(projectShortCode) : null;
      const roleId = roleName ? roleIdByName.get(roleName) : null;

      if (!resourceId) {
        throw new Error(`Unable to resolve resource "${row.resourceExternalId}" during assignment commit`);
      }
      if (!projectShortCode || !projectId) {
        throw new Error(
          `Unable to resolve project for assignment resource "${row.resourceExternalId}" on ${getDateKey(row.assignmentDate ?? row.startDate ?? new Date())}`,
        );
      }
      if (!roleName || !roleId) {
        throw new Error(
          `Unable to resolve role for assignment resource "${row.resourceExternalId}" on ${getDateKey(row.assignmentDate ?? row.startDate ?? new Date())}`,
        );
      }
      if (row.assignmentDate === null || row.hoursPerDay === null || row.percentage === null) {
        throw new Error(`Assignment row "${row.id}" is missing normalized date or load information`);
      }

      return {
        assignmentDate: normalizeDate(row.assignmentDate),
        hoursPerDay: row.hoursPerDay,
        percentage: row.percentage,
        projectId,
        projectShortCode,
        resourceId,
        resourceKey: row.resourceExternalId,
        roleId,
        roleName,
        utilizationCategoryCode: row.utilizationCategoryCode ?? null,
      };
    })
    .sort((left, right) =>
      left.resourceKey.localeCompare(right.resourceKey) ||
      left.projectShortCode.localeCompare(right.projectShortCode) ||
      left.roleName.localeCompare(right.roleName) ||
      left.assignmentDate.getTime() - right.assignmentDate.getTime(),
    );

  const aggregated: AggregatedAssignment[] = [];

  for (const row of resolvedRows) {
    const previous = aggregated.at(-1);
    const canMerge = previous &&
      previous.resourceId === row.resourceId &&
      previous.projectId === row.projectId &&
      previous.roleId === row.roleId &&
      previous.hoursPerDay === row.hoursPerDay &&
      previous.percentage === row.percentage &&
      previous.endDate.getTime() === addDays(row.assignmentDate, -1).getTime();

    if (canMerge) {
      previous.endDate = row.assignmentDate;
      previous.sourceDates.push(getDateKey(row.assignmentDate));
      continue;
    }

    aggregated.push({
      endDate: row.assignmentDate,
      hoursPerDay: row.hoursPerDay,
      percentage: row.percentage,
      projectId: row.projectId,
      projectShortCode: row.projectShortCode,
      resourceId: row.resourceId,
      resourceKey: row.resourceKey,
      roleId: row.roleId,
      roleName: row.roleName,
      sourceDates: [getDateKey(row.assignmentDate)],
      startDate: row.assignmentDate,
      utilizationCategoryCode: row.utilizationCategoryCode,
    });
  }

  return aggregated;
}

export function deriveOverlayAvailability(
  baseAvailability: WeekdayAvailability,
  rules: Awaited<ReturnType<TxClient["stagedAvailabilityRule"]["findMany"]>>,
): WeekdayAvailability {
  const next = { ...baseAvailability };
  const weekdayVotes = new Map<string, Map<number, number>>();

  for (const rule of rules) {
    const date = rule.effectiveStartDate ?? rule.effectiveEndDate;
    if (!date) {
      continue;
    }
    const weekdayIndex = normalizeDate(date).getUTCDay();
    if (weekdayIndex === 0 || weekdayIndex === 6) {
      continue;
    }
    const weekdayKey = WEEKDAY_KEYS[weekdayIndex] as (typeof WORKDAY_KEYS)[number] | undefined;
    if (!weekdayKey || !WORKDAY_KEYS.includes(weekdayKey)) {
      continue;
    }
    const availableHours = rule.availableHours ?? (
      rule.percentage !== null && rule.percentage !== undefined
        ? roundToOneDecimal((rule.percentage / 100) * 8)
        : null
    );
    if (availableHours === null) {
      continue;
    }

    const hoursMap = weekdayVotes.get(weekdayKey) ?? new Map<number, number>();
    hoursMap.set(availableHours, (hoursMap.get(availableHours) ?? 0) + 1);
    weekdayVotes.set(weekdayKey, hoursMap);
  }

  for (const weekdayKey of WORKDAY_KEYS) {
    const hoursMap = weekdayVotes.get(weekdayKey);
    if (!hoursMap || hoursMap.size === 0) {
      continue;
    }

    const firstEntry = [...hoursMap.entries()].sort(
      (left, right) => right[1] - left[1] || left[0] - right[0],
    )[0];
    if (!firstEntry) {
      continue;
    }
    const [resolvedHours] = firstEntry;
    next[weekdayKey] = Math.min(next[weekdayKey], resolvedHours);
  }

  return next;
}
