import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { assertPermission, actorLite, AppError, requireActor, can } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { nextCode } from "./lib/seq";
import { getSettings } from "./lib/settings";
import { addDaysKey, addMonthsKey, todayKey } from "./lib/days";
import { fmtMoney } from "../src/lib/format";
import { PAYMENT_METHODS } from "../src/lib/domain";

const expenseArgs = {
  date: v.string(),
  categoryId: v.id("expenseCategories"),
  apartmentId: v.optional(v.union(v.id("apartments"), v.null())),
  description: v.string(),
  amount: v.number(),
  paymentMethod: v.optional(v.string()),
  vendor: v.optional(v.union(v.string(), v.null())),
  isRecurring: v.optional(v.boolean()),
  recurrence: v.optional(v.union(v.string(), v.null())),
  notes: v.optional(v.union(v.string(), v.null())),
};

function validate(d: { date: string; description: string; amount: number; paymentMethod?: string; recurrence?: string | null }) {
  const fields: Record<string, string> = {};
  if (!/^\d{4}-\d{2}-\d{2}$/.test(d.date)) fields.date = "Invalid date";
  if (d.description.trim().length < 2) fields.description = "Description is required";
  if (!(d.amount > 0)) fields.amount = "Amount must be greater than zero";
  if (d.paymentMethod && !PAYMENT_METHODS.includes(d.paymentMethod as (typeof PAYMENT_METHODS)[number])) fields.paymentMethod = "Invalid method";
  if (d.recurrence && !["WEEKLY", "MONTHLY", "YEARLY"].includes(d.recurrence)) fields.recurrence = "Invalid recurrence";
  if (Object.keys(fields).length) throw new AppError(Object.values(fields)[0], "VALIDATION", { fields });
}

export const categories = query({
  args: {},
  returns: v.array(v.object({ id: v.id("expenseCategories"), key: v.string(), name: v.string(), icon: v.union(v.string(), v.null()), isActive: v.boolean(), sortOrder: v.number(), count: v.number() })),
  handler: async (ctx) => {
    await requireActor(ctx);
    const cats = (await ctx.db.query("expenseCategories").collect()).sort((a, b) => a.sortOrder - b.sortOrder);
    return Promise.all(cats.map(async (c) => ({ id: c._id, key: c.key, name: c.name, icon: c.icon ?? null, isActive: c.isActive, sortOrder: c.sortOrder, count: (await ctx.db.query("expenses").withIndex("by_category", (q) => q.eq("categoryId", c._id)).collect()).filter((e) => !e.deletedAt).length })));
  },
});

export const list = query({
  args: { from: v.optional(v.string()), to: v.optional(v.string()) },
  returns: v.array(v.any()),
  handler: async (ctx, { from, to }) => {
    const actor = await assertPermission(ctx, "expenses.view", "expenses.create");
    const mineOnly = !can(actor, "expenses.view");
    const start = from ?? addDaysKey(todayKey(), -365);
    const end = to ?? addDaysKey(todayKey(), 1);
    const rows = (await ctx.db.query("expenses").withIndex("by_date", (q) => q.gte("date", start).lte("date", end)).collect()).filter((e) => !e.deletedAt && (!mineOnly || e.addedById === actor.id)).sort((a, b) => b.date.localeCompare(a.date));
    const cats = await ctx.db.query("expenseCategories").collect();
    return Promise.all(
      rows.map(async (e) => {
        const [apt, by] = await Promise.all([e.apartmentId ? ctx.db.get(e.apartmentId) : null, ctx.db.get(e.addedById)]);
        const docs = await ctx.db.query("documents").withIndex("by_expense", (q) => q.eq("expenseId", e._id)).collect();
        const cat = cats.find((c) => c._id === e.categoryId);
        return { ...e, id: e._id, category: cat ? { id: cat._id, name: cat.name, key: cat.key } : null, apartment: apt ? { id: apt._id, code: apt.code } : null, addedBy: by?.fullName ?? "", documents: docs.filter((d) => !d.deletedAt).length };
      })
    );
  },
});

export const create = mutation({
  args: expenseArgs,
  returns: v.object({ id: v.id("expenses") }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "expenses.create");
    const settings = await getSettings(ctx);
    validate(data);
    const cat = await ctx.db.get(data.categoryId);
    if (!cat) throw new AppError("Category not found", "NOT_FOUND");
    const apt = data.apartmentId ? await ctx.db.get(data.apartmentId) : null;
    const isRecurring = !!data.isRecurring;
    const id = await ctx.db.insert("expenses", { code: await nextCode(ctx, "expense"), date: data.date, categoryId: data.categoryId, apartmentId: data.apartmentId ?? undefined, description: data.description.trim(), amount: data.amount, paymentMethod: data.paymentMethod ?? "CASH", vendor: data.vendor || undefined, isRecurring, recurrence: isRecurring ? (data.recurrence ?? "MONTHLY") : undefined, notes: data.notes || undefined, addedById: user.id, updatedAt: Date.now() });
    const row = (await ctx.db.get(id))!;
    await audit(ctx, actorLite(user), { action: "EXPENSE_ADDED", module: "expenses", entityType: "expense", entityId: id, entityLabel: `${row.code} · ${row.description}`, newValue: { amount: row.amount, category: cat.name, apartment: apt?.code ?? null, recurring: isRecurring }, apartmentId: data.apartmentId ?? null });
    await notify(ctx, { type: "NEW_EXPENSE", title: "New expense", body: `${user.fullName} added ${cat.name}: ${row.description} — ${fmtMoney(row.amount, settings.currency)}${apt ? ` (${apt.code})` : ""}.`, href: "/expenses", actorId: user.id });
    return { id };
  },
});

export const update = mutation({
  args: { id: v.id("expenses"), ...expenseArgs },
  returns: v.null(),
  handler: async (ctx, { id, ...data }) => {
    const user = await assertPermission(ctx, "expenses.edit");
    validate(data);
    const ex = await ctx.db.get(id);
    if (!ex || ex.deletedAt) throw new AppError("Expense not found", "NOT_FOUND");
    const isRecurring = !!data.isRecurring;
    await ctx.db.patch(id, { date: data.date, categoryId: data.categoryId, apartmentId: data.apartmentId ?? undefined, description: data.description.trim(), amount: data.amount, paymentMethod: data.paymentMethod ?? "CASH", vendor: data.vendor || undefined, isRecurring, recurrence: isRecurring ? (data.recurrence ?? "MONTHLY") : undefined, notes: data.notes || undefined, updatedAt: Date.now() });
    await audit(ctx, actorLite(user), { action: "EXPENSE_EDITED", module: "expenses", entityType: "expense", entityId: id, entityLabel: `${ex.code} · ${data.description}`, previousValue: { amount: ex.amount, description: ex.description, date: ex.date }, newValue: { amount: data.amount, description: data.description, date: data.date }, apartmentId: data.apartmentId ?? null });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("expenses"), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, reason }) => {
    const user = await assertPermission(ctx, "expenses.delete");
    const ex = await ctx.db.get(id);
    if (!ex || ex.deletedAt) throw new AppError("Expense not found", "NOT_FOUND");
    await ctx.db.patch(id, { deletedAt: Date.now(), updatedAt: Date.now() });
    await audit(ctx, actorLite(user), { action: "EXPENSE_DELETED", module: "expenses", entityType: "expense", entityId: id, entityLabel: `${ex.code} · ${ex.description}`, previousValue: { amount: ex.amount }, reason, apartmentId: ex.apartmentId ?? null, severity: "WARNING" });
    return null;
  },
});

/** Materialise due occurrences of recurring expenses. Idempotent. */
export const generateRecurring = mutation({
  args: {},
  returns: v.object({ created: v.number() }),
  handler: async (ctx) => {
    const user = await assertPermission(ctx, "expenses.create");
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const templates = (await ctx.db.query("expenses").collect()).filter((e) => e.isRecurring && !e.deletedAt && !e.recurringParentId);
    let created = 0;
    for (const t of templates) {
      const children = (await ctx.db.query("expenses").withIndex("by_parent", (q) => q.eq("recurringParentId", t._id)).collect()).filter((e) => !e.deletedAt);
      const last = [t, ...children].sort((a, b) => b.date.localeCompare(a.date))[0];
      const step = (k: string) => (t.recurrence === "WEEKLY" ? addDaysKey(k, 7) : t.recurrence === "YEARLY" ? addMonthsKey(k, 12) : addMonthsKey(k, 1));
      let next = step(last.date);
      while (next <= today && created < 500) {
        await ctx.db.insert("expenses", { code: await nextCode(ctx, "expense"), date: next, categoryId: t.categoryId, apartmentId: t.apartmentId, description: t.description, amount: t.amount, paymentMethod: t.paymentMethod, vendor: t.vendor, isRecurring: false, recurringParentId: t._id, addedById: user.id, notes: "Generated from recurring expense", updatedAt: Date.now() });
        created++;
        next = step(next);
      }
    }
    if (created) await audit(ctx, actorLite(user), { action: "EXPENSE_ADDED", module: "expenses", entityType: "expense", entityLabel: `${created} recurring occurrences`, newValue: { created } });
    return { created };
  },
});

export const saveCategory = mutation({
  args: { id: v.optional(v.id("expenseCategories")), name: v.string(), key: v.optional(v.string()), isActive: v.optional(v.boolean()) },
  returns: v.null(),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "settings.manage");
    const name = input.name.trim();
    if (name.length < 2) throw new AppError("Name is too short", "VALIDATION");
    if (input.id) await ctx.db.patch(input.id, { name, isActive: input.isActive ?? true });
    else {
      const key = (input.key ?? name).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
      const count = (await ctx.db.query("expenseCategories").collect()).length;
      await ctx.db.insert("expenseCategories", { key, name, sortOrder: count, isActive: true });
    }
    await audit(ctx, actorLite(user), { action: "SETTINGS_CHANGED", module: "settings", entityType: "expenseCategory", entityLabel: name, newValue: input });
    return null;
  },
});
