/* eslint-disable @typescript-eslint/no-explicit-any -- Convex table-generic helpers; runtime validators enforce the shapes */
import { v } from "convex/values";
import { query } from "./_generated/server";
import type { Doc } from "./_generated/dataModel";
import { assertPermission } from "./lib/access";
import { parseRef } from "../src/lib/refs";
import { withId } from "./lib/shape";
import { addDaysKey } from "./lib/days";

/** Audit explorer. Filters mirror the URL search params. */
export const explorer = query({
  args: { worker: v.optional(v.string()), action: v.optional(v.string()), module: v.optional(v.string()), apartment: v.optional(v.string()), reservation: v.optional(v.string()), customer: v.optional(v.string()), entity: v.optional(v.string()), severity: v.optional(v.string()), from: v.optional(v.string()), to: v.optional(v.string()), device: v.optional(v.string()), q: v.optional(v.string()), event: v.optional(v.string()) },
  returns: v.any(),
  handler: async (ctx, sp) => {
    await assertPermission(ctx, "audit.view");
    let refLabel: string | null = null;
    let refId: string | null = null;
    let refField: "reservationId" | "customerId" | "userId" | "apartmentId" | null = null;
    let codeFilter: string | null = null;
    let labelFilter: string | null = null;
    let none = false;
    if (sp.q) {
      const ref = parseRef(sp.q);
      if (ref?.kind === "event") codeFilter = ref.code;
      else if (ref?.kind === "reservation" || ref?.kind === "customer" || ref?.kind === "worker" || ref?.kind === "apartment") {
        const table = ref.kind === "reservation" ? "reservations" : ref.kind === "customer" ? "customers" : ref.kind === "worker" ? "users" : "apartments";
        const row = await (ctx.db.query(table as "reservations") as any).withIndex("by_code", (q: any) => q.eq("code", ref.code)).unique();
        if (row) { refId = row._id; refField = ref.kind === "reservation" ? "reservationId" : ref.kind === "customer" ? "customerId" : ref.kind === "worker" ? "userId" : "apartmentId"; refLabel = ref.code; } else none = true;
      } else if (ref && ["contract", "payment", "expense", "commission", "maintenance", "document", "incident"].includes(ref.kind)) { labelFilter = ref.code; refLabel = ref.code; }
    }
    const fromMs = sp.from ? Date.parse(sp.from + "T00:00:00Z") : null;
    const toMs = sp.to ? Date.parse(addDaysKey(sp.to, 1) + "T00:00:00Z") : null;
    let base: Doc<"auditLog">[];
    if (none) base = [];
    else if (codeFilter) base = (await ctx.db.query("auditLog").withIndex("by_code", (q) => q.eq("code", codeFilter!)).collect());
    else if (refField === "reservationId") base = await ctx.db.query("auditLog").withIndex("by_reservation", (q) => q.eq("reservationId", refId as any)).collect();
    else if (refField === "customerId") base = await ctx.db.query("auditLog").withIndex("by_customer", (q) => q.eq("customerId", refId as any)).collect();
    else if (refField === "apartmentId") base = await ctx.db.query("auditLog").withIndex("by_apartment", (q) => q.eq("apartmentId", refId as any)).collect();
    else if (refField === "userId") base = await ctx.db.query("auditLog").withIndex("by_user_at", (q) => q.eq("userId", refId as any)).collect();
    else if (sp.worker) base = await ctx.db.query("auditLog").withIndex("by_user_at", (q) => q.eq("userId", sp.worker as any)).order("desc").take(2000);
    else if (sp.action) base = await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", sp.action!)).order("desc").take(2000);
    else if (sp.reservation) base = await ctx.db.query("auditLog").withIndex("by_reservation", (q) => q.eq("reservationId", sp.reservation as any)).collect();
    else if (sp.customer) base = await ctx.db.query("auditLog").withIndex("by_customer", (q) => q.eq("customerId", sp.customer as any)).collect();
    else if (sp.apartment) base = await ctx.db.query("auditLog").withIndex("by_apartment", (q) => q.eq("apartmentId", sp.apartment as any)).collect();
    else if (sp.severity) base = await ctx.db.query("auditLog").withIndex("by_severity_at", (q) => q.eq("severity", sp.severity!)).order("desc").take(2000);
    else base = await ctx.db.query("auditLog").withIndex("by_at").order("desc").take(fromMs || toMs ? 5000 : 1500);
    if (refId) { const extra = await ctx.db.query("auditLog").withIndex("by_entity").collect().then((x) => x.filter((a) => a.entityId === refId)); const seen = new Set(base.map((b) => b._id)); for (const e of extra) if (!seen.has(e._id)) base.push(e); }
    const lower = sp.q?.toLowerCase();
    const rows = base
      .filter((a) => (!sp.worker || a.userId === sp.worker) && (!sp.action || a.action === sp.action) && (!sp.module || a.module === sp.module) && (!sp.severity || a.severity === sp.severity) && (!sp.entity || a.entityType === sp.entity) && (!sp.apartment || a.apartmentId === sp.apartment) && (!sp.reservation || a.reservationId === sp.reservation) && (!sp.customer || a.customerId === sp.customer) && (!sp.device || (a.device ?? "").toLowerCase().includes(sp.device.toLowerCase())) && (fromMs === null || a.at >= fromMs) && (toMs === null || a.at < toMs) && (!labelFilter || (a.entityLabel ?? "").includes(labelFilter)) && (!lower || refId || codeFilter || labelFilter || (a.entityLabel ?? "").toLowerCase().includes(lower) || a.userName.toLowerCase().includes(lower) || (a.reason ?? "").toLowerCase().includes(lower) || a.code.toLowerCase().includes(lower)))
      .sort((a, b) => b.at - a.at)
      .slice(0, 500);
    const focus = sp.event ? await ctx.db.get(sp.event as any).catch(() => null) : null;
    const all = focus && "action" in (focus as any) && !rows.some((l) => l._id === (focus as any)._id) ? [focus as Doc<"auditLog">, ...rows] : rows;
    const workers = (await ctx.db.query("users").collect()).filter((u) => !u.deletedAt && u.roleId).map((u) => ({ id: u._id, fullName: u.fullName ?? "" })).sort((a, b) => a.fullName.localeCompare(b.fullName));
    const apartments = (await ctx.db.query("apartments").collect()).filter((a) => !a.deletedAt).map((a) => ({ id: a._id, code: a.code })).sort((a, b) => a.code.localeCompare(b.code));
    const sample = await ctx.db.query("auditLog").withIndex("by_at").order("desc").take(3000);
    const modules = [...new Set(sample.map((a) => a.module))].sort();
    const entityTypes = [...new Set(sample.map((a) => a.entityType).filter((x): x is string => !!x))].sort();
    return { rows: all.map((l) => ({ ...withId(l), createdAt: l.at })), workers, apartments, modules, entityTypes, refLabel };
  },
});

/** Recent activity stream for entity pages and the dashboard (reactive). */
export const recent = query({
  args: { limit: v.optional(v.number()), apartmentId: v.optional(v.id("apartments")), reservationId: v.optional(v.id("reservations")), customerId: v.optional(v.id("customers")) },
  returns: v.array(v.any()),
  handler: async (ctx, { limit, apartmentId, reservationId, customerId }) => {
    await assertPermission(ctx, "audit.view", "financials.view_revenue", "apartments.view");
    const n = limit ?? 25;
    const rows = apartmentId ? await ctx.db.query("auditLog").withIndex("by_apartment", (q) => q.eq("apartmentId", apartmentId)).order("desc").take(n) : reservationId ? await ctx.db.query("auditLog").withIndex("by_reservation", (q) => q.eq("reservationId", reservationId)).order("desc").take(n) : customerId ? await ctx.db.query("auditLog").withIndex("by_customer", (q) => q.eq("customerId", customerId)).order("desc").take(n) : (await ctx.db.query("auditLog").withIndex("by_at").order("desc").take(n * 3)).filter((a) => !["LOGIN", "LOGOUT", "LOGIN_FAILED"].includes(a.action)).slice(0, n);
    return rows.map((l) => ({ ...withId(l), createdAt: l.at }));
  },
});
