import { TRPCError } from "@trpc/server";
import { calculateEffectiveAvailableHours, calculateEffectiveBookedHours } from "../lib/resource-capacity.js";
import {
  calculateScenarioEntryHours,
  collectScenarioSkillSet,
  getScenarioAvailability,
  loadScenarioAvailabilityContexts,
  roundToTenths,
  scenarioResourceSelect,
  scenarioSimulationAssignmentInclude,
  scenarioSimulationProjectSelect,
  scenarioUtilizationAssignmentSelect,
  type ScenarioChangeInput,
  type ScenarioDb,
  type ScenarioEntry,
} from "./scenario-shared.js";

export async function simulateProjectScenario(
  db: ScenarioDb,
  input: {
    projectId: string;
    changes: ScenarioChangeInput[];
  },
) {
  const { projectId, changes } = input;

  const project = await db.project.findUnique({
    where: { id: projectId },
    select: scenarioSimulationProjectSelect,
  });
  if (!project) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
  }

  const currentAssignments = await db.assignment.findMany({
    where: { projectId, status: { not: "CANCELLED" } },
    include: scenarioSimulationAssignmentInclude,
  });

  const resourceIds = new Set<string>();
  for (const change of changes) {
    if (change.resourceId) resourceIds.add(change.resourceId);
  }
  for (const assignment of currentAssignments) {
    if (assignment.resourceId) resourceIds.add(assignment.resourceId);
  }

  const resources = await db.resource.findMany({
    where: { id: { in: [...resourceIds] } },
    select: scenarioResourceSelect,
  });
  const resourceMap = new Map(resources.map((resource) => [resource.id, resource]));

  const removedAssignmentIds = new Set(
    changes.filter((change) => change.remove && change.assignmentId).map((change) => change.assignmentId!),
  );
  const modifiedAssignmentIds = new Set(
    changes.filter((change) => !change.remove && change.assignmentId).map((change) => change.assignmentId!),
  );

  const scenarioEntries: ScenarioEntry[] = [];

  for (const assignment of currentAssignments) {
    if (removedAssignmentIds.has(assignment.id)) continue;
    if (modifiedAssignmentIds.has(assignment.id)) continue;

    scenarioEntries.push({
      resourceId: assignment.resourceId,
      lcrCents: assignment.resource?.lcrCents ?? 0,
      hoursPerDay: assignment.hoursPerDay,
      startDate: assignment.startDate,
      endDate: assignment.endDate,
      availability: getScenarioAvailability(assignment.resource?.availability),
      isNew: false,
    });
  }

  for (const change of changes) {
    if (change.remove) continue;

    const resource = change.resourceId ? resourceMap.get(change.resourceId) : null;
    scenarioEntries.push({
      resourceId: change.resourceId ?? null,
      lcrCents: resource?.lcrCents ?? 0,
      hoursPerDay: change.hoursPerDay,
      startDate: change.startDate,
      endDate: change.endDate,
      availability: getScenarioAvailability(resource?.availability),
      isNew: !change.assignmentId,
    });
  }

  const affectedResourceIds = [...new Set(
    scenarioEntries
      .map((entry) => entry.resourceId)
      .filter((resourceId): resourceId is string => Boolean(resourceId)),
  )];

  const allAssignmentsForResources = affectedResourceIds.length > 0
    ? await db.assignment.findMany({
        where: {
          resourceId: { in: affectedResourceIds },
          status: { not: "CANCELLED" },
        },
        select: scenarioUtilizationAssignmentSelect,
      })
    : [];

  const assignmentsByResource = new Map<string, typeof allAssignmentsForResources>();
  for (const assignment of allAssignmentsForResources) {
    if (!assignment.resourceId) continue;
    const existing = assignmentsByResource.get(assignment.resourceId) ?? [];
    existing.push(assignment);
    assignmentsByResource.set(assignment.resourceId, existing);
  }

  let windowStart = project.startDate;
  let windowEnd = project.endDate;
  for (const entry of scenarioEntries) {
    if (entry.startDate < windowStart) windowStart = entry.startDate;
    if (entry.endDate > windowEnd) windowEnd = entry.endDate;
  }

  const contexts = await loadScenarioAvailabilityContexts(
    db,
    resources,
    windowStart,
    windowEnd,
  );

  let scenarioCostCents = 0;
  let scenarioHours = 0;
  for (const entry of scenarioEntries) {
    const totalHours = calculateScenarioEntryHours(entry, {
      periodStart: windowStart,
      periodEnd: windowEnd,
      contexts,
    });
    scenarioCostCents += Math.round(totalHours * entry.lcrCents);
    scenarioHours += totalHours;
  }

  let baselineCostCents = 0;
  let baselineHours = 0;
  for (const assignment of currentAssignments) {
    const totalHours = calculateScenarioEntryHours({
      resourceId: assignment.resourceId,
      lcrCents: assignment.resource?.lcrCents ?? 0,
      hoursPerDay: assignment.hoursPerDay,
      startDate: assignment.startDate,
      endDate: assignment.endDate,
      availability: getScenarioAvailability(assignment.resource?.availability),
    }, {
      periodStart: windowStart,
      periodEnd: windowEnd,
      contexts,
    });
    baselineHours += totalHours;
    baselineCostCents += Math.round(totalHours * (assignment.resource?.lcrCents ?? 0));
  }

  const resourceImpacts = affectedResourceIds.map((resourceId) => {
    const resource = resourceMap.get(resourceId);
    if (!resource) return null;

    const availability = getScenarioAvailability(resource.availability);
    const context = contexts.get(resourceId);
    const totalAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart: windowStart,
      periodEnd: windowEnd,
      context,
    });

    const currentProjectAssignments = (assignmentsByResource.get(resourceId) ?? []).filter(
      (assignment) => assignment.projectId === projectId,
    );
    let currentProjectHours = 0;
    for (const assignment of currentProjectAssignments) {
      currentProjectHours += calculateEffectiveBookedHours({
        availability,
        startDate: assignment.startDate,
        endDate: assignment.endDate,
        hoursPerDay: assignment.hoursPerDay,
        periodStart: windowStart,
        periodEnd: windowEnd,
        context,
      });
    }

    const scenarioResourceEntries = scenarioEntries.filter((entry) => entry.resourceId === resourceId);
    let scenarioProjectHours = 0;
    for (const entry of scenarioResourceEntries) {
      scenarioProjectHours += calculateEffectiveBookedHours({
        availability,
        startDate: entry.startDate,
        endDate: entry.endDate,
        hoursPerDay: entry.hoursPerDay,
        periodStart: windowStart,
        periodEnd: windowEnd,
        context,
      });
    }

    const otherProjectAssignments = (assignmentsByResource.get(resourceId) ?? []).filter(
      (assignment) => assignment.projectId !== projectId,
    );
    let otherProjectsHours = 0;
    for (const assignment of otherProjectAssignments) {
      otherProjectsHours += calculateEffectiveBookedHours({
        availability,
        startDate: assignment.startDate,
        endDate: assignment.endDate,
        hoursPerDay: assignment.hoursPerDay,
        periodStart: windowStart,
        periodEnd: windowEnd,
        context,
      });
    }

    const currentTotalHours = otherProjectsHours + currentProjectHours;
    const scenarioTotalHours = otherProjectsHours + scenarioProjectHours;
    const currentUtilization = totalAvailableHours > 0 ? (currentTotalHours / totalAvailableHours) * 100 : 0;
    const scenarioUtilization = totalAvailableHours > 0 ? (scenarioTotalHours / totalAvailableHours) * 100 : 0;

    return {
      resourceId,
      resourceName: resource.displayName,
      chargeabilityTarget: resource.chargeabilityTarget,
      currentUtilization: roundToTenths(currentUtilization),
      scenarioUtilization: roundToTenths(scenarioUtilization),
      utilizationDelta: roundToTenths(scenarioUtilization - currentUtilization),
      isOverallocated: scenarioUtilization > 100,
    };
  }).filter((impact): impact is NonNullable<typeof impact> => impact !== null);

  const warnings: string[] = [];
  for (const impact of resourceImpacts) {
    if (impact.isOverallocated) {
      warnings.push(
        `${impact.resourceName} would be at ${impact.scenarioUtilization.toFixed(1)}% utilization (over-allocated)`,
      );
    }
  }

  const budgetCents = project.budgetCents ?? 0;
  if (budgetCents > 0 && scenarioCostCents > budgetCents) {
    const overBudgetPct = Math.round(((scenarioCostCents - budgetCents) / budgetCents) * 100);
    warnings.push(`Scenario exceeds budget by ${overBudgetPct}%`);
  }

  const currentSkills = new Set<string>();
  for (const assignment of currentAssignments) {
    for (const skill of collectScenarioSkillSet(assignment.resource?.skills as Array<{ skill?: string | null }> | null | undefined)) {
      currentSkills.add(skill);
    }
  }

  const scenarioSkills = new Set<string>();
  for (const entry of scenarioEntries) {
    if (!entry.resourceId) continue;
    const resource = resourceMap.get(entry.resourceId);
    for (const skill of collectScenarioSkillSet(resource?.skills as Array<{ skill?: string | null }> | null | undefined)) {
      scenarioSkills.add(skill);
    }
  }

  const baselineSkillCount = currentSkills.size;
  const scenarioSkillCount = scenarioSkills.size;
  const skillCoveragePct = baselineSkillCount > 0
    ? Math.round((scenarioSkillCount / baselineSkillCount) * 100)
    : scenarioSkillCount > 0 ? 100 : 0;

  return {
    baseline: {
      totalCostCents: baselineCostCents,
      totalHours: baselineHours,
      headcount: currentAssignments.length,
      skillCount: baselineSkillCount,
    },
    scenario: {
      totalCostCents: scenarioCostCents,
      totalHours: scenarioHours,
      headcount: scenarioEntries.length,
      skillCount: scenarioSkillCount,
    },
    delta: {
      costCents: scenarioCostCents - baselineCostCents,
      hours: scenarioHours - baselineHours,
      headcount: scenarioEntries.length - currentAssignments.length,
      skillCoveragePct,
    },
    resourceImpacts,
    warnings,
    budgetCents,
  };
}
