import { countWorkingDays } from "@capakraken/engine";
import { countEstimateHandoffPlanningEntries } from "../allocation/count-estimate-handoff-planning-entries.js";
import { createAssignment } from "../allocation/create-assignment.js";
import { createDemandRequirement } from "../allocation/create-demand-requirement.js";
import {
  AllocationStatus,
  EstimateVersionStatus,
  type CreateEstimatePlanningHandoffInput,
  type EstimatePlanningHandoffAllocationRef,
  type EstimatePlanningHandoffResult,
  type WeekdayAvailability,
} from "@capakraken/shared";
import {
  ESTIMATE_DETAIL_INCLUDE,
  PROJECT_SNAPSHOT_SELECT,
  type EstimateDbClient,
  type EstimateVersionDetails,
  type EstimateWithDetails,
} from "./shared.js";

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

function roundHoursPerDay(value: number) {
  return Math.max(0.25, Math.min(24, Number(value.toFixed(2))));
}

function resolveVersion(
  estimate: EstimateWithDetails,
  versionId: string | undefined | null,
): EstimateVersionDetails {
  const version = versionId
    ? estimate.versions.find((candidate) => candidate.id === versionId)
    : estimate.versions.find((candidate) => candidate.status === EstimateVersionStatus.APPROVED);

  if (!version) {
    throw new Error(versionId ? "Estimate version not found" : "Estimate has no approved version");
  }

  if (version.status !== EstimateVersionStatus.APPROVED) {
    throw new Error("Only approved versions can be handed off to planning");
  }

  return version;
}

async function getEstimateOrThrow(
  db: EstimateDbClient,
  estimateId: string,
): Promise<EstimateWithDetails> {
  const estimate = await db.estimate.findUnique({
    where: { id: estimateId },
    include: ESTIMATE_DETAIL_INCLUDE,
  });

  if (!estimate) {
    throw new Error("Estimate not found");
  }

  return estimate;
}

function canFallbackToPlaceholder(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  return [
    "Resource not found",
    "Resource is required for non-placeholder allocations",
    "Resource has availability conflicts on",
  ].some((message) => error.message.startsWith(message));
}

export async function createEstimatePlanningHandoff(
  db: EstimateDbClient,
  input: CreateEstimatePlanningHandoffInput,
): Promise<EstimatePlanningHandoffResult> {
  const estimate = await getEstimateOrThrow(db, input.estimateId);
  const targetVersion = resolveVersion(estimate, input.versionId);

  if (!estimate.projectId) {
    throw new Error("Estimate must be linked to a project before planning handoff");
  }

  const project = await db.project.findUnique({
    where: { id: estimate.projectId },
    select: PROJECT_SNAPSHOT_SELECT,
  });

  if (!project) {
    throw new Error("Linked project not found");
  }

  if (project.endDate < project.startDate) {
    throw new Error("Linked project has an invalid date range");
  }

  const existingHandoffPlanningEntryCount =
    await countEstimateHandoffPlanningEntries(db, {
      projectId: estimate.projectId,
      estimateVersionId: targetVersion.id,
    });

  if (existingHandoffPlanningEntryCount > 0) {
    throw new Error("Planning handoff already exists for this approved version");
  }

  const resourceIds = [
    ...new Set(
      targetVersion.demandLines
        .map((line) => line.resourceId)
        .filter((resourceId): resourceId is string => typeof resourceId === "string"),
    ),
  ];

  const resources = resourceIds.length
    ? await db.resource.findMany({
        where: { id: { in: resourceIds } },
        select: {
          id: true,
          availability: true,
        },
      })
    : [];

  const resourceMap = new Map(
    resources.map((resource) => [
      resource.id,
      resource.availability as unknown as WeekdayAvailability,
    ]),
  );

  const createdHandoff = await db.$transaction(async (tx) => {
    const handoffAllocations: EstimatePlanningHandoffAllocationRef[] = [];
    let fallbackPlaceholderCount = 0;

    for (const line of targetVersion.demandLines) {
      if (line.hours <= 0) {
        continue;
      }

      const headcount = Math.max(1, Math.ceil(line.fte != null && line.fte > 0 ? line.fte : 1));
      const resourceAvailability =
        line.resourceId != null ? resourceMap.get(line.resourceId) : null;
      const workingDays = countWorkingDays(
        project.startDate,
        project.endDate,
        resourceAvailability ?? STANDARD_BUSINESS_AVAILABILITY,
      );

      if (workingDays <= 0) {
        throw new Error(`Project window has no working days for demand line "${line.name}"`);
      }

      const hoursPerDay = roundHoursPerDay(line.hours / workingDays / headcount);
      const percentage = Math.max(1, Math.min(100, Math.round((hoursPerDay / 8) * 100)));
      const estimateHandoffMetadata = {
        estimateId: estimate.id,
        estimateVersionId: targetVersion.id,
        estimateVersionNumber: targetVersion.versionNumber,
        estimateDemandLineId: line.id,
        estimateDemandLineName: line.name,
        handedOffAt: new Date().toISOString(),
        estimatedHours: line.hours,
        estimatedDays: line.days,
        estimatedFte: line.fte,
        costRateCents: line.costRateCents,
        billRateCents: line.billRateCents,
        currency: line.currency,
        costTotalCents: line.costTotalCents,
        priceTotalCents: line.priceTotalCents,
        sourceMetadata: line.metadata,
        sourceStaffingAttributes: line.staffingAttributes,
        sourceMonthlySpread: line.monthlySpread,
      } satisfies Record<string, unknown>;
      const demandMetadata = {
        ...(line.resourceId ? { suggestedResourceId: line.resourceId } : {}),
        estimateHandoff: estimateHandoffMetadata,
      } satisfies Record<string, unknown>;
      try {
        if (line.resourceId) {
          const demandRequirement = await createDemandRequirement(
            tx as unknown as Parameters<typeof createDemandRequirement>[0],
            {
              projectId: project.id,
              startDate: project.startDate,
              endDate: project.endDate,
              hoursPerDay,
              percentage,
              role: line.name,
              roleId: line.roleId ?? undefined,
              headcount,
              status: AllocationStatus.PROPOSED,
              metadata: {
                ...demandMetadata,
                estimateHandoff: {
                  ...estimateHandoffMetadata,
                  handoffMode: "resource",
                  linkedResourceId: line.resourceId,
                },
              },
            },
          );
          const assignment = await createAssignment(
            tx as unknown as Parameters<typeof createAssignment>[0],
            {
              resourceId: line.resourceId,
              demandRequirementId: demandRequirement.id,
              projectId: project.id,
              startDate: project.startDate,
              endDate: project.endDate,
              hoursPerDay,
              percentage,
              role: line.name,
              roleId: line.roleId ?? undefined,
              status: AllocationStatus.PROPOSED,
              metadata: {
                estimateHandoff: {
                  ...estimateHandoffMetadata,
                  handoffMode: "resource",
                  linkedResourceId: line.resourceId,
                },
              },
            },
          );

          handoffAllocations.push({
            id: assignment.id,
            projectId: assignment.projectId,
            resourceId: assignment.resourceId,
            isPlaceholder: false,
          });
          continue;
        }
      } catch (error) {
        if (!canFallbackToPlaceholder(error)) {
          throw error;
        }
      }

      const placeholder = await createDemandRequirement(
        tx as unknown as Parameters<typeof createDemandRequirement>[0],
        {
          projectId: project.id,
          startDate: project.startDate,
          endDate: project.endDate,
          hoursPerDay,
          percentage,
          role: line.name,
          roleId: line.roleId ?? undefined,
          headcount,
          status: AllocationStatus.PROPOSED,
          metadata: {
            ...(line.resourceId ? { suggestedResourceId: line.resourceId } : {}),
            estimateHandoff: {
              ...estimateHandoffMetadata,
              handoffMode: line.resourceId ? "fallback_placeholder" : "placeholder",
              ...(line.resourceId ? { suggestedResourceId: line.resourceId } : {}),
            },
          },
        },
      );

      if (line.resourceId) {
        fallbackPlaceholderCount += 1;
      }

      handoffAllocations.push({
        id: placeholder.id,
        projectId: placeholder.projectId,
        resourceId: null,
        isPlaceholder: true,
      });
    }

    return { allocations: handoffAllocations, fallbackPlaceholderCount };
  });

  const placeholderCount = createdHandoff.allocations.filter(
    (allocation) => allocation.isPlaceholder,
  ).length;
  const assignedCount = createdHandoff.allocations.length - placeholderCount;

  return {
    estimateId: estimate.id,
    estimateVersionId: targetVersion.id,
    estimateVersionNumber: targetVersion.versionNumber,
    projectId: project.id,
    createdCount: createdHandoff.allocations.length,
    placeholderCount,
    assignedCount,
    fallbackPlaceholderCount: createdHandoff.fallbackPlaceholderCount,
    allocations: createdHandoff.allocations,
  };
}
