import { Prisma } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { requireSession } from "../../lib/auth.js";
import { writeAuditLog } from "../../lib/audit.js";
import { createId } from "../../lib/id.js";
import { ensureWorkflowInstanceForCase } from "../../lib/intelligence.js";
import { prisma } from "../../lib/prisma.js";
import { getSessionProfile } from "../../lib/session.js";

type CaseRecordRow = Awaited<ReturnType<typeof prisma.caseRecord.findFirstOrThrow>>;

function formatClientName(client: {
  first_name: string;
  middle_name?: string | null;
  last_name: string;
}) {
  return [client.first_name, client.middle_name, client.last_name]
    .map((value) => value?.trim())
    .filter(Boolean)
    .join(" ");
}

function serializeDecimalValue(
  value: Prisma.Decimal | number | string | null | undefined,
) {
  if (value === null || value === undefined || value === "") {
    return null;
  }

  if (value instanceof Prisma.Decimal) {
    return value.toString();
  }

  return String(value);
}

function serializeDateOnlyValue(value: Date | string | null | undefined) {
  if (!value) {
    return null;
  }

  if (typeof value === "string") {
    return value.slice(0, 10);
  }

  return value.toISOString().slice(0, 10);
}

function serializeCaseSummary(caseRecord: CaseRecordRow, clientName: string) {
  return {
    id: caseRecord.id,
    caseNumber: caseRecord.case_number,
    title: caseRecord.title,
    caseTypeCode: caseRecord.case_type_code,
    caseSubtypeCode: caseRecord.case_subtype_code,
    workflowTemplateId: caseRecord.selected_workflow_template_id,
    statusCode: caseRecord.status_code,
    priorityCode: caseRecord.priority_code,
    clientId: caseRecord.client_id,
    clientName,
    billingPrincipalAmount: serializeDecimalValue(caseRecord.billing_principal_amount),
    billingDownPaymentAmount: serializeDecimalValue(caseRecord.billing_down_payment_amount),
    billingInstallmentCount: caseRecord.billing_installment_count,
    billingInstallmentDueDate: serializeDateOnlyValue(caseRecord.billing_installment_due_date),
    billingLateFeeAmount: serializeDecimalValue(caseRecord.billing_late_fee_amount),
    billingMonthlyInterestAmount: serializeDecimalValue(
      caseRecord.billing_monthly_interest_amount,
    ),
    openedAt: caseRecord.opened_at,
    archivedAt: caseRecord.archived_at,
  };
}

const moneyPattern = /^\d+(?:[.,]\d{1,2})?$/;
const dateOnlyPattern = /^\d{4}-\d{2}-\d{2}$/;

const moneySchema = z
  .string()
  .trim()
  .min(1)
  .refine((value) => moneyPattern.test(value), "Inform a valid amount with up to 2 decimals.");

const caseMutationSchema = z
  .object({
    clientId: z.string().uuid(),
    workflowTemplateId: z.string().uuid(),
    title: z.string().trim().max(255).optional().or(z.literal("")),
    caseTypeCode: z.string().trim().max(50).optional().or(z.literal("")),
    caseSubtypeCode: z.string().trim().max(50).optional().or(z.literal("")),
    statusCode: z.string().trim().min(2).max(50).default("intake"),
    priorityCode: z.string().trim().min(2).max(30).default("normal"),
    billingPrincipalAmount: moneySchema,
    billingDownPaymentAmount: moneySchema,
    billingInstallmentCount: z.coerce.number().int().min(1).max(360),
    billingInstallmentDueDate: z
      .string()
      .trim()
      .regex(dateOnlyPattern, "Inform the installment due date in YYYY-MM-DD format."),
    billingLateFeeAmount: moneySchema,
    billingMonthlyInterestAmount: moneySchema,
  })
  .superRefine((payload, context) => {
    const principalAmount = Number(payload.billingPrincipalAmount.replace(",", "."));
    const downPaymentAmount = Number(payload.billingDownPaymentAmount.replace(",", "."));

    if (downPaymentAmount > principalAmount) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Down payment cannot exceed the principal amount.",
        path: ["billingDownPaymentAmount"],
      });
    }
  });

function normalizeMoneyValue(value: string) {
  return value.replace(",", ".");
}

function parseDateOnlyValue(value: string) {
  return new Date(`${value}T12:00:00.000Z`);
}

function buildCaseTitle(input: {
  title: string;
  workflowName: string;
  clientName: string;
  caseNumber: string;
}) {
  if (input.title.trim()) {
    return input.title.trim();
  }

  const composed = `${input.workflowName} • ${input.clientName}`.trim();
  return composed || input.caseNumber;
}

async function nextCaseNumber(lawFirmId: string) {
  const count = await prisma.caseRecord.count({
    where: { law_firm_id: lawFirmId },
  });

  return `CASE-${String(count + 1).padStart(6, "0")}`;
}

async function findAccessibleWorkflowTemplate(lawFirmId: string, templateId: string) {
  const [template] = await prisma.$queryRaw<
    Array<{
      id: string;
      name: string;
      case_type_code: string;
      case_subtype_code: string | null;
      law_firm_id: string | null;
    }>
  >`
    SELECT id, name, case_type_code, case_subtype_code, law_firm_id
    FROM workflow_templates
    WHERE id = ${templateId}
      AND retired_at IS NULL
      AND (law_firm_id = ${lawFirmId} OR law_firm_id IS NULL)
    LIMIT 1
  `;

  return template ?? null;
}

export async function registerCaseRoutes(app: FastifyInstance) {
  app.get("/", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const [cases, clients] = await prisma.$transaction([
      prisma.caseRecord.findMany({
        where: {
          law_firm_id: profile.lawFirm.id,
          deleted_at: null,
        },
        orderBy: {
          created_at: "desc",
        },
      }),
      prisma.client.findMany({
        where: {
          law_firm_id: profile.lawFirm.id,
          deleted_at: null,
        },
        select: {
          id: true,
          first_name: true,
          middle_name: true,
          last_name: true,
        },
      }),
    ]);

    const clientsMap = new Map(clients.map((client) => [client.id, formatClientName(client)]));

    return cases.map((caseRecord) =>
      serializeCaseSummary(caseRecord, clientsMap.get(caseRecord.client_id) ?? "Unknown client"),
    );
  });

  app.post("/", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const payload = caseMutationSchema.parse(request.body);

    const [client, workflowTemplate] = await Promise.all([
      prisma.client.findFirst({
        where: {
          id: payload.clientId,
          law_firm_id: profile.lawFirm.id,
          deleted_at: null,
        },
      }),
      findAccessibleWorkflowTemplate(profile.lawFirm.id, payload.workflowTemplateId),
    ]);

    if (!client) {
      throw reply.notFound("Client not found");
    }

    if (!workflowTemplate) {
      throw reply.notFound("Workflow template not found");
    }

    const caseNumber = await nextCaseNumber(profile.lawFirm.id);
    const clientName = formatClientName(client);
    const resolvedTitle = buildCaseTitle({
      title: payload.title ?? "",
      workflowName: workflowTemplate.name,
      clientName,
      caseNumber,
    });

    const caseRecord = await prisma.$transaction(async (tx) => {
      const createdCase = await tx.caseRecord.create({
        data: {
          id: createId(),
          law_firm_id: profile.lawFirm.id,
          office_id: profile.user.primaryOfficeId ?? null,
          client_id: payload.clientId,
          case_number: caseNumber,
          title: resolvedTitle,
          case_type_code: payload.caseTypeCode?.trim() || workflowTemplate.case_type_code,
          case_subtype_code:
            payload.caseSubtypeCode?.trim() || workflowTemplate.case_subtype_code || null,
          selected_workflow_template_id: workflowTemplate.id,
          status_code: payload.statusCode,
          priority_code: payload.priorityCode,
          billing_principal_amount: normalizeMoneyValue(payload.billingPrincipalAmount),
          billing_down_payment_amount: normalizeMoneyValue(payload.billingDownPaymentAmount),
          billing_installment_count: payload.billingInstallmentCount,
          billing_installment_due_date: parseDateOnlyValue(payload.billingInstallmentDueDate),
          billing_late_fee_amount: normalizeMoneyValue(payload.billingLateFeeAmount),
          billing_monthly_interest_amount: normalizeMoneyValue(
            payload.billingMonthlyInterestAmount,
          ),
          responsible_attorney_id: profile.user.roleCodes.some((code) =>
            ["admin", "partner", "attorney"].includes(code),
          )
            ? profile.user.id
            : null,
          created_by_user_id: profile.user.id,
        },
      });

      await ensureWorkflowInstanceForCase({
        lawFirmId: profile.lawFirm.id,
        caseId: createdCase.id,
        clientId: createdCase.client_id,
        workflowTemplateId: workflowTemplate.id,
        actorUserId: profile.user.id,
        db: tx,
      });

      return createdCase;
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "case",
      entityId: caseRecord.id,
      action: "case.create",
      afterJson: {
        caseNumber: caseRecord.case_number,
        title: caseRecord.title,
        clientId: caseRecord.client_id,
        workflowTemplateId: caseRecord.selected_workflow_template_id,
        statusCode: caseRecord.status_code,
        billingPrincipalAmount: serializeDecimalValue(caseRecord.billing_principal_amount),
      },
      request,
    });

    return reply.code(201).send(serializeCaseSummary(caseRecord, clientName));
  });

  app.patch("/:caseId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { caseId } = request.params as { caseId: string };

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const payload = caseMutationSchema.parse(request.body);

    const existingCase = await prisma.caseRecord.findFirst({
      where: {
        id: caseId,
        law_firm_id: profile.lawFirm.id,
        deleted_at: null,
      },
    });

    if (!existingCase) {
      throw reply.notFound("Case not found");
    }

    const [client, workflowTemplate] = await Promise.all([
      prisma.client.findFirst({
        where: {
          id: payload.clientId,
          law_firm_id: profile.lawFirm.id,
          deleted_at: null,
        },
      }),
      findAccessibleWorkflowTemplate(profile.lawFirm.id, payload.workflowTemplateId),
    ]);

    if (!client) {
      throw reply.notFound("Client not found");
    }

    if (!workflowTemplate) {
      throw reply.notFound("Workflow template not found");
    }

    const clientName = formatClientName(client);
    const resolvedTitle = buildCaseTitle({
      title: payload.title ?? "",
      workflowName: workflowTemplate.name,
      clientName,
      caseNumber: existingCase.case_number,
    });
    const shouldRefreshWorkflow =
      existingCase.client_id !== payload.clientId ||
      existingCase.selected_workflow_template_id !== workflowTemplate.id;

    const updatedCase = await prisma.$transaction(async (tx) => {
      const nextCase = await tx.caseRecord.update({
        where: {
          id: existingCase.id,
        },
        data: {
          client_id: payload.clientId,
          title: resolvedTitle,
          case_type_code: payload.caseTypeCode?.trim() || workflowTemplate.case_type_code,
          case_subtype_code:
            payload.caseSubtypeCode?.trim() || workflowTemplate.case_subtype_code || null,
          selected_workflow_template_id: workflowTemplate.id,
          status_code: payload.statusCode,
          priority_code: payload.priorityCode,
          billing_principal_amount: normalizeMoneyValue(payload.billingPrincipalAmount),
          billing_down_payment_amount: normalizeMoneyValue(payload.billingDownPaymentAmount),
          billing_installment_count: payload.billingInstallmentCount,
          billing_installment_due_date: parseDateOnlyValue(payload.billingInstallmentDueDate),
          billing_late_fee_amount: normalizeMoneyValue(payload.billingLateFeeAmount),
          billing_monthly_interest_amount: normalizeMoneyValue(
            payload.billingMonthlyInterestAmount,
          ),
        },
      });

      if (!nextCase.archived_at && shouldRefreshWorkflow) {
        await ensureWorkflowInstanceForCase({
          lawFirmId: profile.lawFirm.id,
          caseId: nextCase.id,
          clientId: nextCase.client_id,
          workflowTemplateId: workflowTemplate.id,
          actorUserId: profile.user.id,
          db: tx,
          forceActivate: true,
        });
      }

      return nextCase;
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "case",
      entityId: existingCase.id,
      action: "case.update",
      beforeJson: {
        title: existingCase.title,
        clientId: existingCase.client_id,
        workflowTemplateId: existingCase.selected_workflow_template_id,
        billingPrincipalAmount: serializeDecimalValue(existingCase.billing_principal_amount),
      },
      afterJson: {
        title: updatedCase.title,
        clientId: updatedCase.client_id,
        workflowTemplateId: updatedCase.selected_workflow_template_id,
        billingPrincipalAmount: serializeDecimalValue(updatedCase.billing_principal_amount),
      },
      request,
    });

    return serializeCaseSummary(updatedCase, clientName);
  });

  app.post("/:caseId/archive", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { caseId } = request.params as { caseId: string };

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const caseRecord = await prisma.caseRecord.findFirst({
      where: {
        id: caseId,
        law_firm_id: profile.lawFirm.id,
        deleted_at: null,
      },
    });

    if (!caseRecord) {
      throw reply.notFound("Case not found");
    }

    if (caseRecord.archived_at) {
      throw reply.badRequest("Case is already archived");
    }

    const updatedCase = await prisma.caseRecord.update({
      where: { id: caseRecord.id },
      data: {
        archived_at: new Date(),
        archived_by_user_id: profile.user.id,
      },
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "case",
      entityId: caseRecord.id,
      action: "case.archive",
      beforeJson: {
        archivedAt: caseRecord.archived_at,
      },
      afterJson: {
        archivedAt: updatedCase.archived_at,
      },
      request,
    });

    return {
      id: updatedCase.id,
      archivedAt: updatedCase.archived_at,
    };
  });

  app.post("/:caseId/unarchive", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { caseId } = request.params as { caseId: string };

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const caseRecord = await prisma.caseRecord.findFirst({
      where: {
        id: caseId,
        law_firm_id: profile.lawFirm.id,
        deleted_at: null,
      },
    });

    if (!caseRecord) {
      throw reply.notFound("Case not found");
    }

    if (!caseRecord.archived_at) {
      throw reply.badRequest("Case is not archived");
    }

    const updatedCase = await prisma.caseRecord.update({
      where: { id: caseRecord.id },
      data: {
        archived_at: null,
        archived_by_user_id: null,
      },
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "case",
      entityId: caseRecord.id,
      action: "case.unarchive",
      beforeJson: {
        archivedAt: caseRecord.archived_at,
      },
      afterJson: {
        archivedAt: updatedCase.archived_at,
      },
      request,
    });

    return {
      id: updatedCase.id,
      archivedAt: updatedCase.archived_at,
    };
  });
}
