import {
  cloneEstimate,
  createEstimate,
  createEstimateExport,
  createEstimatePlanningHandoff,
  updateEstimateDraft,
} from "@capakraken/application";
import type { Prisma } from "@capakraken/db";
import {
  CloneEstimateSchema,
  CreateEstimateExportSchema,
  CreateEstimatePlanningHandoffSchema,
  CreateEstimateSchema,
  UpdateEstimateDraftSchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { lookupRate } from "../lib/rate-card-lookup.js";
import { emitAllocationCreated } from "../sse/event-bus.js";
import type { TRPCContext } from "../trpc.js";
import {
  autoFillDemandLineRates,
  withComputedMetrics,
} from "./estimate-demand-lines.js";

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

type EstimateRouterErrorCode = "NOT_FOUND" | "PRECONDITION_FAILED";

type EstimateRouterErrorRule = {
  code: EstimateRouterErrorCode;
  messages?: readonly string[];
  predicates?: readonly ((message: string) => boolean)[];
};

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

export function rethrowEstimateRouterError(
  error: unknown,
  rules: readonly EstimateRouterErrorRule[],
): never {
  if (!(error instanceof Error)) {
    throw error;
  }

  const matchingRule = rules.find(
    (rule) =>
      rule.messages?.includes(error.message) === true ||
      rule.predicates?.some((predicate) => predicate(error.message)) === true,
  );

  if (matchingRule) {
    throw new TRPCError({
      code: matchingRule.code,
      message: error.message,
    });
  }

  throw error;
}

export const lookupDemandLineRateInputSchema = z.object({
  projectId: z.string().optional(),
  clientId: z.string().optional(),
  roleId: z.string().optional(),
  chapter: z.string().optional(),
  seniority: z.string().optional(),
  location: z.string().optional(),
  workType: z.string().optional(),
  effectiveDate: z.coerce.date().optional(),
});

type CreateEstimateInput = z.infer<typeof CreateEstimateSchema>;
type CloneEstimateInput = z.infer<typeof CloneEstimateSchema>;
type UpdateEstimateDraftInput = z.infer<typeof UpdateEstimateDraftSchema>;
type CreateEstimateExportInput = z.infer<typeof CreateEstimateExportSchema>;
type CreateEstimatePlanningHandoffInput = z.infer<
  typeof CreateEstimatePlanningHandoffSchema
>;
type LookupDemandLineRateInput = z.infer<typeof lookupDemandLineRateInputSchema>;

export async function createEstimateRecord(
  ctx: EstimateProcedureContext,
  input: CreateEstimateInput,
) {
  if (input.projectId) {
    await findUniqueOrThrow(
      ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: { id: true },
      }),
      "Project",
    );
  }

  const { demandLines: enrichedLines, autoFilledIndices } =
    await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
  const enrichedInput = { ...input, demandLines: enrichedLines };

  const estimate = await createEstimate(
    ctx.db as unknown as Parameters<typeof createEstimate>[0],
    withComputedMetrics(enrichedInput, input.baseCurrency),
  );

  await ctx.db.auditLog.create({
    data: {
      entityType: "Estimate",
      entityId: estimate.id,
      action: "CREATE",
      ...withAuditUser(ctx.dbUser?.id),
      changes: {
        after: {
          id: estimate.id,
          name: estimate.name,
          status: estimate.status,
          projectId: estimate.projectId,
          latestVersionNumber: estimate.latestVersionNumber,
          autoFilledRateCardLines: autoFilledIndices.length,
        },
      } as Prisma.InputJsonValue,
    },
  });

  return estimate;
}

export async function cloneEstimateRecord(
  ctx: EstimateProcedureContext,
  input: CloneEstimateInput,
) {
  let estimate;
  try {
    estimate = await cloneEstimate(
      ctx.db as unknown as Parameters<typeof cloneEstimate>[0],
      input,
    );
  } catch (error) {
    rethrowEstimateRouterError(error, [
      {
        code: "NOT_FOUND",
        messages: ["Source estimate not found", "Source estimate has no versions"],
      },
    ]);
  }

  await ctx.db.auditLog.create({
    data: {
      entityType: "Estimate",
      entityId: estimate.id,
      action: "CREATE",
      ...withAuditUser(ctx.dbUser?.id),
      changes: {
        after: {
          id: estimate.id,
          name: estimate.name,
          clonedFrom: input.sourceEstimateId,
        },
      } as Prisma.InputJsonValue,
    },
  });

  return estimate;
}

export async function updateEstimateDraftRecord(
  ctx: EstimateProcedureContext,
  input: UpdateEstimateDraftInput,
) {
  if (input.projectId) {
    await findUniqueOrThrow(
      ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: { id: true },
      }),
      "Project",
    );
  }

  let effectiveProjectId = input.projectId;
  if (!effectiveProjectId) {
    const existing = await ctx.db.estimate.findUnique({
      where: { id: input.id },
      select: { projectId: true },
    });
    effectiveProjectId = existing?.projectId ?? undefined;
  }

  const { demandLines: enrichedLines, autoFilledIndices } =
    await autoFillDemandLineRates(ctx.db, input.demandLines, effectiveProjectId);
  const enrichedInput = { ...input, demandLines: enrichedLines };

  let estimate;
  try {
    estimate = await updateEstimateDraft(
      ctx.db as unknown as Parameters<typeof updateEstimateDraft>[0],
      withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
    );
  } catch (error) {
    rethrowEstimateRouterError(error, [
      {
        code: "NOT_FOUND",
        messages: ["Estimate not found"],
      },
      {
        code: "PRECONDITION_FAILED",
        messages: ["Estimate has no working version"],
      },
    ]);
  }

  await ctx.db.auditLog.create({
    data: {
      entityType: "Estimate",
      entityId: estimate.id,
      action: "UPDATE",
      ...withAuditUser(ctx.dbUser?.id),
      changes: {
        after: {
          id: estimate.id,
          name: estimate.name,
          status: estimate.status,
          latestVersionNumber: estimate.latestVersionNumber,
          workingVersionId: estimate.versions.find(
            (version) => version.status === "WORKING",
          )?.id,
          autoFilledRateCardLines: autoFilledIndices.length,
        },
      } as Prisma.InputJsonValue,
    },
  });

  return estimate;
}

export async function createEstimateExportRecord(
  ctx: EstimateProcedureContext,
  input: CreateEstimateExportInput,
) {
  let estimate;
  try {
    estimate = await createEstimateExport(
      ctx.db as unknown as Parameters<typeof createEstimateExport>[0],
      input,
    );
  } catch (error) {
    rethrowEstimateRouterError(error, [
      {
        code: "NOT_FOUND",
        messages: [
          "Estimate not found",
          "Estimate version not found",
          "Estimate has no version to export",
        ],
      },
    ]);
  }

  const exportedVersion = input.versionId
    ? estimate.versions.find((version) => version.id === input.versionId)
    : estimate.versions[0];

  await ctx.db.auditLog.create({
    data: {
      entityType: "Estimate",
      entityId: estimate.id,
      action: "UPDATE",
      ...withAuditUser(ctx.dbUser?.id),
      changes: {
        after: {
          id: estimate.id,
          exportFormat: input.format,
          exportCount: exportedVersion?.exports.length ?? null,
          versionId: exportedVersion?.id ?? null,
        },
      } as Prisma.InputJsonValue,
    },
  });

  return estimate;
}

export async function createEstimatePlanningHandoffRecord(
  ctx: EstimateProcedureContext,
  input: CreateEstimatePlanningHandoffInput,
) {
  let result;
  try {
    result = await createEstimatePlanningHandoff(
      ctx.db as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
      input,
    );
  } catch (error) {
    rethrowEstimateRouterError(error, [
      {
        code: "NOT_FOUND",
        messages: [
          "Estimate not found",
          "Estimate version not found",
          "Linked project not found",
        ],
      },
      {
        code: "PRECONDITION_FAILED",
        messages: [
          "Estimate has no approved version",
          "Only approved versions can be handed off to planning",
          "Estimate must be linked to a project before planning handoff",
          "Planning handoff already exists for this approved version",
          "Linked project has an invalid date range",
        ],
        predicates: [
          (message) =>
            message.startsWith("Project window has no working days for demand line"),
        ],
      },
    ]);
  }

  await ctx.db.auditLog.create({
    data: {
      entityType: "Estimate",
      entityId: result.estimateId,
      action: "UPDATE",
      ...withAuditUser(ctx.dbUser?.id),
      changes: {
        after: {
          planningHandoff: {
            versionId: result.estimateVersionId,
            versionNumber: result.estimateVersionNumber,
            projectId: result.projectId,
            createdCount: result.createdCount,
            assignedCount: result.assignedCount,
            placeholderCount: result.placeholderCount,
            fallbackPlaceholderCount: result.fallbackPlaceholderCount,
          },
        },
      } as Prisma.InputJsonValue,
    },
  });

  for (const allocation of result.allocations) {
    emitAllocationCreated({
      id: allocation.id,
      projectId: allocation.projectId,
      resourceId: allocation.resourceId ?? null,
    });
  }

  return result;
}

export async function lookupDemandLineRateForEstimate(
  ctx: EstimateProcedureContext,
  input: LookupDemandLineRateInput,
) {
  let clientId = input.clientId ?? null;
  if (!clientId && input.projectId) {
    const project = await ctx.db.project.findUnique({
      where: { id: input.projectId },
      select: { clientId: true },
    });
    clientId = project?.clientId ?? null;
  }

  const result = await lookupRate(ctx.db, {
    clientId,
    chapter: input.chapter ?? null,
    roleId: input.roleId ?? null,
    seniority: input.seniority ?? null,
    location: input.location ?? null,
    workType: input.workType ?? null,
    effectiveDate: input.effectiveDate ?? null,
  });

  if (!result) {
    return { found: false as const };
  }

  return {
    found: true as const,
    costRateCents: result.costRateCents,
    billRateCents: result.billRateCents,
    currency: result.currency,
    rateCardId: result.rateCardId,
    rateCardLineId: result.rateCardLineId,
    rateCardName: result.rateCardName,
  };
}
