import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { getAuthSessionId } from "@convex-dev/auth/server";
import { assertPermission, actorLite, AppError, requireActor, can, currentActor, revokeUserSessions } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { sessionsOf } from "./workers";
import { getSettings } from "./lib/settings";
import { todayKey } from "./lib/days";

const clientArgs = v.optional(v.object({ ip: v.optional(v.string()), device: v.optional(v.string()), browser: v.optional(v.string()), os: v.optional(v.string()), userAgent: v.optional(v.string()) }));

/** Called by the web layer right after a successful sign-in with the device context of the request. */
export const recordLogin = mutation({
  args: { client: clientArgs },
  returns: v.null(),
  handler: async (ctx, { client }) => {
    const actor = await requireActor(ctx);
    const sessionId = await getAuthSessionId(ctx);
    const now = Date.now();
    if (sessionId) {
      const existing = await ctx.db.query("sessionMeta").withIndex("by_session", (q) => q.eq("sessionId", sessionId)).unique();
      if (!existing) await ctx.db.insert("sessionMeta", { sessionId, userId: actor.id, device: client?.device, browser: client?.browser, os: client?.os, ipAddress: client?.ip, userAgent: client?.userAgent, lastSeenAt: now });
    }
    await ctx.db.patch(actor.id, { lastLoginAt: now, lastSeenAt: now });
    await ctx.db.insert("loginHistory", { userId: actor.id, email: actor.user.email ?? "", success: true, ipAddress: client?.ip, userAgent: client?.userAgent, device: client?.device, browser: client?.browser, at: now });
    await audit(ctx, actorLite(actor), { action: "LOGIN", module: "auth", entityType: "user", entityId: actor.id, entityLabel: actor.fullName, client: { ip: client?.ip, device: client?.device, browser: client?.browser } });
    // New device? (no previous successful login from the same device+browser)
    const prior = await ctx.db.query("loginHistory").withIndex("by_user_at", (q) => q.eq("userId", actor.id)).order("desc").take(60);
    const known = prior.filter((l) => l.success && l.at < now).some((l) => l.device === client?.device && l.browser === client?.browser);
    if (prior.filter((l) => l.success).length > 1 && !known && client?.device) await notify(ctx, { type: "SECURITY_ALERT", title: "Sign-in from a new device", body: `${actor.fullName} signed in from ${client.device} · ${client.browser ?? ""}${client.ip ? ` (${client.ip})` : ""}.`, href: `/workers/${actor.id}?tab=security`, actorId: actor.id, priority: "HIGH" });
    return null;
  },
});

/** Unauthenticated: failed attempt bookkeeping (throttled per email to 30 rows / hour). */
export const recordFailedLogin = mutation({
  args: { email: v.string(), reason: v.optional(v.string()), client: clientArgs },
  returns: v.null(),
  handler: async (ctx, { email, reason, client }) => {
    const e = email.toLowerCase().trim().slice(0, 120);
    const now = Date.now();
    const recent = await ctx.db.query("loginHistory").withIndex("by_email_at", (q) => q.eq("email", e).gte("at", now - 3_600_000)).collect();
    if (recent.length >= 30) return null;
    const user = await ctx.db.query("users").withIndex("email", (q) => q.eq("email", e)).unique();
    await ctx.db.insert("loginHistory", { userId: user?._id, email: e, success: false, reason: reason?.slice(0, 120), ipAddress: client?.ip, userAgent: client?.userAgent, device: client?.device, browser: client?.browser, at: now });
    const failures = recent.filter((l) => !l.success).length + 1;
    if (failures === 3 || failures === 10) await notify(ctx, { type: "SECURITY_ALERT", title: "Failed login attempts", body: `${failures} failed login attempts for ${e}${client?.device ? ` from ${client.device}` : ""}.`, href: "/security", priority: "CRITICAL" });
    return null;
  },
});

/** Is this account currently throttled? (5 failures in 15 minutes) */
export const loginThrottle = query({
  args: { email: v.string() },
  returns: v.object({ blocked: v.boolean(), retryAfterSeconds: v.number() }),
  handler: async (ctx, { email }) => {
    const e = email.toLowerCase().trim();
    const window = 15 * 60_000;
    const recent = (await ctx.db.query("loginHistory").withIndex("by_email_at", (q) => q.eq("email", e).gte("at", Date.now() - window)).collect()).filter((l) => !l.success);
    if (recent.length < 5) return { blocked: false, retryAfterSeconds: 0 };
    const oldest = Math.min(...recent.map((l) => l.at));
    return { blocked: true, retryAfterSeconds: Math.max(1, Math.ceil((oldest + window - Date.now()) / 1000)) };
  },
});

export const recordLogout = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const actor = await currentActor(ctx);
    if (!actor) return null;
    await audit(ctx, actorLite(actor), { action: "LOGOUT", module: "auth", entityType: "user", entityId: actor.id, entityLabel: actor.fullName });
    return null;
  },
});

/** Presence heartbeat — the client calls this at most once a minute while the tab is visible. */
export const heartbeat = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const actor = await currentActor(ctx);
    if (!actor) return null;
    const now = Date.now();
    // Every write here invalidates the queries that read these rows, so each one
    // is throttled well inside the 3-minute window that counts as "online".
    if ((actor.user.lastSeenAt ?? 0) < now - 90_000) await ctx.db.patch(actor.id, { lastSeenAt: now });
    const sessionId = await getAuthSessionId(ctx);
    if (sessionId) {
      const meta = await ctx.db.query("sessionMeta").withIndex("by_session", (q) => q.eq("sessionId", sessionId)).unique();
      if (meta && meta.lastSeenAt < now - 90_000) await ctx.db.patch(meta._id, { lastSeenAt: now });
    }
    // Shared desk signal for the public site: one row, at most one write a
    // minute no matter how many staff are online.
    const row = await ctx.db.query("presence").withIndex("by_key", (q) => q.eq("key", "staff")).unique();
    if (!row) await ctx.db.insert("presence", { key: "staff", lastSeenAt: now });
    else if (row.lastSeenAt < now - 60_000) await ctx.db.patch(row._id, { lastSeenAt: now });
    return null;
  },
});

/** The signed-in worker's own profile + effective permissions (drives the shell). */
export const me = query({
  args: {},
  returns: v.union(v.null(), v.any()),
  handler: async (ctx) => {
    const a = await currentActor(ctx);
    if (!a) return null;
    return { id: a.id, code: a.user.code ?? null, fullName: a.fullName, email: a.user.email ?? "", phone: a.user.phone ?? null, roleKey: a.roleKey, roleName: a.role.name, isAdmin: a.isAdmin, permissions: [...a.perms], locale: a.user.locale ?? "en", timezone: a.user.timezone ?? "Africa/Casablanca", twoFactorEnabled: !!a.user.twoFactorEnabled, lastLoginAt: a.user.lastLoginAt ?? null, passwordChangedAt: a.user.passwordChangedAt ?? null, sessionId: a.sessionId };
  },
});

export const mySessions = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    const a = await requireActor(ctx);
    return sessionsOf(ctx, a.id);
  },
});

export const revokeSession = mutation({
  args: { sessionId: v.id("authSessions") },
  returns: v.null(),
  handler: async (ctx, { sessionId }) => {
    const actor = await requireActor(ctx);
    const s = (await ctx.db.get(sessionId)) as { _id: Id<"authSessions">; userId: Id<"users"> } | null;
    if (!s) throw new AppError("Session not found", "NOT_FOUND");
    if (s.userId !== actor.id && !can(actor, "workers.activity") && !can(actor, "security.manage")) throw new AppError("Not allowed", "PERMISSION");
    const target = await ctx.db.get(s.userId);
    for (const t of await ctx.db.query("authRefreshTokens").withIndex("sessionId", (q) => q.eq("sessionId", sessionId)).collect()) await ctx.db.delete(t._id);
    await ctx.db.delete(sessionId);
    const meta = await ctx.db.query("sessionMeta").withIndex("by_session", (q) => q.eq("sessionId", sessionId)).unique();
    if (meta) await ctx.db.patch(meta._id, { revokedAt: Date.now() });
    await audit(ctx, actorLite(actor), { action: "SESSION_REVOKED", module: "auth", entityType: "user", entityId: s.userId, entityLabel: target?.fullName ?? "", newValue: { device: meta?.device ?? null, browser: meta?.browser ?? null } });
    return null;
  },
});

export const revokeAllSessions = mutation({
  args: { userId: v.id("users"), reason: v.optional(v.string()) },
  returns: v.object({ count: v.number() }),
  handler: async (ctx, { userId, reason }) => {
    const actor = await requireActor(ctx);
    if (userId !== actor.id && !can(actor, "security.manage") && !can(actor, "workers.activity")) throw new AppError("Not allowed", "PERMISSION");
    const w = await ctx.db.get(userId);
    if (!w) throw new AppError("Worker not found", "NOT_FOUND");
    const me = await getAuthSessionId(ctx);
    const count = await revokeUserSessions(ctx, userId, userId === actor.id && me ? [me] : []);
    await audit(ctx, actorLite(actor), { action: "SESSIONS_REVOKED_ALL", module: "security", entityType: "user", entityId: userId, entityLabel: w.fullName ?? "", newValue: { revoked: count }, reason, severity: "WARNING" });
    return { count };
  },
});

/** Security Center payload. */
export const center = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    await assertPermission(ctx, "security.view");
    const now = Date.now();
    const since14 = now - 14 * 86_400_000;
    const roles = await ctx.db.query("roles").collect();
    const users = (await ctx.db.query("users").collect()).filter((u) => u.roleId && !u.deletedAt);
    const sessions = (await Promise.all(users.map(async (u) => (await sessionsOf(ctx, u._id)).map((s) => ({ ...s, user: { id: u._id, fullName: u.fullName ?? "", code: u.code ?? null, role: roles.find((r) => r._id === u.roleId)?.name ?? "" } }))))).flat();
    const logins = await ctx.db.query("loginHistory").withIndex("by_at", (q) => q.gte("at", since14)).order("desc").take(2000);
    const permChanges = (await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", "PERMISSIONS_CHANGED")).order("desc").take(10)).concat(await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", "ROLE_EDITED")).order("desc").take(5), await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", "WORKER_STATUS_CHANGED")).order("desc").take(5), await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", "ACCOUNT_DISABLED")).order("desc").take(5)).sort((a, b) => b.at - a.at);
    const sensitive = await ctx.db.query("auditLog").withIndex("by_action_at", (q) => q.eq("action", "DOCUMENT_VIEWED")).order("desc").take(25);
    const events = (await ctx.db.query("auditLog").withIndex("by_severity_at", (q) => q.eq("severity", "WARNING")).order("desc").take(20)).concat(await ctx.db.query("auditLog").withIndex("by_severity_at", (q) => q.eq("severity", "CRITICAL")).order("desc").take(10), await ctx.db.query("auditLog").withIndex("by_module_action", (q) => q.eq("module", "security")).order("desc").take(15)).sort((a, b) => b.at - a.at).filter((e, i, arr) => arr.findIndex((x) => x._id === e._id) === i).slice(0, 30);
    return {
      sessions,
      logins: logins.map((l) => ({ ...l, id: l._id, user: users.find((u) => u._id === l.userId) ? { id: l.userId, fullName: users.find((u) => u._id === l.userId)!.fullName ?? "" } : null })),
      workers: users.map((u) => ({ id: u._id, fullName: u.fullName ?? "", code: u.code ?? null, status: u.status ?? "ACTIVE", twoFactorEnabled: !!u.twoFactorEnabled, passwordChangedAt: u.passwordChangedAt ?? null, lastLoginAt: u.lastLoginAt ?? null, lastSeenAt: u.lastSeenAt ?? null, role: { name: roles.find((r) => r._id === u.roleId)?.name ?? "", key: roles.find((r) => r._id === u.roleId)?.key ?? "" } })),
      permChanges: permChanges.map((a) => ({ ...a, id: a._id })),
      sensitiveAccess: sensitive.map((a) => ({ ...a, id: a._id })),
      securityEvents: events.map((a) => ({ ...a, id: a._id })),
    };
  },
});

/** Public: lets staff sign in with a username (the Password provider keys accounts by email). */
export const emailForIdentifier = query({
  args: { identifier: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, { identifier }) => {
    const id = identifier.trim().toLowerCase();
    if (!id) return null;
    if (id.includes("@")) return id;
    const u = await ctx.db.query("users").withIndex("by_username", (q) => q.eq("username", id)).unique();
    return u && !u.deletedAt ? (u.email ?? null) : null;
  },
});

/** Sidebar badges (reactive): unread notifications, tasks due, cleaning pending. */
export const shellBadges = query({
  args: {},
  returns: v.object({ notifications: v.number(), tasks: v.number(), cleaning: v.number() }),
  handler: async (ctx) => {
    const actor = await currentActor(ctx);
    if (!actor) return { notifications: 0, tasks: 0, cleaning: 0 };
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const notifications = (await ctx.db.query("notifications").withIndex("by_user_read", (q) => q.eq("userId", actor.id).eq("readAt", undefined)).take(100)).length;
    let tasks = 0;
    if (can(actor, "tasks.view")) {
      const open = actor.isAdmin ? (await ctx.db.query("tasks").withIndex("by_status", (q) => q.eq("status", "TODO")).collect()).concat(await ctx.db.query("tasks").withIndex("by_status", (q) => q.eq("status", "IN_PROGRESS")).collect()) : (await ctx.db.query("tasks").withIndex("by_assignee_status", (q) => q.eq("assigneeId", actor.id).eq("status", "TODO")).collect()).concat(await ctx.db.query("tasks").withIndex("by_assignee_status", (q) => q.eq("assigneeId", actor.id).eq("status", "IN_PROGRESS")).collect());
      tasks = open.filter((t) => t.dueDate && t.dueDate <= today).length;
    }
    const cleaning = can(actor, "cleaning.view") ? (await ctx.db.query("cleaningTasks").collect()).filter((c) => c.status === "NEEDS_CLEANING" || c.status === "IN_PROGRESS").length : 0;
    return { notifications, tasks, cleaning };
  },
});

/** The signed-in worker's own recent login attempts. */
export const myLogins = query({
  args: { limit: v.optional(v.number()) },
  returns: v.array(v.any()),
  handler: async (ctx, { limit }) => {
    const actor = await requireActor(ctx);
    return (await ctx.db.query("loginHistory").withIndex("by_user_at", (q) => q.eq("userId", actor.id)).order("desc").take(limit ?? 15)).map((l) => ({ ...l, id: l._id }));
  },
});
