import { z } from "zod";
import { PaymentStatus, QuoteType, type Prisma } from "../../generated/prisma/client";
import { cache } from "../../lib/cache";
import { prisma } from "../../lib/prisma";
import { roundTo } from "../../lib/utils";

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(),
  walletId: z.number().int().positive(),
  usdcConversionRate: z.number().positive().optional(),
});

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

export const updateExpenseSchema = z.object({
  amount: z.number().positive("El monto debe ser mayor a 0"),
  dueDate: z
    .string()
    .datetime({ message: "La fecha debe ser una fecha ISO válida" }),
  status: z.enum(["PENDING"]).optional(),
  amountPayed: z
    .number()
    .positive("El monto pagado debe ser mayor a 0")
    .optional(),
  paymentDate: z
    .string()
    .datetime({ message: "La fecha debe ser una fecha ISO válida" })
    .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 };
}

function isSameDay(a: Date, b: Date): boolean {
  return (
    a.getUTCFullYear() === b.getUTCFullYear() &&
    a.getUTCMonth() === b.getUTCMonth() &&
    a.getUTCDate() === b.getUTCDate()
  );
}

export async function getBeloSellPriceForDate(
  paymentDate: Date,
): Promise<number | null> {
  const now = new Date();

  if (isSameDay(paymentDate, now)) {
    const quote = await prisma.quote.findFirst({
      where: { type: QuoteType.BELO },
    });
    return quote ? Number(quote.sell) : null;
  }

  const target = new Date(
    Date.UTC(
      paymentDate.getUTCFullYear(),
      paymentDate.getUTCMonth(),
      paymentDate.getUTCDate(),
      17, 0, 0, 0,
    ),
  );

  const before = await prisma.quoteHistory.findFirst({
    where: {
      type: QuoteType.BELO,
      timeStamp: { lte: target },
    },
    orderBy: { timeStamp: "desc" },
  });

  const after = await prisma.quoteHistory.findFirst({
    where: {
      type: QuoteType.BELO,
      timeStamp: { gte: target },
    },
    orderBy: { timeStamp: "asc" },
  });

  if (before && after) {
    const beforeDiff = Math.abs(before.timeStamp.getTime() - target.getTime());
    const afterDiff = Math.abs(after.timeStamp.getTime() - target.getTime());
    return beforeDiff <= afterDiff ? Number(before.sell) : Number(after.sell);
  }

  if (before) return Number(before.sell);
  if (after) return Number(after.sell);
  return null;
}

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 { 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 wallet = await prisma.wallet.findUnique({
    where: { id: data.walletId },
  });
  if (!wallet) {
    throw new Error("Billetera no encontrada");
  }

  const isUSDC = wallet.currency === "USDC";

  if (isUSDC && !data.usdcConversionRate) {
    throw new Error("La tasa de conversión es obligatoria para billeteras USDC");
  }

  const belo = isUSDC ? null : await getBeloSellPriceForDate(dueDate);

  const movementAmount =
    isUSDC && data.usdcConversionRate
      ? -roundTo(data.amount / data.usdcConversionRate, 4)
      : -data.amount;

  const expenseData: Prisma.ExpenseUncheckedCreateInput = {
    description: data.description,
    amount: data.amount,
    amountPayed: data.amount,
    dueDate,
    paymentDate: dueDate,
    year,
    month,
    status: PaymentStatus.PAYED,
    walletId: data.walletId,
  };

  if (isUSDC && data.usdcConversionRate) {
    expenseData.beloPrice = data.usdcConversionRate;
    expenseData.usdcEquivalent = roundTo(
      data.amount / data.usdcConversionRate,
      6,
    );
  } else if (belo !== null) {
    expenseData.beloPrice = belo;
    expenseData.usdcEquivalent = roundTo(data.amount / belo, 6);
  }

  const [result] = await prisma.$transaction([
    prisma.expense.create({ data: expenseData }),
    prisma.wallet.update({
      where: { id: data.walletId },
      data: { balance: { increment: movementAmount } },
    }),
    prisma.walletMovement.create({
      data: {
        walletId: data.walletId,
        type: "WITHDRAWAL",
        amount: movementAmount,
        description: `Pago: ${data.description}`,
      },
    }),
  ]);

  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function payExpense(
  id: number,
  data: z.infer<typeof payExpenseSchema>,
) {
  const expense = await prisma.expense.findUniqueOrThrow({ where: { id } });

  const paymentDate = data.paymentDate
    ? new Date(data.paymentDate)
    : new Date();

  const wallet = await prisma.wallet.findUnique({
    where: { id: data.walletId },
  });
  if (!wallet) {
    throw new Error("Billetera no encontrada");
  }

  const isUSDC = wallet.currency === "USDC";

  if (isUSDC && !data.usdcConversionRate) {
    throw new Error("La tasa de conversión es obligatoria para billeteras USDC");
  }

  const belo = isUSDC ? null : await getBeloSellPriceForDate(paymentDate);

  const movementAmount = isUSDC && data.usdcConversionRate
    ? -roundTo(data.amountPayed / data.usdcConversionRate, 4)
    : -data.amountPayed;

  const expenseUpdateData: Prisma.ExpenseUncheckedUpdateInput = {
    status: PaymentStatus.PAYED,
    amountPayed: data.amountPayed,
    paymentDate,
    walletId: data.walletId,
  };

  if (isUSDC && data.usdcConversionRate) {
    expenseUpdateData.beloPrice = data.usdcConversionRate;
    expenseUpdateData.usdcEquivalent = roundTo(data.amountPayed / data.usdcConversionRate, 6);
  } else if (belo !== null) {
    expenseUpdateData.beloPrice = belo;
    expenseUpdateData.usdcEquivalent = roundTo(data.amountPayed / belo, 6);
  }

  const [result] = await prisma.$transaction([
    prisma.expense.update({ where: { id }, data: expenseUpdateData }),
    prisma.wallet.update({
      where: { id: data.walletId },
      data: { balance: { increment: movementAmount } },
    }),
    prisma.walletMovement.create({
      data: {
        walletId: data.walletId,
        type: "WITHDRAWAL",
        amount: movementAmount,
        description: `Pago: ${expense.description}`,
      },
    }),
  ]);

  cache.invalidateByPrefix("expenses:");
  return result;
}

export async function updateExpense(
  id: number,
  data: z.infer<typeof updateExpenseSchema>,
) {
  const existing = await prisma.expense.findUniqueOrThrow({ where: { id } });

  const dueDate = new Date(data.dueDate);
  const year = dueDate.getUTCFullYear();
  const month = dueDate.getUTCMonth() + 1;

  const updateData: Prisma.ExpenseUncheckedUpdateInput = {
    amount: data.amount,
    dueDate,
    year,
    month,
  };

  let walletAdjustment: {
    walletId: number;
    amount: number;
    description: string;
  } | null = null;

  if (data.status === "PENDING") {
    updateData.status = PaymentStatus.PENDING;
    updateData.amountPayed = null;
    updateData.paymentDate = null;
    updateData.beloPrice = null;
    updateData.usdcEquivalent = null;

    if (existing.status === PaymentStatus.PAYED && existing.walletId) {
      const refundAmount = existing.amountPayed
        ? Number(existing.amountPayed)
        : Number(existing.amount);
      walletAdjustment = {
        walletId: existing.walletId,
        amount: refundAmount,
        description: "Anulación de gasto",
      };
    }
  } else if (existing.status === PaymentStatus.PAYED) {
    if (data.amountPayed !== undefined) {
      updateData.amountPayed = data.amountPayed;
    }
    if (data.paymentDate !== undefined) {
      updateData.paymentDate = new Date(data.paymentDate);
    }

    const resolvedPaymentDate = updateData.paymentDate instanceof Date
      ? updateData.paymentDate
      : (existing.paymentDate ?? undefined);
    const amountPayed = updateData.amountPayed ?? existing.amountPayed;
    if (resolvedPaymentDate && amountPayed !== undefined && amountPayed !== null) {
      const belo = await getBeloSellPriceForDate(resolvedPaymentDate);
      if (belo !== null) {
        updateData.beloPrice = belo;
        updateData.usdcEquivalent = roundTo(Number(amountPayed) / belo, 6);
      } else {
        updateData.beloPrice = null;
        updateData.usdcEquivalent = null;
      }
    }

    if (existing.walletId) {
      const oldPayed = existing.amountPayed
        ? Number(existing.amountPayed)
        : Number(existing.amount);
      const newPayed = data.amountPayed !== undefined
        ? Number(data.amountPayed)
        : Number(data.amount);

      if (newPayed !== oldPayed) {
        const difference = newPayed - oldPayed;
        walletAdjustment = {
          walletId: existing.walletId,
          amount: -difference,
          description: "Ajuste por edición de gasto",
        };
      }
    }
  }

  if (walletAdjustment) {
    const wallet = await prisma.wallet.findUnique({
      where: { id: walletAdjustment.walletId },
    });
    if (!wallet) {
      throw new Error("Billetera no encontrada");
    }

    const [result] = await prisma.$transaction([
      prisma.expense.update({ where: { id }, data: updateData }),
      prisma.wallet.update({
        where: { id: walletAdjustment.walletId },
        data: { balance: { increment: walletAdjustment.amount } },
      }),
      prisma.walletMovement.create({
        data: {
          walletId: walletAdjustment.walletId,
          type: walletAdjustment.amount > 0 ? "DEPOSIT" : "WITHDRAWAL",
          amount: walletAdjustment.amount,
          description: walletAdjustment.description,
        },
      }),
    ]);
    cache.invalidateByPrefix("expenses:");
    return result;
  }

  const result = await prisma.expense.update({
    where: { id },
    data: updateData,
  });
  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;
}
