import { calculateAllocation } from "@capakraken/engine/allocation";
import type { WeekdayAvailability } from "@capakraken/shared";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, controllerProcedure, protectedProcedure } from "../trpc.js";
import { createAuditEntry } from "../lib/audit.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";

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

const ScenarioChangeSchema = z.object({
  /** Existing assignment to modify — omit to add a new allocation */
  assignmentId: z.string().optional(),
  resourceId: z.string().optional(),
  roleId: z.string().optional(),
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
  hoursPerDay: z.number().min(0).max(24),
  /** Set to true to mark an existing assignment for removal */
  remove: z.boolean().optional(),
});

const SimulateInputSchema = z.object({
  projectId: z.string(),
  changes: z.array(ScenarioChangeSchema).min(1),
});

export const scenarioRouter = createTRPCRouter({
  /**
   * Returns current allocations/costs for a project — the baseline for comparison.
   */
  getProjectBaseline: protectedProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const project = await ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: {
          id: true,
          name: true,
          shortCode: true,
          startDate: true,
          endDate: true,
          budgetCents: true,
          orderType: true,
        },
      });
      if (!project) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
      }

      const assignments = await ctx.db.assignment.findMany({
        where: {
          projectId: input.projectId,
          status: { not: "CANCELLED" },
        },
        include: {
          resource: {
            select: {
              id: true,
              displayName: true,
              eid: true,
              lcrCents: true,
              availability: true,
              chargeabilityTarget: true,
              skills: true,
              countryId: true,
              federalState: true,
              metroCityId: true,
              country: { select: { code: true } },
              metroCity: { select: { name: true } },
            },
          },
          roleEntity: { select: { id: true, name: true, color: true } },
        },
      });

      const demands = await ctx.db.demandRequirement.findMany({
        where: {
          projectId: input.projectId,
          status: { not: "CANCELLED" },
        },
        include: {
          roleEntity: { select: { id: true, name: true, color: true } },
        },
      });

      const assignmentRangeStart = assignments.length > 0
        ? new Date(Math.min(...assignments.map((assignment) => assignment.startDate.getTime())))
        : project.startDate;
      const assignmentRangeEnd = assignments.length > 0
        ? new Date(Math.max(...assignments.map((assignment) => assignment.endDate.getTime())))
        : project.endDate;
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        assignments
          .flatMap((assignment) => (assignment.resource ? [assignment.resource] : []))
          .map((resource) => ({
            id: resource.id,
            availability: resource.availability as unknown as WeekdayAvailability,
            countryId: resource.countryId,
            countryCode: resource.country?.code,
            federalState: resource.federalState,
            metroCityId: resource.metroCityId,
            metroCityName: resource.metroCity?.name,
          })),
        assignmentRangeStart,
        assignmentRangeEnd,
      );

      const baselineAllocations = assignments.map((a) => {
        const availability = (a.resource?.availability as typeof DEFAULT_AVAILABILITY) ?? DEFAULT_AVAILABILITY;
        const lcrCents = a.resource?.lcrCents ?? 0;
        const totalHours = a.resourceId
          ? calculateEffectiveBookedHours({
            availability,
            startDate: a.startDate,
            endDate: a.endDate,
            hoursPerDay: a.hoursPerDay,
            periodStart: assignmentRangeStart,
            periodEnd: assignmentRangeEnd,
            context: contexts.get(a.resourceId),
          })
          : calculateAllocation({
            lcrCents,
            hoursPerDay: a.hoursPerDay,
            startDate: a.startDate,
            endDate: a.endDate,
            availability,
          }).totalHours;
        const costCents = Math.round(totalHours * lcrCents);
        const workingDays = a.hoursPerDay > 0
          ? Math.round((totalHours / a.hoursPerDay) * 100) / 100
          : 0;

        return {
          id: a.id,
          resourceId: a.resourceId,
          resourceName: a.resource?.displayName ?? "Unknown",
          resourceEid: a.resource?.eid ?? "",
          lcrCents,
          roleId: a.roleId,
          roleName: a.roleEntity?.name ?? a.role ?? "",
          roleColor: a.roleEntity?.color ?? null,
          startDate: a.startDate.toISOString(),
          endDate: a.endDate.toISOString(),
          hoursPerDay: a.hoursPerDay,
          status: a.status,
          costCents,
          totalHours,
          workingDays,
        };
      });
      const totalCostCents = baselineAllocations.reduce((sum, allocation) => sum + allocation.costCents, 0);
      const totalHours = baselineAllocations.reduce((sum, allocation) => sum + allocation.totalHours, 0);

      const baselineDemands = demands.map((d) => ({
        id: d.id,
        roleId: d.roleId,
        roleName: d.roleEntity?.name ?? d.role ?? "",
        roleColor: d.roleEntity?.color ?? null,
        startDate: d.startDate.toISOString(),
        endDate: d.endDate.toISOString(),
        hoursPerDay: d.hoursPerDay,
        headcount: d.headcount,
        status: d.status,
      }));

      return {
        project,
        assignments: baselineAllocations,
        demands: baselineDemands,
        totalCostCents,
        totalHours,
        budgetCents: project.budgetCents,
      };
    }),

  /**
   * Pure simulation: computes cost/hours/utilization impact of scenario changes
   * without persisting anything.
   */
  simulate: controllerProcedure
    .input(SimulateInputSchema)
    .mutation(async ({ ctx, input }) => {
      const { projectId, changes } = input;

      // Load project
      const project = await ctx.db.project.findUnique({
        where: { id: projectId },
        select: { id: true, name: true, budgetCents: true, orderType: true, startDate: true, endDate: true },
      });
      if (!project) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
      }

      // Load current assignments for baseline
      const currentAssignments = await ctx.db.assignment.findMany({
        where: { projectId, status: { not: "CANCELLED" } },
        include: {
          resource: {
            select: {
              id: true,
              displayName: true,
              eid: true,
              lcrCents: true,
              availability: true,
              chargeabilityTarget: true,
              skills: true,
              countryId: true,
              federalState: true,
              metroCityId: true,
              country: { select: { code: true } },
              metroCity: { select: { name: true } },
            },
          },
        },
      });

      // Collect all resource IDs we need to look up (from changes)
      const resourceIds = new Set<string>();
      for (const c of changes) {
        if (c.resourceId) resourceIds.add(c.resourceId);
      }
      // Also add resources from existing assignments
      for (const a of currentAssignments) {
        if (a.resourceId) resourceIds.add(a.resourceId);
      }

      // Load resources
      const resources = await ctx.db.resource.findMany({
        where: { id: { in: [...resourceIds] } },
        select: {
          id: true,
          displayName: true,
          eid: true,
          lcrCents: true,
          availability: true,
          chargeabilityTarget: true,
          skills: true,
          countryId: true,
          federalState: true,
          metroCityId: true,
          country: { select: { code: true } },
          metroCity: { select: { name: true } },
        },
      });
      const resourceMap = new Map(resources.map((r) => [r.id, r]));

      // Load roles referenced in changes
      const roleIds = new Set<string>();
      for (const c of changes) {
        if (c.roleId) roleIds.add(c.roleId);
      }
      const roles = roleIds.size > 0
        ? await ctx.db.role.findMany({
            where: { id: { in: [...roleIds] } },
            select: { id: true, name: true, color: true },
          })
        : [];
      const roleMap = new Map(roles.map((r) => [r.id, r]));

      // Build scenario: start from current assignments, apply changes
      const removedAssignmentIds = new Set(
        changes.filter((c) => c.remove && c.assignmentId).map((c) => c.assignmentId!),
      );
      const modifiedAssignmentIds = new Set(
        changes.filter((c) => !c.remove && c.assignmentId).map((c) => c.assignmentId!),
      );

      // Keep untouched assignments
      const scenarioEntries: Array<{
        resourceId: string | null;
        lcrCents: number;
        hoursPerDay: number;
        startDate: Date;
        endDate: Date;
        availability: typeof DEFAULT_AVAILABILITY;
        isNew: boolean;
      }> = [];

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

        scenarioEntries.push({
          resourceId: a.resourceId,
          lcrCents: a.resource?.lcrCents ?? 0,
          hoursPerDay: a.hoursPerDay,
          startDate: a.startDate,
          endDate: a.endDate,
          availability: (a.resource?.availability as typeof DEFAULT_AVAILABILITY) ?? DEFAULT_AVAILABILITY,
          isNew: false,
        });
      }

      // Add modified and new entries from changes
      for (const c of changes) {
        if (c.remove) continue;

        const resource = c.resourceId ? resourceMap.get(c.resourceId) : null;
        const lcrCents = resource?.lcrCents ?? 0;
        const availability = (resource?.availability as typeof DEFAULT_AVAILABILITY) ?? DEFAULT_AVAILABILITY;

        scenarioEntries.push({
          resourceId: c.resourceId ?? null,
          lcrCents,
          hoursPerDay: c.hoursPerDay,
          startDate: c.startDate,
          endDate: c.endDate,
          availability,
          isNew: !c.assignmentId,
        });
      }

      // Compute per-resource utilization impact
      // Load ALL assignments for affected resources (across all projects) to measure total utilization
      const affectedResourceIds = [...new Set(scenarioEntries.map((e) => e.resourceId).filter(Boolean))] as string[];

      const allAssignmentsForResources = affectedResourceIds.length > 0
        ? await ctx.db.assignment.findMany({
            where: {
              resourceId: { in: affectedResourceIds },
              status: { not: "CANCELLED" },
            },
            select: {
              id: true,
              resourceId: true,
              projectId: true,
              hoursPerDay: true,
              startDate: true,
              endDate: true,
            },
          })
        : [];

      // Group by resource
      const assignmentsByResource = new Map<string, typeof allAssignmentsForResources>();
      for (const a of allAssignmentsForResources) {
        if (!a.resourceId) continue;
        const list = assignmentsByResource.get(a.resourceId) ?? [];
        list.push(a);
        assignmentsByResource.set(a.resourceId, list);
      }

      // Determine analysis window (the widest date range from scenario changes)
      let windowStart = project.startDate;
      let windowEnd = project.endDate;
      for (const e of scenarioEntries) {
        if (e.startDate < windowStart) windowStart = e.startDate;
        if (e.endDate > windowEnd) windowEnd = e.endDate;
      }

      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        resources.map((resource) => ({
          id: resource.id,
          availability: resource.availability as unknown as WeekdayAvailability,
          countryId: resource.countryId,
          countryCode: resource.country?.code,
          federalState: resource.federalState,
          metroCityId: resource.metroCityId,
          metroCityName: resource.metroCity?.name,
        })),
        windowStart,
        windowEnd,
      );

      function calculateEntryHours(entry: {
        resourceId: string | null;
        lcrCents: number;
        hoursPerDay: number;
        startDate: Date;
        endDate: Date;
        availability: typeof DEFAULT_AVAILABILITY;
      }) {
        if (!entry.resourceId) {
          return calculateAllocation({
            lcrCents: entry.lcrCents,
            hoursPerDay: entry.hoursPerDay,
            startDate: entry.startDate,
            endDate: entry.endDate,
            availability: entry.availability,
          }).totalHours;
        }

        return calculateEffectiveBookedHours({
          availability: entry.availability,
          startDate: entry.startDate,
          endDate: entry.endDate,
          hoursPerDay: entry.hoursPerDay,
          periodStart: windowStart,
          periodEnd: windowEnd,
          context: contexts.get(entry.resourceId),
        });
      }

      // Compute scenario totals
      let scenarioCostCents = 0;
      let scenarioHours = 0;

      for (const entry of scenarioEntries) {
        const totalHours = calculateEntryHours(entry);
        scenarioCostCents += Math.round(totalHours * entry.lcrCents);
        scenarioHours += totalHours;
      }

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

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

        const availability = (resource.availability as typeof DEFAULT_AVAILABILITY) ?? DEFAULT_AVAILABILITY;
        const context = contexts.get(resId);
        const totalAvailableHours = calculateEffectiveAvailableHours({
          availability,
          periodStart: windowStart,
          periodEnd: windowEnd,
          context,
        });

        // Current utilization on this project
        const currentProjectAssignments = (assignmentsByResource.get(resId) ?? []).filter(
          (a) => a.projectId === projectId,
        );
        let currentProjectHours = 0;
        for (const a of currentProjectAssignments) {
          currentProjectHours += calculateEffectiveBookedHours({
            availability,
            startDate: a.startDate,
            endDate: a.endDate,
            hoursPerDay: a.hoursPerDay,
            periodStart: windowStart,
            periodEnd: windowEnd,
            context,
          });
        }

        // Scenario hours for this resource on this project
        const scenarioResourceEntries = scenarioEntries.filter((e) => e.resourceId === resId);
        let scenarioProjectHours = 0;
        for (const e of scenarioResourceEntries) {
          scenarioProjectHours += calculateEffectiveBookedHours({
            availability,
            startDate: e.startDate,
            endDate: e.endDate,
            hoursPerDay: e.hoursPerDay,
            periodStart: windowStart,
            periodEnd: windowEnd,
            context,
          });
        }

        // Total hours across all projects (excluding this project's current, adding scenario)
        const otherProjectAssignments = (assignmentsByResource.get(resId) ?? []).filter(
          (a) => a.projectId !== projectId,
        );
        let otherProjectsHours = 0;
        for (const a of otherProjectAssignments) {
          otherProjectsHours += calculateEffectiveBookedHours({
            availability,
            startDate: a.startDate,
            endDate: a.endDate,
            hoursPerDay: a.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: resId,
          resourceName: resource.displayName,
          chargeabilityTarget: resource.chargeabilityTarget,
          currentUtilization: Math.round(currentUtilization * 10) / 10,
          scenarioUtilization: Math.round(scenarioUtilization * 10) / 10,
          utilizationDelta: Math.round((scenarioUtilization - currentUtilization) * 10) / 10,
          isOverallocated: scenarioUtilization > 100,
        };
      }).filter((x): x is NonNullable<typeof x> => x !== null);

      // Build warnings
      const warnings: string[] = [];
      for (const impact of resourceImpacts) {
        if (impact && 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}%`);
      }

      // Skill coverage: how many unique skills does the scenario team bring vs. current?
      const currentSkills = new Set<string>();
      const scenarioSkills = new Set<string>();

      for (const a of currentAssignments) {
        const skills = (a.resource?.skills ?? []) as Array<{ skill: string }>;
        for (const s of skills) currentSkills.add(s.skill.toLowerCase());
      }

      for (const entry of scenarioEntries) {
        if (!entry.resourceId) continue;
        const resource = resourceMap.get(entry.resourceId);
        const skills = (resource?.skills ?? []) as Array<{ skill: string }>;
        for (const s of skills) scenarioSkills.add(s.skill.toLowerCase());
      }

      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,
      };
    }),

  /**
   * Applies a scenario: creates real assignments from scenario changes.
   * Manager+ access required.
   */
  applyScenario: controllerProcedure
    .input(SimulateInputSchema)
    .mutation(async ({ ctx, input }) => {
      const { projectId, changes } = input;

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

      const created: string[] = [];

      for (const change of changes) {
        if (change.remove && change.assignmentId) {
          // Cancel the existing assignment
          await ctx.db.assignment.update({
            where: { id: change.assignmentId },
            data: { status: "CANCELLED" },
          });
          continue;
        }

        if (change.assignmentId) {
          // Modify existing assignment
          await ctx.db.assignment.update({
            where: { id: change.assignmentId },
            data: {
              startDate: change.startDate,
              endDate: change.endDate,
              hoursPerDay: change.hoursPerDay,
              ...(change.resourceId ? { resourceId: change.resourceId } : {}),
              ...(change.roleId ? { roleId: change.roleId } : {}),
            },
          });
          created.push(change.assignmentId);
          continue;
        }

        if (!change.resourceId) {
          // Skip entries without a resource — cannot create an assignment
          continue;
        }

        // Look up the resource LCR for dailyCostCents
        const resource = await ctx.db.resource.findUnique({
          where: { id: change.resourceId },
          select: { lcrCents: true },
        });
        const dailyCostCents = Math.round((resource?.lcrCents ?? 0) * change.hoursPerDay);

        const newAssignment = await ctx.db.assignment.create({
          data: {
            projectId,
            resourceId: change.resourceId,
            ...(change.roleId ? { roleId: change.roleId } : {}),
            startDate: change.startDate,
            endDate: change.endDate,
            hoursPerDay: change.hoursPerDay,
            percentage: 100,
            dailyCostCents,
            status: "PROPOSED",
            metadata: {},
          },
        });
        created.push(newAssignment.id);
      }

      void createAuditEntry({
        db: ctx.db,
        entityType: "ScenarioApplication",
        entityId: projectId,
        entityName: project.name,
        action: "CREATE",
        userId: ctx.dbUser?.id,
        summary: `Applied scenario to project "${project.name}" (${created.length} allocations created/modified)`,
        metadata: { appliedCount: created.length, assignmentIds: created },
        source: "ui",
      });

      return { appliedCount: created.length };
    }),
});
