import {
  createAssignment,
  deleteAllocationEntry,
  loadAllocationEntry,
  updateAssignment,
} from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import { AllocationStatus } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";

const ALLOCATION_GROUP_ID_KEY = "allocationGroupId";
const ALLOCATION_ORIGIN_ID_KEY = "allocationOriginId";
const ALLOCATION_FRAGMENTED_KEY = "isFragmentedAllocation";

function toUtcCalendarDate(value: Date): Date {
  return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
}

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

function toSharedAllocationStatus(status: unknown): AllocationStatus {
  switch (status) {
    case "PROPOSED":
      return AllocationStatus.PROPOSED;
    case "CONFIRMED":
      return AllocationStatus.CONFIRMED;
    case "ACTIVE":
      return AllocationStatus.ACTIVE;
    case "COMPLETED":
      return AllocationStatus.COMPLETED;
    case "CANCELLED":
      return AllocationStatus.CANCELLED;
    default:
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unsupported allocation status "${String(status)}" for fragmented assignment creation.`,
      });
  }
}

function readFragmentMetadata(metadata: unknown, allocationId: string) {
  const record =
    metadata && typeof metadata === "object" && !Array.isArray(metadata)
      ? (metadata as Record<string, unknown>)
      : {};
  const groupId =
    typeof record[ALLOCATION_GROUP_ID_KEY] === "string" && record[ALLOCATION_GROUP_ID_KEY].length > 0
      ? (record[ALLOCATION_GROUP_ID_KEY] as string)
      : crypto.randomUUID();
  const originId =
    typeof record[ALLOCATION_ORIGIN_ID_KEY] === "string" && record[ALLOCATION_ORIGIN_ID_KEY].length > 0
      ? (record[ALLOCATION_ORIGIN_ID_KEY] as string)
      : allocationId;

  return {
    groupId,
    metadata: {
      ...record,
      [ALLOCATION_GROUP_ID_KEY]: groupId,
      [ALLOCATION_ORIGIN_ID_KEY]: originId,
      [ALLOCATION_FRAGMENTED_KEY]: true,
    },
  };
}

export interface TimelineAllocationCarveResult {
  action: "updated" | "split" | "deleted";
  allocationGroupId: string;
  updatedAllocationIds: string[];
  createdAllocationIds: string[];
  deletedAllocationIds: string[];
  projectId: string;
  resourceId: string | null;
}

export interface TimelineAllocationExtractResult {
  action: "unchanged" | "extracted";
  allocationGroupId: string;
  extractedAllocationId: string;
  updatedAllocationIds: string[];
  createdAllocationIds: string[];
  projectId: string;
  resourceId: string | null;
}

export async function carveTimelineAllocationRange(input: {
  db: PrismaClient;
  allocationId: string;
  startDate: Date;
  endDate: Date;
}): Promise<TimelineAllocationCarveResult> {
  const resolved = await loadAllocationEntry(input.db, input.allocationId);
  if (resolved.kind !== "assignment") {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Only staffed assignments can currently be split into fragments.",
    });
  }

  const carveStart = toUtcCalendarDate(input.startDate);
  const carveEnd = toUtcCalendarDate(input.endDate);
  if (carveEnd < carveStart) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Carve end date must be on or after the carve start date.",
    });
  }

  const assignment = resolved.assignment;
  const assignmentStart = toUtcCalendarDate(assignment.startDate);
  const assignmentEnd = toUtcCalendarDate(assignment.endDate);
  if (carveStart < assignmentStart || carveEnd > assignmentEnd) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "The requested carve range must be fully inside the existing allocation.",
    });
  }

  const { groupId, metadata } = readFragmentMetadata(assignment.metadata, assignment.id);
  const leftEnd = addDays(carveStart, -1);
  const rightStart = addDays(carveEnd, 1);
  const hasLeftFragment = leftEnd >= assignmentStart;
  const hasRightFragment = rightStart <= assignmentEnd;

  return input.db.$transaction(async (tx) => {
    if (!hasLeftFragment && !hasRightFragment) {
      await deleteAllocationEntry(
        tx as unknown as Parameters<typeof deleteAllocationEntry>[0],
        resolved,
      );

      return {
        action: "deleted" as const,
        allocationGroupId: groupId,
        updatedAllocationIds: [],
        createdAllocationIds: [],
        deletedAllocationIds: [assignment.id],
        projectId: assignment.projectId,
        resourceId: assignment.resourceId,
      };
    }

    const updatedAllocationIds: string[] = [];
    const createdAllocationIds: string[] = [];

    const keptStart = hasLeftFragment ? assignmentStart : rightStart;
    const keptEnd = hasLeftFragment ? leftEnd : assignmentEnd;

    const updated = await updateAssignment(
      tx as unknown as Parameters<typeof updateAssignment>[0],
      assignment.id,
      {
        startDate: keptStart,
        endDate: keptEnd,
        metadata,
      },
    );
    updatedAllocationIds.push(updated.id);

    if (hasLeftFragment && hasRightFragment) {
      const created = await createAssignment(
        tx as unknown as Parameters<typeof createAssignment>[0],
        {
          demandRequirementId: assignment.demandRequirementId ?? undefined,
          resourceId: assignment.resourceId,
          projectId: assignment.projectId,
          startDate: rightStart,
          endDate: assignmentEnd,
          hoursPerDay: assignment.hoursPerDay,
          percentage: assignment.percentage,
          role: assignment.role ?? undefined,
          roleId: assignment.roleId ?? undefined,
          dailyCostCents: assignment.dailyCostCents,
          status: toSharedAllocationStatus(assignment.status),
          metadata,
        },
      );
      createdAllocationIds.push(created.id);
    }

    return {
      action: hasLeftFragment && hasRightFragment ? "split" as const : "updated" as const,
      allocationGroupId: groupId,
      updatedAllocationIds,
      createdAllocationIds,
      deletedAllocationIds: [],
      projectId: assignment.projectId,
      resourceId: assignment.resourceId,
    };
  });
}

export async function extractTimelineAllocationFragment(input: {
  db: PrismaClient;
  allocationId: string;
  startDate: Date;
  endDate: Date;
}): Promise<TimelineAllocationExtractResult> {
  const resolved = await loadAllocationEntry(input.db, input.allocationId);
  if (resolved.kind !== "assignment") {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Only staffed assignments can currently be extracted into fragments.",
    });
  }

  const extractStart = toUtcCalendarDate(input.startDate);
  const extractEnd = toUtcCalendarDate(input.endDate);
  if (extractEnd < extractStart) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Extract end date must be on or after the extract start date.",
    });
  }

  const assignment = resolved.assignment;
  const assignmentStart = toUtcCalendarDate(assignment.startDate);
  const assignmentEnd = toUtcCalendarDate(assignment.endDate);
  if (extractStart < assignmentStart || extractEnd > assignmentEnd) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "The requested extract range must be fully inside the existing allocation.",
    });
  }

  const { groupId, metadata } = readFragmentMetadata(assignment.metadata, assignment.id);
  const hasLeftFragment = extractStart > assignmentStart;
  const hasRightFragment = extractEnd < assignmentEnd;

  if (!hasLeftFragment && !hasRightFragment) {
    return {
      action: "unchanged",
      allocationGroupId: groupId,
      extractedAllocationId: assignment.id,
      updatedAllocationIds: [],
      createdAllocationIds: [],
      projectId: assignment.projectId,
      resourceId: assignment.resourceId,
    };
  }

  return input.db.$transaction(async (tx) => {
    const createdAllocationIds: string[] = [];

    const updated = await updateAssignment(
      tx as unknown as Parameters<typeof updateAssignment>[0],
      assignment.id,
      {
        startDate: extractStart,
        endDate: extractEnd,
        metadata,
      },
    );

    if (hasLeftFragment) {
      const createdLeft = await createAssignment(
        tx as unknown as Parameters<typeof createAssignment>[0],
        {
          demandRequirementId: assignment.demandRequirementId ?? undefined,
          resourceId: assignment.resourceId,
          projectId: assignment.projectId,
          startDate: assignmentStart,
          endDate: addDays(extractStart, -1),
          hoursPerDay: assignment.hoursPerDay,
          percentage: assignment.percentage,
          role: assignment.role ?? undefined,
          roleId: assignment.roleId ?? undefined,
          dailyCostCents: assignment.dailyCostCents,
          status: toSharedAllocationStatus(assignment.status),
          metadata,
        },
      );
      createdAllocationIds.push(createdLeft.id);
    }

    if (hasRightFragment) {
      const createdRight = await createAssignment(
        tx as unknown as Parameters<typeof createAssignment>[0],
        {
          demandRequirementId: assignment.demandRequirementId ?? undefined,
          resourceId: assignment.resourceId,
          projectId: assignment.projectId,
          startDate: addDays(extractEnd, 1),
          endDate: assignmentEnd,
          hoursPerDay: assignment.hoursPerDay,
          percentage: assignment.percentage,
          role: assignment.role ?? undefined,
          roleId: assignment.roleId ?? undefined,
          dailyCostCents: assignment.dailyCostCents,
          status: toSharedAllocationStatus(assignment.status),
          metadata,
        },
      );
      createdAllocationIds.push(createdRight.id);
    }

    return {
      action: "extracted" as const,
      allocationGroupId: groupId,
      extractedAllocationId: updated.id,
      updatedAllocationIds: [updated.id],
      createdAllocationIds,
      projectId: assignment.projectId,
      resourceId: assignment.resourceId,
    };
  });
}
