import { z } from "zod";
import { prisma } from "../../lib/prisma";
import { cache } from "../../lib/cache";
import { PaymentStatus } from "../../generated/prisma/client";

export const createPeriodicExpenseSchema = z.object({
  description: z.string().min(1).max(50),
  defaultDueDay: z.number().int().min(1).max(31),
  defaultAmount: z.number().positive(),
  periods: z.array(z.number().int().min(1).max(12)).min(1),
});

export const updatePeriodicExpenseSchema = createPeriodicExpenseSchema;

export const createNonPeriodicExpenseSchema = z.object({
  description: z.string().min(1).max(50),
  amount: z.number().positive(),
  dueDate: z.string().datetime(),
});

export const payExpenseSchema = z.object({
  amountPayed: z.number().positive(),
  paymentDate: z.string().datetime().optional(),
});

function lastDayOfMonth(year: number, month: number): number {
  return new Date(year, month, 0).getDate();
}

export function buildDueDate(year: number, month: number, dueDay: number): Date {
  const clampedDay = Math.min(dueDay, lastDayOfMonth(year, month));
  return new Date(Date.UTC(year, month - 1, clampedDay, 0, 0, 0, 0));
}

export function getCurrentYearMonthUTC(): { year: number; month: number } {
  const now = new Date();
  return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1 };
}

export async function listPeriodicExpenses() {
  const cached = cache.get("expenses:periodic");
  if (cached) return cached;

  const data = await prisma.periodicExpense.findMany({
    where: { isDeleted: false },
    orderBy: { description: "asc" },
  });
  cache.set("expenses:periodic", data);
  return data;
}

export async function createPeriodicExpense(data: z.infer<typeof createPeriodicExpenseSchema>) {
  const result = await prisma.periodicExpense.create({ data });
  const { year, month } = getCurrentYearMonthUTC();
  const currentMonthApplicable = data.periods.includes(month);
  cache.invalidateByPrefix("expenses:");
  return { ...result, currentMonthApplicable };
}

export async function updatePeriodicExpense(id: number, data: z.infer<typeof updatePeriodicExpenseSchema>) {
  const result = await prisma.periodicExpense.update({ where: { id }, data });
  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function softDeletePeriodicExpense(id: number) {
  const result = await prisma.periodicExpense.update({
    where: { id },
    data: { isDeleted: true },
  });
  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function generateMonthlyExpense(id: number) {
  const periodic = await prisma.periodicExpense.findUniqueOrThrow({ where: { id } });
  const { year, month } = getCurrentYearMonthUTC();

  const existing = await prisma.expense.findFirst({
    where: { periodicExpenseId: id, year, month },
  });
  if (existing) return existing;

  const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
  const result = await prisma.expense.create({
    data: {
      description: periodic.description,
      periodicExpenseId: id,
      year,
      month,
      amount: periodic.defaultAmount,
      dueDate,
      status: PaymentStatus.PENDING,
    },
  });
  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function listExpenses(params: {
  status?: string;
  page?: number;
  pageSize?: number;
  periodicExpenseId?: number;
  search?: string;
}) {
  const { status, page = 1, pageSize = 10, periodicExpenseId, search } = params;

  const cacheKey = `expenses:list:${status ?? "all"}:${page}:${pageSize}:${periodicExpenseId ?? "none"}:${search ?? ""}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const where: Record<string, unknown> = {};
  if (status === "PENDING" || status === "PAYED") {
    where.status = status;
  }
  if (periodicExpenseId !== undefined) {
    where.periodicExpenseId = periodicExpenseId;
  }
  if (search) {
    where.description = { contains: search, mode: "insensitive" };
  }
  const [data, total] = await Promise.all([
    prisma.expense.findMany({
      where,
      include: { periodicExpense: true },
      orderBy: { dueDate: "desc" },
      skip: (page - 1) * pageSize,
      take: pageSize,
    }),
    prisma.expense.count({ where }),
  ]);
  const result = { data, total, page, pageSize };
  cache.set(cacheKey, result);
  return result;
}

export async function createNonPeriodicExpense(data: z.infer<typeof createNonPeriodicExpenseSchema>) {
  const dueDate = new Date(data.dueDate);
  const year = dueDate.getUTCFullYear();
  const month = dueDate.getUTCMonth() + 1;
  const result = await prisma.expense.create({
    data: {
      description: data.description,
      amount: data.amount,
      amountPayed: data.amount,
      dueDate,
      paymentDate: dueDate,
      year,
      month,
      status: PaymentStatus.PAYED,
    },
  });
  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function payExpense(id: number, data: z.infer<typeof payExpenseSchema>) {
  const paymentDate = data.paymentDate ? new Date(data.paymentDate) : new Date();
  const result = await prisma.expense.update({
    where: { id },
    data: {
      status: PaymentStatus.PAYED,
      amountPayed: data.amountPayed,
      paymentDate,
    },
  });
  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function getMonthlyPayedTotal(year: number, month: number) {
  const cacheKey = `expenses:monthly-total:${year}:${month}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const result = await prisma.$queryRaw<Array<{ total: string | null }>>`
    SELECT CAST(SUM("amountPayed") AS NUMERIC) as total
    FROM expenses
    WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PAYED}::"PaymentStatus"
  `;
  const data = { total: Number(result[0]?.total ?? 0) };
  cache.set(cacheKey, data);
  return data;
}

export async function getMonthlyTotals(year: number, month: number) {
  const cacheKey = `expenses:totals:${year}:${month}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const result = await prisma.$queryRaw<Array<{ payed: string | null; pending: string | null }>>`
    SELECT
      (SELECT CAST(SUM("amountPayed") AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PAYED}::"PaymentStatus") as payed,
      (SELECT CAST(SUM(amount) AS NUMERIC) FROM expenses WHERE year = ${year} AND month = ${month} AND status = ${PaymentStatus.PENDING}::"PaymentStatus") as pending
  `;
  const data = {
    payed: Number(result[0]?.payed ?? 0),
    pending: Number(result[0]?.pending ?? 0),
  };
  cache.set(cacheKey, data);
  return data;
}

export async function getTotalPending() {
  const cached = cache.get("expenses:pending-total");
  if (cached) return cached;

  const result = await prisma.$queryRaw<Array<{ total: string | null }>>`
    SELECT CAST(SUM(amount) AS NUMERIC) as total
    FROM expenses
    WHERE status = ${PaymentStatus.PENDING}::"PaymentStatus"
  `;
  const data = { total: Number(result[0]?.total ?? 0) };
  cache.set("expenses:pending-total", data);
  return data;
}

export async function listPendingUpcomingExpenses() {
  const now = new Date();
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0, 0));
  const fiveDaysLater = new Date(today);
  fiveDaysLater.setUTCDate(fiveDaysLater.getUTCDate() + 5);

  const data = await prisma.expense.findMany({
    where: {
      status: PaymentStatus.PENDING,
      dueDate: { lte: fiveDaysLater },
    },
    include: { periodicExpense: true },
    orderBy: { dueDate: "asc" },
  });

  return data.map((expense) => ({
    ...expense,
    dueType: expense.dueDate < today ? "overdue" as const : "upcoming" as const,
  }));
}

export async function generateExpensesForCurrentMonth() {
  const { year, month } = getCurrentYearMonthUTC();

  const periodics = await prisma.periodicExpense.findMany({
    where: { isDeleted: false, periods: { has: month } },
  });

  const results: Array<{ id: number; status: string }> = [];
  for (const periodic of periodics) {
    const existing = await prisma.expense.findFirst({
      where: { periodicExpenseId: periodic.id, year, month },
    });
    if (existing) {
      results.push({ id: periodic.id, status: "already_exists" });
      continue;
    }
    const dueDate = buildDueDate(year, month, periodic.defaultDueDay);
    await prisma.expense.create({
      data: {
        description: periodic.description,
        periodicExpenseId: periodic.id,
        year,
        month,
        amount: periodic.defaultAmount,
        dueDate,
        status: PaymentStatus.PENDING,
      },
    });
    results.push({ id: periodic.id, status: "created" });
  }
  cache.invalidateByPrefix("expenses:");
  return results;
}
