import {
  expandScopeToEffort,
  aggregateByDiscipline,
  type EffortRuleInput,
  type ScopeItemInput,
} 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";

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

export const effortRuleRouter = createTRPCRouter({
  list: controllerProcedure.query(async ({ ctx }) => {
    return ctx.db.effortRuleSet.findMany({
      include: ruleInclude,
      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: ruleInclude,
        }),
        "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: {
          name: input.name,
          ...(input.description ? { description: input.description } : {}),
          isDefault: input.isDefault,
          rules: {
            create: input.rules.map((r, i) => ({
              scopeType: r.scopeType,
              discipline: r.discipline,
              ...(r.chapter ? { chapter: r.chapter } : {}),
              unitMode: r.unitMode,
              hoursPerUnit: r.hoursPerUnit,
              ...(r.description ? { description: r.description } : {}),
              sortOrder: r.sortOrder ?? i,
            })),
          },
        },
        include: ruleInclude,
      });

      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: ruleInclude }),
        "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: input.rules.map((r, i) => ({
            ruleSetId: input.id,
            scopeType: r.scopeType,
            discipline: r.discipline,
            ...(r.chapter ? { chapter: r.chapter } : {}),
            unitMode: r.unitMode,
            hoursPerUnit: r.hoursPerUnit,
            ...(r.description ? { description: r.description } : {}),
            sortOrder: r.sortOrder ?? i,
          })),
        });
      }

      const updated = await ctx.db.effortRuleSet.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: "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: ruleInclude,
          }),
          "Effort rule set",
        ),
      ]);

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

      const scopeItems: ScopeItemInput[] = version.scopeItems.map((s) => ({
        name: s.name,
        scopeType: s.scopeType,
        frameCount: s.frameCount,
        itemCount: s.itemCount,
        unitMode: s.unitMode,
      }));

      const rules: EffortRuleInput[] = ruleSet.rules.map((r) => ({
        scopeType: r.scopeType,
        discipline: r.discipline,
        chapter: r.chapter,
        unitMode: r.unitMode as "per_frame" | "per_item" | "flat",
        hoursPerUnit: r.hoursPerUnit,
        sortOrder: r.sortOrder,
      }));

      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: ruleInclude,
          }),
          "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: ScopeItemInput[] = version.scopeItems.map((s) => ({
        name: s.name,
        scopeType: s.scopeType,
        frameCount: s.frameCount,
        itemCount: s.itemCount,
        unitMode: s.unitMode,
      }));

      const rules: EffortRuleInput[] = ruleSet.rules.map((r) => ({
        scopeType: r.scopeType,
        discipline: r.discipline,
        chapter: r.chapter,
        unitMode: r.unitMode as "per_frame" | "per_item" | "flat",
        hoursPerUnit: r.hoursPerUnit,
        sortOrder: r.sortOrder,
      }));

      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: result.lines.map((line) => ({
            estimateVersionId: version.id,
            lineType: "LABOR",
            name: `${line.discipline} — ${line.scopeItemName}`,
            ...(line.chapter ? { chapter: line.chapter } : {}),
            hours: line.hours,
            costRateCents: 0,
            billRateCents: 0,
            currency: estimate.baseCurrency,
            costTotalCents: 0,
            priceTotalCents: 0,
            monthlySpread: {},
            staffingAttributes: {},
            metadata: {
              effortRule: {
                ruleSetId: ruleSet.id,
                ruleSetName: ruleSet.name,
                discipline: line.discipline,
                unitMode: line.unitMode,
                unitCount: line.unitCount,
                hoursPerUnit: line.hoursPerUnit,
              },
            },
          })),
        });
      }

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