import {
  expandScopeToEffort,
  aggregateByDiscipline,
} from "@capakraken/engine";
import {
  CreateEffortRuleSetSchema,
  UpdateEffortRuleSetSchema,
  ApplyEffortRulesSchema,
} 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";
import {
  buildEstimateDemandLineRows,
  buildEffortRuleCreateManyRows,
  buildEffortRuleSetCreateData,
  buildEffortRuleSetUpdateData,
  effortRuleInclude,
  toEffortRuleEngineInputs,
  toScopeItemInputs,
} from "./effort-rule-support.js";

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

  getById: controllerProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const ruleSet = await findUniqueOrThrow(
        ctx.db.effortRuleSet.findUnique({
          where: { id: input.id },
          include: effortRuleInclude,
        }),
        "Effort rule set",
      );
      return ruleSet;
    }),

  create: managerProcedure
    .input(CreateEffortRuleSetSchema)
    .mutation(async ({ ctx, input }) => {
      // If this is set as default, unset other defaults
      if (input.isDefault) {
        await ctx.db.effortRuleSet.updateMany({
          where: { isDefault: true },
          data: { isDefault: false },
        });
      }

      const ruleSet = await ctx.db.effortRuleSet.create({
        data: buildEffortRuleSetCreateData(input),
        include: effortRuleInclude,
      });

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

      return ruleSet;
    }),

  update: managerProcedure
    .input(UpdateEffortRuleSetSchema)
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.effortRuleSet.findUnique({ where: { id: input.id }, include: effortRuleInclude }),
        "Effort rule set",
      );

      // If setting as default, unset others
      if (input.isDefault) {
        await ctx.db.effortRuleSet.updateMany({
          where: { isDefault: true, id: { not: input.id } },
          data: { isDefault: false },
        });
      }

      // If rules are provided, replace all existing rules
      if (input.rules) {
        await ctx.db.effortRule.deleteMany({ where: { ruleSetId: input.id } });
        await ctx.db.effortRule.createMany({
          data: buildEffortRuleCreateManyRows(input.rules, input.id),
        });
      }

      const updated = await ctx.db.effortRuleSet.update({
        where: { id: input.id },
        data: buildEffortRuleSetUpdateData(input),
        include: effortRuleInclude,
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "EffortRuleSet",
        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 ruleSet = await findUniqueOrThrow(
        ctx.db.effortRuleSet.findUnique({ where: { id: input.id } }),
        "Effort rule set",
      );
      await ctx.db.effortRuleSet.delete({ where: { id: input.id } });

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

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

  /** Preview the expansion result without persisting */
  preview: controllerProcedure
    .input(z.object({
      estimateId: z.string(),
      ruleSetId: z.string(),
    }))
    .query(async ({ ctx, input }) => {
      const [estimate, ruleSet] = await Promise.all([
        findUniqueOrThrow(
          ctx.db.estimate.findUnique({
            where: { id: input.estimateId },
            include: {
              versions: {
                orderBy: { versionNumber: "desc" },
                take: 1,
                include: { scopeItems: { orderBy: { sortOrder: "asc" } } },
              },
            },
          }),
          "Estimate",
        ),
        findUniqueOrThrow(
        ctx.db.effortRuleSet.findUnique({
          where: { id: input.ruleSetId },
          include: effortRuleInclude,
        }),
        "Effort rule set",
      ),
      ]);

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

      const scopeItems = toScopeItemInputs(version.scopeItems);
      const rules = toEffortRuleEngineInputs(ruleSet.rules);

      const result = expandScopeToEffort(scopeItems, rules);
      const aggregated = aggregateByDiscipline(result.lines);

      return {
        ...result,
        aggregated,
        scopeItemCount: scopeItems.length,
        ruleCount: rules.length,
      };
    }),

  /** Apply effort rules to generate demand lines on the working version */
  applyRules: managerProcedure
    .input(ApplyEffortRulesSchema)
    .mutation(async ({ ctx, input }) => {
      const [estimate, ruleSet] = await Promise.all([
        findUniqueOrThrow(
          ctx.db.estimate.findUnique({
            where: { id: input.estimateId },
            include: {
              versions: {
                orderBy: { versionNumber: "desc" },
                take: 1,
                include: {
                  scopeItems: { orderBy: { sortOrder: "asc" } },
                  demandLines: true,
                },
              },
            },
          }),
          "Estimate",
        ),
        findUniqueOrThrow(
        ctx.db.effortRuleSet.findUnique({
          where: { id: input.ruleSetId },
          include: effortRuleInclude,
        }),
        "Effort rule 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 rules to a WORKING version" });
      }

      const scopeItems = toScopeItemInputs(version.scopeItems);
      const rules = toEffortRuleEngineInputs(ruleSet.rules);

      const result = expandScopeToEffort(scopeItems, rules);

      // In replace mode, delete existing demand lines first
      if (input.mode === "replace") {
        await ctx.db.estimateDemandLine.deleteMany({
          where: { estimateVersionId: version.id },
        });
      }

      // Create demand lines from expanded results
      if (result.lines.length > 0) {
        await ctx.db.estimateDemandLine.createMany({
          data: buildEstimateDemandLineRows({
            estimateVersionId: version.id,
            currency: estimate.baseCurrency,
            ruleSet: { id: ruleSet.id, name: ruleSet.name },
            lines: result.lines,
          }),
        });
      }

      // Log audit
      await ctx.db.auditLog.create({
        data: {
          entityType: "Estimate",
          entityId: estimate.id,
          action: "UPDATE",
          ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
          changes: {
            after: {
              effortRulesApplied: {
                ruleSetId: ruleSet.id,
                ruleSetName: ruleSet.name,
                mode: input.mode,
                linesGenerated: result.lines.length,
                warnings: result.warnings,
              },
            },
          },
        },
      });

      return {
        linesGenerated: result.lines.length,
        warnings: result.warnings,
        unmatchedScopeItems: result.unmatchedScopeItems,
      };
    }),
});
