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 { nextCode } from "./lib/seq";
import { DOCUMENT_CATEGORIES } from "../src/lib/domain";

const ALLOWED = ["image/jpeg", "image/png", "image/webp", "image/heic", "application/pdf"];

/** Step 1 of an upload: a short-lived signed URL for Convex file storage. */
export const generateUploadUrl = mutation({
  args: { customerId: v.optional(v.id("customers")) },
  returns: v.string(),
  handler: async (ctx, { customerId }) => {
    const actor = await requireActor(ctx);
    if (!(customerId ? can(actor, "customers.upload_documents") || can(actor, "documents.upload") : can(actor, "documents.upload"))) throw new AppError("You don't have permission to do this.", "PERMISSION");
    return ctx.storage.generateUploadUrl();
  },
});

/** Step 2: register the stored file as a document with a DOC reference. */
export const register = mutation({
  args: { storageId: v.id("_storage"), category: v.string(), fileName: v.string(), mimeType: v.string(), size: v.number(), customerId: v.optional(v.id("customers")), reservationId: v.optional(v.id("reservations")), apartmentId: v.optional(v.id("apartments")), expenseId: v.optional(v.id("expenses")), replacesId: v.optional(v.id("documents")) },
  returns: v.object({ id: v.id("documents"), code: v.string() }),
  handler: async (ctx, data) => {
    const actor = await requireActor(ctx);
    if (!(data.customerId ? can(actor, "customers.upload_documents") || can(actor, "documents.upload") : can(actor, "documents.upload"))) throw new AppError("You don't have permission to do this.", "PERMISSION");
    if (!DOCUMENT_CATEGORIES.includes(data.category as (typeof DOCUMENT_CATEGORIES)[number])) throw new AppError("Invalid category", "VALIDATION");
    if (!ALLOWED.includes(data.mimeType)) throw new AppError("Only JPG, PNG, WEBP, HEIC or PDF files are accepted", "VALIDATION");
    if (data.size > 10 * 1024 * 1024) throw new AppError("File too large (max 10 MB)", "VALIDATION");
    const meta = await ctx.db.system.get(data.storageId);
    if (!meta) throw new AppError("Upload not found", "NOT_FOUND");
    if (data.replacesId) {
      const prev = await ctx.db.get(data.replacesId);
      if (prev) await ctx.db.patch(prev._id, { deletedAt: Date.now() });
    }
    const code = await nextCode(ctx, "document");
    const id = await ctx.db.insert("documents", { code, category: data.category, fileName: data.fileName.slice(0, 120), storageId: data.storageId, mimeType: data.mimeType, size: data.size, customerId: data.customerId, reservationId: data.reservationId, apartmentId: data.apartmentId, expenseId: data.expenseId, uploadedById: actor.id, replacesId: data.replacesId, isSensitive: data.category === "ID_FRONT" || data.category === "ID_BACK" || data.category === "WORKER", at: Date.now() });
    await audit(ctx, actorLite(actor), { action: "DOCUMENT_UPLOADED", module: "documents", entityType: "document", entityId: id, entityLabel: `${code} · ${data.category.replace(/_/g, " ").toLowerCase()} · ${data.fileName}`, newValue: { category: data.category, size: data.size, replaces: data.replacesId ?? null }, customerId: data.customerId ?? null, reservationId: data.reservationId ?? null, apartmentId: data.apartmentId ?? null });
    return { id, code };
  },
});

export const remove = mutation({
  args: { id: v.id("documents"), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, reason }) => {
    const actor = await assertPermission(ctx, "documents.delete", "customers.delete_documents");
    const d = await ctx.db.get(id);
    if (!d || d.deletedAt) throw new AppError("Document not found", "NOT_FOUND");
    if (d.customerId && !can(actor, "customers.delete_documents") && !can(actor, "documents.delete")) throw new AppError("Not allowed", "PERMISSION");
    await ctx.db.patch(id, { deletedAt: Date.now() });
    await ctx.storage.delete(d.storageId);
    await audit(ctx, actorLite(actor), { action: "DOCUMENT_DELETED", module: "documents", entityType: "document", entityId: id, entityLabel: `${d.code} · ${d.fileName}`, previousValue: { category: d.category }, reason, customerId: d.customerId ?? null, reservationId: d.reservationId ?? null, apartmentId: d.apartmentId ?? null, severity: "WARNING" });
    return null;
  },
});

/** Authorised access for the private delivery route; returns a storage URL the web server proxies. */
export const access = query({
  args: { id: v.id("documents") },
  returns: v.union(v.null(), v.object({ url: v.union(v.string(), v.null()), fileName: v.string(), mimeType: v.string(), isSensitive: v.boolean() })),
  handler: async (ctx, { id }) => {
    const actor = await requireActor(ctx);
    const d = await ctx.db.get(id);
    if (!d || d.deletedAt) return null;
    const allowed = d.customerId ? can(actor, "customers.view_documents") || can(actor, "documents.view") : can(actor, "documents.view") || can(actor, "expenses.view") || can(actor, "apartments.view");
    if (!allowed) return null;
    return { url: await ctx.storage.getUrl(d.storageId), fileName: d.fileName, mimeType: d.mimeType, isSensitive: d.isSensitive };
  },
});

/** Sensitive-view ledger entry, deduplicated per viewer / document / 10 minutes. */
export const recordView = mutation({
  args: { id: v.id("documents"), client: v.optional(v.object({ ip: v.optional(v.string()), device: v.optional(v.string()), browser: v.optional(v.string()) })) },
  returns: v.null(),
  handler: async (ctx, { id, client }) => {
    const actor = await requireActor(ctx);
    const d = await ctx.db.get(id);
    if (!d || !d.isSensitive) return null;
    const recent = (await ctx.db.query("auditLog").withIndex("by_user_at", (q) => q.eq("userId", actor.id).gte("at", Date.now() - 10 * 60_000)).collect()).some((a) => a.action === "DOCUMENT_VIEWED" && a.entityId === id);
    if (!recent) await audit(ctx, actorLite(actor), { action: "DOCUMENT_VIEWED", module: "security", entityType: "document", entityId: id, entityLabel: `${d.code} ${d.category.replace(/_/g, " ").toLowerCase()} · ${d.fileName}`, customerId: d.customerId ?? null, reservationId: d.reservationId ?? null, client });
    return null;
  },
});

export const list = query({
  args: { limit: v.optional(v.number()) },
  returns: v.array(v.any()),
  handler: async (ctx, { limit }) => {
    const actor = await assertPermission(ctx, "documents.view", "contracts.view");
    if (!can(actor, "documents.view")) return [];
    const rows = (await ctx.db.query("documents").order("desc").take(limit ?? 300)).filter((d) => !d.deletedAt);
    return Promise.all(
      rows.map(async (d) => {
        const [c, r, a, by] = await Promise.all([d.customerId ? ctx.db.get(d.customerId) : null, d.reservationId ? ctx.db.get(d.reservationId) : null, d.apartmentId ? ctx.db.get(d.apartmentId) : null, ctx.db.get(d.uploadedById)]);
        return { id: d._id, code: d.code, category: d.category, fileName: d.fileName, mimeType: d.mimeType, size: d.size, at: d.at, isSensitive: d.isSensitive, customer: c ? { id: c._id, fullName: c.fullName } : null, reservation: r ? { id: r._id, code: r.code } : null, apartment: a ? { id: a._id, code: a.code } : null, uploadedBy: by?.fullName ?? "" };
      })
    );
  },
});

/** Documents page header numbers. */
export const stats = query({
  args: {},
  returns: v.object({ missingIds: v.number() }),
  handler: async (ctx) => {
    await assertPermission(ctx, "documents.view", "contracts.view");
    const ids = new Set<string>();
    for (const status of ["CONFIRMED", "CHECKED_IN"]) for (const r of await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", status)).collect()) ids.add(r.customerId);
    let missing = 0;
    for (const id of ids) {
      const c = await ctx.db.get(id as never as import("./_generated/dataModel").Id<"customers">);
      if (!c || c.deletedAt) continue;
      const docs = (await ctx.db.query("documents").withIndex("by_customer", (q) => q.eq("customerId", c._id)).collect()).filter((d) => !d.deletedAt && (d.category === "ID_FRONT" || d.category === "ID_BACK"));
      if (!docs.length) missing++;
    }
    return { missingIds: missing };
  },
});
