import {
  applyExperienceMultipliers,
  applyExperienceMultipliersBatch,
  type ExperienceMultiplierRule as EngineRule,
} from "@capakraken/engine";
import {
  CreateExperienceMultiplierSetSchema,
  UpdateExperienceMultiplierSetSchema,
  ApplyExperienceMultipliersSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js";
import { createAuditEntry } from "../lib/audit.js";

const ruleInclude = {
  rules: { orderBy: { sortOrder: "asc" as const } },
} as const;

function toEngineRules(
  dbRules: Array<{
    chapter: string | null;
    location: string | null;
    level: string | null;
    costMultiplier: number;
    billMultiplier: number;
    shoringRatio: number | null;
    additionalEffortRatio: number | null;
    description: string | null;
  }>,
): EngineRule[] {
  return dbRules.map((r) => ({
    ...(r.chapter != null ? { chapter: r.chapter } : {}),
    ...(r.location != null ? { location: r.location } : {}),
    ...(r.level != null ? { level: r.level } : {}),
    costMultiplier: r.costMultiplier,
    billMultiplier: r.billMultiplier,
    ...(r.shoringRatio != null ? { shoringRatio: r.shoringRatio } : {}),
    ...(r.additionalEffortRatio != null ? { additionalEffortRatio: r.additionalEffortRatio } : {}),
    ...(r.description != null ? { description: r.description } : {}),
  }));
}

export const experienceMultiplierRouter = createTRPCRouter({
  list: controllerProcedure.query(async ({ ctx }) => {
    return ctx.db.experienceMultiplierSet.findMany({
      include: ruleInclude,
      orderBy: [{ isDefault: "desc" }, { name: "asc" }],
    });
  }),

  getById: controllerProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const set = await findUniqueOrThrow(
        ctx.db.experienceMultiplierSet.findUnique({
          where: { id: input.id },
          include: ruleInclude,
        }),
        "Experience multiplier set",
      );
      return set;
    }),

  create: managerProcedure
    .input(CreateExperienceMultiplierSetSchema)
    .mutation(async ({ ctx, input }) => {
      if (input.isDefault) {
        await ctx.db.experienceMultiplierSet.updateMany({
          where: { isDefault: true },
          data: { isDefault: false },
        });
      }

      const set = await ctx.db.experienceMultiplierSet.create({
        data: {
          name: input.name,
          ...(input.description ? { description: input.description } : {}),
          isDefault: input.isDefault,
          rules: {
            create: input.rules.map((r, i) => ({
              ...(r.chapter ? { chapter: r.chapter } : {}),
              ...(r.location ? { location: r.location } : {}),
              ...(r.level ? { level: r.level } : {}),
              costMultiplier: r.costMultiplier,
              billMultiplier: r.billMultiplier,
              ...(r.shoringRatio !== undefined ? { shoringRatio: r.shoringRatio } : {}),
              ...(r.additionalEffortRatio !== undefined ? { additionalEffortRatio: r.additionalEffortRatio } : {}),
              ...(r.description ? { description: r.description } : {}),
              sortOrder: r.sortOrder ?? i,
            })),
          },
        },
        include: ruleInclude,
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "ExperienceMultiplierSet",
        entityId: set.id,
        entityName: set.name,
        action: "CREATE",
        userId: ctx.dbUser?.id,
        after: { name: input.name, isDefault: input.isDefault, ruleCount: input.rules.length },
        source: "ui",
      });

      return set;
    }),

  update: managerProcedure
    .input(UpdateExperienceMultiplierSetSchema)
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.experienceMultiplierSet.findUnique({ where: { id: input.id }, include: ruleInclude }),
        "Experience multiplier set",
      );

      if (input.isDefault) {
        await ctx.db.experienceMultiplierSet.updateMany({
          where: { isDefault: true, id: { not: input.id } },
          data: { isDefault: false },
        });
      }

      if (input.rules) {
        await ctx.db.experienceMultiplierRule.deleteMany({ where: { multiplierSetId: input.id } });
        await ctx.db.experienceMultiplierRule.createMany({
          data: input.rules.map((r, i) => ({
            multiplierSetId: input.id,
            ...(r.chapter ? { chapter: r.chapter } : {}),
            ...(r.location ? { location: r.location } : {}),
            ...(r.level ? { level: r.level } : {}),
            costMultiplier: r.costMultiplier,
            billMultiplier: r.billMultiplier,
            ...(r.shoringRatio !== undefined ? { shoringRatio: r.shoringRatio } : {}),
            ...(r.additionalEffortRatio !== undefined ? { additionalEffortRatio: r.additionalEffortRatio } : {}),
            ...(r.description ? { description: r.description } : {}),
            sortOrder: r.sortOrder ?? i,
          })),
        });
      }

      const updated = await ctx.db.experienceMultiplierSet.update({
        where: { id: input.id },
        data: {
          ...(input.name !== undefined ? { name: input.name } : {}),
          ...(input.description !== undefined ? { description: input.description } : {}),
          ...(input.isDefault !== undefined ? { isDefault: input.isDefault } : {}),
        },
        include: ruleInclude,
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "ExperienceMultiplierSet",
        entityId: input.id,
        entityName: updated.name,
        action: "UPDATE",
        userId: ctx.dbUser?.id,
        before: { name: before.name, isDefault: before.isDefault, ruleCount: before.rules.length },
        after: { name: updated.name, isDefault: updated.isDefault, ruleCount: updated.rules.length },
        source: "ui",
      });

      return updated;
    }),

  delete: managerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const set = await findUniqueOrThrow(
        ctx.db.experienceMultiplierSet.findUnique({ where: { id: input.id } }),
        "Experience multiplier set",
      );
      await ctx.db.experienceMultiplierSet.delete({ where: { id: input.id } });

      void createAuditEntry({
        db: ctx.db,
        entityType: "ExperienceMultiplierSet",
        entityId: input.id,
        entityName: set.name,
        action: "DELETE",
        userId: ctx.dbUser?.id,
        source: "ui",
      });

      return { id: input.id };
    }),

  /** Preview the rate adjustment without persisting */
  preview: controllerProcedure
    .input(z.object({
      estimateId: z.string(),
      multiplierSetId: z.string(),
    }))
    .query(async ({ ctx, input }) => {
      const [estimate, multiplierSet] = await Promise.all([
        findUniqueOrThrow(
          ctx.db.estimate.findUnique({
            where: { id: input.estimateId },
            include: {
              versions: {
                orderBy: { versionNumber: "desc" },
                take: 1,
                include: { demandLines: { orderBy: { createdAt: "asc" } } },
              },
            },
          }),
          "Estimate",
        ),
        findUniqueOrThrow(
          ctx.db.experienceMultiplierSet.findUnique({
            where: { id: input.multiplierSetId },
            include: ruleInclude,
          }),
          "Experience multiplier set",
        ),
      ]);

      const version = estimate.versions[0];
      if (!version) throw new TRPCError({ code: "NOT_FOUND", message: "Estimate has no versions" });

      const engineRules = toEngineRules(multiplierSet.rules);
      const demandLines = version.demandLines;

      const previews = demandLines.map((line) => {
        const result = applyExperienceMultipliers(
          {
            costRateCents: line.costRateCents,
            billRateCents: line.billRateCents,
            hours: line.hours,
            ...(line.chapter != null ? { chapter: line.chapter } : {}),
            ...(line.metadata != null && typeof line.metadata === "object" && "location" in (line.metadata as Record<string, unknown>)
              ? { location: (line.metadata as Record<string, unknown>).location as string }
              : {}),
            ...(line.staffingAttributes != null && typeof line.staffingAttributes === "object" && "level" in (line.staffingAttributes as Record<string, unknown>)
              ? { level: (line.staffingAttributes as Record<string, unknown>).level as string }
              : {}),
          },
          engineRules,
        );

        return {
          demandLineId: line.id,
          name: line.name,
          chapter: line.chapter,
          originalCostRateCents: line.costRateCents,
          originalBillRateCents: line.billRateCents,
          originalHours: line.hours,
          adjustedCostRateCents: result.adjustedCostRateCents,
          adjustedBillRateCents: result.adjustedBillRateCents,
          adjustedHours: result.adjustedHours,
          appliedRules: result.appliedRules,
          hasChanges:
            result.adjustedCostRateCents !== line.costRateCents ||
            result.adjustedBillRateCents !== line.billRateCents ||
            result.adjustedHours !== line.hours,
        };
      });

      const linesChanged = previews.filter((p) => p.hasChanges).length;
      const totalOriginalCostCents = demandLines.reduce((s, l) => s + l.costRateCents * l.hours, 0);
      const totalAdjustedCostCents = previews.reduce((s, p) => s + p.adjustedCostRateCents * p.adjustedHours, 0);

      return {
        previews,
        demandLineCount: demandLines.length,
        linesChanged,
        totalOriginalCostCents: Math.round(totalOriginalCostCents),
        totalAdjustedCostCents: Math.round(totalAdjustedCostCents),
        multiplierSetName: multiplierSet.name,
        ruleCount: multiplierSet.rules.length,
      };
    }),

  /** Apply multipliers to demand lines on the working version */
  applyRules: managerProcedure
    .input(ApplyExperienceMultipliersSchema)
    .mutation(async ({ ctx, input }) => {
      const [estimate, multiplierSet] = await Promise.all([
        findUniqueOrThrow(
          ctx.db.estimate.findUnique({
            where: { id: input.estimateId },
            include: {
              versions: {
                orderBy: { versionNumber: "desc" },
                take: 1,
                include: { demandLines: true },
              },
            },
          }),
          "Estimate",
        ),
        findUniqueOrThrow(
          ctx.db.experienceMultiplierSet.findUnique({
            where: { id: input.multiplierSetId },
            include: ruleInclude,
          }),
          "Experience multiplier set",
        ),
      ]);

      const version = estimate.versions[0];
      if (!version) throw new TRPCError({ code: "NOT_FOUND", message: "Estimate has no versions" });
      if (version.status !== "WORKING") {
        throw new TRPCError({ code: "BAD_REQUEST", message: "Can only apply multipliers to a WORKING version" });
      }

      const engineRules = toEngineRules(multiplierSet.rules);
      const demandLines = version.demandLines;

      const inputs = demandLines.map((line) => ({
        costRateCents: line.costRateCents,
        billRateCents: line.billRateCents,
        hours: line.hours,
        ...(line.chapter != null ? { chapter: line.chapter } : {}),
        ...(line.metadata != null && typeof line.metadata === "object" && "location" in (line.metadata as Record<string, unknown>)
          ? { location: (line.metadata as Record<string, unknown>).location as string }
          : {}),
        ...(line.staffingAttributes != null && typeof line.staffingAttributes === "object" && "level" in (line.staffingAttributes as Record<string, unknown>)
          ? { level: (line.staffingAttributes as Record<string, unknown>).level as string }
          : {}),
      }));

      const batch = applyExperienceMultipliersBatch(inputs, engineRules);

      // Update each demand line that changed
      let updatedCount = 0;
      for (let i = 0; i < demandLines.length; i++) {
        const line = demandLines[i]!;
        const result = batch.results[i]!;

        if (
          result.adjustedCostRateCents !== line.costRateCents ||
          result.adjustedBillRateCents !== line.billRateCents ||
          result.adjustedHours !== line.hours
        ) {
          const newCostTotal = Math.round(result.adjustedCostRateCents * result.adjustedHours);
          const newPriceTotal = Math.round(result.adjustedBillRateCents * result.adjustedHours);

          await ctx.db.estimateDemandLine.update({
            where: { id: line.id },
            data: {
              costRateCents: result.adjustedCostRateCents,
              billRateCents: result.adjustedBillRateCents,
              hours: result.adjustedHours,
              costTotalCents: newCostTotal,
              priceTotalCents: newPriceTotal,
              metadata: {
                ...(typeof line.metadata === "object" && line.metadata !== null ? line.metadata as Record<string, unknown> : {}),
                experienceMultiplier: {
                  setId: multiplierSet.id,
                  setName: multiplierSet.name,
                  appliedRules: result.appliedRules,
                  originalCostRateCents: line.costRateCents,
                  originalBillRateCents: line.billRateCents,
                  originalHours: line.hours,
                },
              },
            },
          });
          updatedCount++;
        }
      }

      // Audit log
      await ctx.db.auditLog.create({
        data: {
          entityType: "Estimate",
          entityId: estimate.id,
          action: "UPDATE",
          ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
          changes: {
            after: {
              experienceMultipliersApplied: {
                setId: multiplierSet.id,
                setName: multiplierSet.name,
                linesUpdated: updatedCount,
                totalOriginalHours: batch.totalOriginalHours,
                totalAdjustedHours: batch.totalAdjustedHours,
              },
            },
          },
        },
      });

      return {
        linesUpdated: updatedCount,
        totalOriginalHours: batch.totalOriginalHours,
        totalAdjustedHours: batch.totalAdjustedHours,
      };
    }),
});
