import {
  applyExperienceMultipliers,
  applyExperienceMultipliersBatch,
} from "@capakraken/engine";
import {
  ApplyExperienceMultipliersSchema,
  CreateExperienceMultiplierSetSchema,
  UpdateExperienceMultiplierSetSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js";
import type { TRPCContext } from "../trpc.js";
import {
  buildExperienceMultiplierCreateManyRows,
  buildExperienceMultiplierDemandLineUpdateData,
  buildExperienceMultiplierInput,
  buildExperienceMultiplierSetCreateData,
  buildExperienceMultiplierSetUpdateData,
  experienceMultiplierRuleInclude,
  hasExperienceMultiplierChanges,
  toExperienceMultiplierEngineRules,
} from "./experience-multiplier-support.js";

type ExperienceMultiplierProcedureContext = Pick<TRPCContext, "db" | "dbUser">;

function withAuditUser(userId: string | undefined) {
  return userId ? { userId } : {};
}

export const experienceMultiplierIdInputSchema = z.object({ id: z.string() });

export const experienceMultiplierPreviewInputSchema = z.object({
  estimateId: z.string(),
  multiplierSetId: z.string(),
});

type ExperienceMultiplierIdInput = z.infer<typeof experienceMultiplierIdInputSchema>;
type ExperienceMultiplierCreateInput = z.infer<typeof CreateExperienceMultiplierSetSchema>;
type ExperienceMultiplierUpdateInput = z.infer<typeof UpdateExperienceMultiplierSetSchema>;
type ExperienceMultiplierPreviewInput = z.infer<typeof experienceMultiplierPreviewInputSchema>;
type ExperienceMultiplierApplyInput = z.infer<typeof ApplyExperienceMultipliersSchema>;

async function getExperienceMultiplierSetOrThrow(
  ctx: ExperienceMultiplierProcedureContext,
  id: string,
) {
  return findUniqueOrThrow(
    ctx.db.experienceMultiplierSet.findUnique({
      where: { id },
      include: experienceMultiplierRuleInclude,
    }),
    "Experience multiplier set",
  );
}

async function getEstimateWithLatestVersionOrThrow(
  ctx: ExperienceMultiplierProcedureContext,
  estimateId: string,
  orderedDemandLines: boolean,
) {
  return findUniqueOrThrow(
    ctx.db.estimate.findUnique({
      where: { id: estimateId },
      include: {
        versions: {
          orderBy: { versionNumber: "desc" },
          take: 1,
          include: {
            demandLines: orderedDemandLines
              ? { orderBy: { createdAt: "asc" } }
              : true,
          },
        },
      },
    }),
    "Estimate",
  );
}

export async function listExperienceMultiplierSets(
  ctx: ExperienceMultiplierProcedureContext,
) {
  return ctx.db.experienceMultiplierSet.findMany({
    include: experienceMultiplierRuleInclude,
    orderBy: [{ isDefault: "desc" }, { name: "asc" }],
  });
}

export async function getExperienceMultiplierSetById(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierIdInput,
) {
  return getExperienceMultiplierSetOrThrow(ctx, input.id);
}

export async function createExperienceMultiplierSet(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierCreateInput,
) {
  if (input.isDefault) {
    await ctx.db.experienceMultiplierSet.updateMany({
      where: { isDefault: true },
      data: { isDefault: false },
    });
  }

  const set = await ctx.db.experienceMultiplierSet.create({
    data: buildExperienceMultiplierSetCreateData(input),
    include: experienceMultiplierRuleInclude,
  });

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

  return set;
}

export async function updateExperienceMultiplierSet(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierUpdateInput,
) {
  const before = await getExperienceMultiplierSetOrThrow(ctx, input.id);

  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: buildExperienceMultiplierCreateManyRows(input.rules, input.id),
    });
  }

  const updated = await ctx.db.experienceMultiplierSet.update({
    where: { id: input.id },
    data: buildExperienceMultiplierSetUpdateData(input),
    include: experienceMultiplierRuleInclude,
  });

  void createAuditEntry({
    db: ctx.db,
    entityType: "ExperienceMultiplierSet",
    entityId: input.id,
    entityName: updated.name,
    action: "UPDATE",
    ...withAuditUser(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;
}

export async function deleteExperienceMultiplierSet(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierIdInput,
) {
  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",
    ...withAuditUser(ctx.dbUser?.id),
    source: "ui",
  });

  return { id: input.id };
}

export async function previewExperienceMultipliers(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierPreviewInput,
) {
  const [estimate, multiplierSet] = await Promise.all([
    getEstimateWithLatestVersionOrThrow(ctx, input.estimateId, true),
    getExperienceMultiplierSetOrThrow(ctx, input.multiplierSetId),
  ]);

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

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

  const previews = demandLines.map((line) => {
    const result = applyExperienceMultipliers(
      buildExperienceMultiplierInput(line),
      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: hasExperienceMultiplierChanges(line, result),
    };
  });

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

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

export async function applyExperienceMultiplierRules(
  ctx: ExperienceMultiplierProcedureContext,
  input: ExperienceMultiplierApplyInput,
) {
  const [estimate, multiplierSet] = await Promise.all([
    getEstimateWithLatestVersionOrThrow(ctx, input.estimateId, false),
    getExperienceMultiplierSetOrThrow(ctx, input.multiplierSetId),
  ]);

  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 = toExperienceMultiplierEngineRules(multiplierSet.rules);
  const demandLines = version.demandLines;
  const inputs = demandLines.map((line) => buildExperienceMultiplierInput(line));
  const batch = applyExperienceMultipliersBatch(inputs, engineRules);

  let updatedCount = 0;
  for (let i = 0; i < demandLines.length; i++) {
    const line = demandLines[i]!;
    const result = batch.results[i]!;

    if (hasExperienceMultiplierChanges(line, result)) {
      await ctx.db.estimateDemandLine.update({
        where: { id: line.id },
        data: buildExperienceMultiplierDemandLineUpdateData({
          line,
          result,
          multiplierSet,
        }),
      });
      updatedCount++;
    }
  }

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