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

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

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

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

export const effortRulePreviewInputSchema = z.object({
  estimateId: z.string(),
  ruleSetId: z.string(),
});

type EffortRuleIdInput = z.infer<typeof effortRuleIdInputSchema>;
type EffortRuleCreateInput = z.infer<typeof CreateEffortRuleSetSchema>;
type EffortRuleUpdateInput = z.infer<typeof UpdateEffortRuleSetSchema>;
type EffortRulePreviewInput = z.infer<typeof effortRulePreviewInputSchema>;
type EffortRuleApplyInput = z.infer<typeof ApplyEffortRulesSchema>;

async function getEffortRuleSetOrThrow(
  ctx: EffortRuleProcedureContext,
  id: string,
) {
  return findUniqueOrThrow(
    ctx.db.effortRuleSet.findUnique({
      where: { id },
      include: effortRuleInclude,
    }),
    "Effort rule set",
  );
}

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

export async function listEffortRuleSets(
  ctx: EffortRuleProcedureContext,
) {
  return ctx.db.effortRuleSet.findMany({
    include: effortRuleInclude,
    orderBy: [{ isDefault: "desc" }, { name: "asc" }],
  });
}

export async function getEffortRuleSetById(
  ctx: EffortRuleProcedureContext,
  input: EffortRuleIdInput,
) {
  return getEffortRuleSetOrThrow(ctx, input.id);
}

export async function createEffortRuleSet(
  ctx: EffortRuleProcedureContext,
  input: EffortRuleCreateInput,
) {
  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",
    ...withAuditUser(ctx.dbUser?.id),
    after: { name: input.name, isDefault: input.isDefault, ruleCount: input.rules.length },
    source: "ui",
  });

  return ruleSet;
}

export async function updateEffortRuleSet(
  ctx: EffortRuleProcedureContext,
  input: EffortRuleUpdateInput,
) {
  const before = await getEffortRuleSetOrThrow(ctx, input.id);

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

  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",
    ...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 deleteEffortRuleSet(
  ctx: EffortRuleProcedureContext,
  input: EffortRuleIdInput,
) {
  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",
    ...withAuditUser(ctx.dbUser?.id),
    source: "ui",
  });

  return { id: input.id };
}

export async function previewEffortRules(
  ctx: EffortRuleProcedureContext,
  input: EffortRulePreviewInput,
) {
  const [estimate, ruleSet] = await Promise.all([
    getEstimateWithLatestVersionOrThrow(ctx, input.estimateId, false),
    getEffortRuleSetOrThrow(ctx, input.ruleSetId),
  ]);

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

export async function applyEffortRules(
  ctx: EffortRuleProcedureContext,
  input: EffortRuleApplyInput,
) {
  const [estimate, ruleSet] = await Promise.all([
    getEstimateWithLatestVersionOrThrow(ctx, input.estimateId, true),
    getEffortRuleSetOrThrow(ctx, input.ruleSetId),
  ]);

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

  if (input.mode === "replace") {
    await ctx.db.estimateDemandLine.deleteMany({
      where: { estimateVersionId: version.id },
    });
  }

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

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