import { v } from "convex/values";
import { mutation, query, type MutationCtx } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { assertPermission, actorLite, AppError } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { nextCode } from "./lib/seq";
import { getSettings } from "./lib/settings";
import { fmtMoney } from "../src/lib/format";
import { PAYMENT_METHODS, PAYMENT_TYPES } from "../src/lib/domain";

export async function recomputePaid(ctx: MutationCtx, reservationId: Id<"reservations">) {
  const payments = await ctx.db.query("payments").withIndex("by_reservation", (q) => q.eq("reservationId", reservationId)).collect();
  const paid = Math.max(0, payments.filter((p) => !p.reversedAt && ["PAYMENT", "DEPOSIT", "REFUND"].includes(p.type)).reduce((s, p) => s + p.amount, 0));
  await ctx.db.patch(reservationId, { amountPaid: paid, updatedAt: Date.now() });
  return paid;
}

export const record = mutation({
  args: { reservationId: v.id("reservations"), amount: v.number(), type: v.optional(v.string()), method: v.string(), paidAt: v.optional(v.number()), notes: v.optional(v.union(v.string(), v.null())) },
  returns: v.object({ id: v.id("payments"), remaining: v.number() }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "payments.record");
    const settings = await getSettings(ctx);
    const type = data.type ?? "PAYMENT";
    if (!PAYMENT_TYPES.includes(type as (typeof PAYMENT_TYPES)[number])) throw new AppError("Invalid payment type", "VALIDATION");
    if (!PAYMENT_METHODS.includes(data.method as (typeof PAYMENT_METHODS)[number])) throw new AppError("Invalid method", "VALIDATION");
    if (!(data.amount > 0)) throw new AppError("Amount must be greater than zero", "VALIDATION", { fields: { amount: "Amount must be greater than zero" } });
    const r = await ctx.db.get(data.reservationId);
    if (!r) throw new AppError("Reservation not found", "NOT_FOUND");
    const customer = await ctx.db.get(r.customerId);
    const refund = type === "REFUND" || type === "DEPOSIT_REFUND";
    if (["CANCELLED", "NO_SHOW"].includes(r.status) && !refund) throw new AppError("Cannot record payments on a cancelled reservation.", "VALIDATION");
    const signed = refund ? -Math.abs(data.amount) : Math.abs(data.amount);
    const code = await nextCode(ctx, "payment");
    const id = await ctx.db.insert("payments", { code, reservationId: r._id, customerId: r.customerId, amount: signed, type, method: data.method, paidAt: data.paidAt ?? Date.now(), recordedById: user.id, notes: data.notes || undefined });
    const paid = await recomputePaid(ctx, r._id);
    if (type !== "DEPOSIT_REFUND" && !r.paymentMethod) await ctx.db.patch(r._id, { paymentMethod: data.method });
    await audit(ctx, actorLite(user), { action: refund ? "PAYMENT_REVERSED" : "PAYMENT_RECORDED", module: "payments", entityType: "payment", entityId: id, entityLabel: `${code} · ${r.code}`, newValue: { amount: signed, method: data.method, type }, reservationId: r._id, customerId: r.customerId, apartmentId: r.apartmentId });
    await notify(ctx, { type: "PAYMENT_RECORDED", title: refund ? "Refund issued" : "Payment recorded", body: `${user.fullName} recorded ${fmtMoney(Math.abs(signed), settings.currency)} (${data.method.toLowerCase().replace("_", " ")}) for ${r.code} · ${customer?.fullName ?? ""}.`, href: `/reservations/${r._id}`, actorId: user.id });
    return { id, remaining: Math.max(0, r.totalAmount - paid) };
  },
});

export const reverse = mutation({
  args: { paymentId: v.id("payments"), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { paymentId, reason }) => {
    const user = await assertPermission(ctx, "payments.reverse");
    if (reason.trim().length < 3) throw new AppError("A reason is required.", "VALIDATION");
    const p = await ctx.db.get(paymentId);
    if (!p) throw new AppError("Payment not found", "NOT_FOUND");
    if (p.reversedAt) throw new AppError("Payment already reversed.", "VALIDATION");
    const r = await ctx.db.get(p.reservationId);
    await ctx.db.patch(paymentId, { reversedAt: Date.now(), reversalReason: reason });
    await recomputePaid(ctx, p.reservationId);
    await audit(ctx, actorLite(user), { action: "PAYMENT_REVERSED", module: "payments", entityType: "payment", entityId: p._id, entityLabel: `${p.code} · ${r?.code ?? ""}`, previousValue: { amount: p.amount, reversed: false }, newValue: { reversed: true }, reason, reservationId: p.reservationId, customerId: p.customerId, severity: "WARNING" });
    return null;
  },
});

/** Payments page: recent payments with reservation / customer context. */
export const list = query({
  args: { from: v.optional(v.string()), to: v.optional(v.string()), limit: v.optional(v.number()) },
  returns: v.array(v.any()),
  handler: async (ctx, { limit }) => {
    await assertPermission(ctx, "payments.view", "financials.view_revenue");
    const rows = await ctx.db.query("payments").withIndex("by_paidAt").order("desc").take(limit ?? 400);
    return Promise.all(
      rows.map(async (p) => {
        const [r, c, by] = await Promise.all([ctx.db.get(p.reservationId), ctx.db.get(p.customerId), ctx.db.get(p.recordedById)]);
        const apt = r ? await ctx.db.get(r.apartmentId) : null;
        return { ...p, id: p._id, reservation: r ? { id: r._id, code: r.code, status: r.status, totalAmount: r.totalAmount, amountPaid: r.amountPaid, checkIn: r.checkIn, checkOut: r.checkOut, apartment: apt?.code ?? "" } : null, customer: c ? { id: c._id, fullName: c.fullName } : null, recordedBy: by?.fullName ?? "" };
      })
    );
  },
});

/** Outstanding balances across active / completed stays. */
export const outstanding = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    await assertPermission(ctx, "payments.view", "financials.view_revenue");
    const out: unknown[] = [];
    for (const status of ["CHECKED_IN", "CONFIRMED", "CHECKED_OUT"]) {
      const rs = await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", status)).order("desc").take(400);
      for (const r of rs) {
        const due = r.totalAmount - r.amountPaid;
        if (due <= 0.5) continue;
        const [c, a] = await Promise.all([ctx.db.get(r.customerId), ctx.db.get(r.apartmentId)]);
        out.push({ id: r._id, code: r.code, status: r.status, checkIn: r.checkIn, checkOut: r.checkOut, totalAmount: r.totalAmount, amountPaid: r.amountPaid, due, customer: c ? { id: c._id, fullName: c.fullName, phone: c.phone } : null, apartment: a?.code ?? "" });
      }
    }
    return out;
  },
});
