import {
  PermissionOverrides,
  SystemRole,
  resolvePermissions,
  type ColumnPreferences,
} from "@capakraken/shared/types";
import {
  dashboardLayoutSchema,
  normalizeDashboardLayout,
} from "@capakraken/shared/schemas";
import { Prisma } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { adminProcedure, createTRPCRouter, managerProcedure, protectedProcedure, publicProcedure } from "../trpc.js";
import { createAuditEntry } from "../lib/audit.js";

export const userRouter = createTRPCRouter({
  /** Lightweight user list for task assignment (ADMIN + MANAGER) */
  listAssignable: managerProcedure.query(async ({ ctx }) => {
    return ctx.db.user.findMany({
      select: {
        id: true,
        name: true,
        email: true,
      },
      orderBy: { name: "asc" },
    });
  }),

  list: adminProcedure.query(async ({ ctx }) => {
    return ctx.db.user.findMany({
      select: {
        id: true,
        name: true,
        email: true,
        systemRole: true,
        createdAt: true,
        lastLoginAt: true,
        lastActiveAt: true,
        permissionOverrides: true,
        totpEnabled: true,
      },
      orderBy: { name: "asc" },
    });
  }),

  /** Count of users active in the last 5 minutes */
  activeCount: adminProcedure.query(async ({ ctx }) => {
    const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000);
    const count = await ctx.db.user.count({
      where: { lastActiveAt: { gte: fiveMinAgo } },
    });
    return { count };
  }),

  me: protectedProcedure.query(async ({ ctx }) => {
    const user = await findUniqueOrThrow(
      ctx.db.user.findUnique({
        where: { email: ctx.session.user?.email ?? "" },
        select: {
          id: true,
          name: true,
          email: true,
          systemRole: true,
          permissionOverrides: true,
          createdAt: true,
        },
      }),
      "User",
    );

    return user;
  }),

  create: adminProcedure
    .input(
      z.object({
        email: z.string().email(),
        name: z.string().min(1),
        systemRole: z.nativeEnum(SystemRole).default(SystemRole.USER),
        password: z.string().min(8),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.db.user.findUnique({ where: { email: input.email } });
      if (existing) {
        throw new TRPCError({ code: "CONFLICT", message: "User with this email already exists" });
      }

      const { hash } = await import("@node-rs/argon2");
      const passwordHash = await hash(input.password);

      const user = await ctx.db.user.create({
        data: {
          email: input.email,
          name: input.name,
          systemRole: input.systemRole,
          passwordHash,
        },
        select: { id: true, name: true, email: true, systemRole: true },
      });

      // Auto-link to a resource with matching email (if one exists and isn't already linked)
      const matchingResource = await ctx.db.resource.findFirst({
        where: { email: input.email, userId: null },
        select: { id: true },
      });
      if (matchingResource) {
        await ctx.db.resource.update({
          where: { id: matchingResource.id },
          data: { userId: user.id },
        });
      }

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: user.id,
        entityName: `${user.name} (${user.email})`,
        action: "CREATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        after: user as unknown as Record<string, unknown>,
        source: "ui",
      });

      return user;
    }),

  setPassword: adminProcedure
    .input(
      z.object({
        userId: z.string(),
        password: z.string().min(8, "Password must be at least 8 characters"),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const user = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true, name: true, email: true },
        }),
        "User",
      );

      const { hash } = await import("@node-rs/argon2");
      const passwordHash = await hash(input.password);

      await ctx.db.user.update({
        where: { id: input.userId },
        data: { passwordHash },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: user.id,
        entityName: `${user.name} (${user.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        source: "ui",
        summary: "Password reset by admin",
      });

      return { success: true };
    }),

  updateRole: adminProcedure
    .input(
      z.object({
        id: z.string(),
        systemRole: z.nativeEnum(SystemRole),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.id },
          select: { id: true, name: true, email: true, systemRole: true },
        }),
        "User",
      );

      const updated = await ctx.db.user.update({
        where: { id: input.id },
        data: { systemRole: input.systemRole },
        select: { id: true, name: true, email: true, systemRole: true },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: updated.id,
        entityName: `${updated.name} (${updated.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        before: before as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Changed role from ${before.systemRole} to ${updated.systemRole}`,
      });

      return updated;
    }),

  updateName: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1, "Name is required").max(200),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.id },
          select: { id: true, name: true, email: true },
        }),
        "User",
      );

      const updated = await ctx.db.user.update({
        where: { id: input.id },
        data: { name: input.name },
        select: { id: true, name: true, email: true },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: updated.id,
        entityName: `${updated.name} (${updated.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        before: before as unknown as Record<string, unknown>,
        after: updated as unknown as Record<string, unknown>,
        source: "ui",
        summary: `Changed name from "${before.name}" to "${updated.name}"`,
      });

      return updated;
    }),

  // ─── Resource Linking ──────────────────────────────────────────────────

  linkResource: adminProcedure
    .input(z.object({ userId: z.string(), resourceId: z.string().nullable() }))
    .mutation(async ({ ctx, input }) => {
      await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true },
        }),
        "User",
      );

      if (input.resourceId) {
        await findUniqueOrThrow(
          ctx.db.resource.findUnique({
            where: { id: input.resourceId },
            select: { id: true },
          }),
          "Resource",
        );

        // Unlink any resource previously linked to this user
        await ctx.db.resource.updateMany({
          where: { userId: input.userId },
          data: { userId: null },
        });
        // Link the new resource
        await ctx.db.resource.update({
          where: { id: input.resourceId },
          data: { userId: input.userId },
        });
      } else {
        // Unlink
        await ctx.db.resource.updateMany({
          where: { userId: input.userId },
          data: { userId: null },
        });
      }
      return { success: true };
    }),

  autoLinkAllByEmail: adminProcedure.mutation(async ({ ctx }) => {
    // Find all users without a linked resource, then match by email
    const unlinkedUsers = await ctx.db.user.findMany({
      where: { resource: null },
      select: { id: true, email: true },
    });

    let linked = 0;
    for (const user of unlinkedUsers) {
      const resource = await ctx.db.resource.findFirst({
        where: { email: user.email, userId: null },
        select: { id: true },
      });
      if (resource) {
        await ctx.db.resource.update({
          where: { id: resource.id },
          data: { userId: user.id },
        });
        linked++;
      }
    }
    return { linked, checked: unlinkedUsers.length };
  }),

  getDashboardLayout: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.db.user.findUnique({
      where: { email: ctx.session.user?.email ?? "" },
      select: { dashboardLayout: true, updatedAt: true },
    });
    return {
      layout: user?.dashboardLayout ? normalizeDashboardLayout(user.dashboardLayout) : null,
      updatedAt: user?.updatedAt ?? null,
    };
  }),

  saveDashboardLayout: protectedProcedure
    .input(z.object({ layout: dashboardLayoutSchema }))
    .mutation(async ({ ctx, input }) => {
      const updated = await ctx.db.user.update({
        where: { email: ctx.session.user?.email ?? "" },
        data: { dashboardLayout: input.layout as unknown as import("@capakraken/db").Prisma.InputJsonValue },
        select: { updatedAt: true },
      });
      return { updatedAt: updated.updatedAt };
    }),

  // ─── Favorite Projects ──────────────────────────────────────────────────
  getFavoriteProjectIds: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.db.user.findUnique({
      where: { id: ctx.dbUser!.id },
      select: { favoriteProjectIds: true },
    });
    return ((user?.favoriteProjectIds as string[] | null) ?? []) as string[];
  }),

  toggleFavoriteProject: protectedProcedure
    .input(z.object({ projectId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const user = await ctx.db.user.findUnique({
        where: { id: ctx.dbUser!.id },
        select: { favoriteProjectIds: true },
      });
      const current = ((user?.favoriteProjectIds as string[] | null) ?? []) as string[];
      const next = current.includes(input.projectId)
        ? current.filter((id) => id !== input.projectId)
        : [...current, input.projectId];
      await ctx.db.user.update({
        where: { id: ctx.dbUser!.id },
        data: { favoriteProjectIds: next as unknown as Prisma.InputJsonValue },
      });
      return { favoriteProjectIds: next, added: !current.includes(input.projectId) };
    }),

  setPermissions: adminProcedure
    .input(
      z.object({
        userId: z.string(),
        overrides: z
          .object({
            granted: z.array(z.string()).optional(),
            denied: z.array(z.string()).optional(),
            chapterIds: z.array(z.string()).optional(),
          })
          .nullable(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true, name: true, email: true, permissionOverrides: true },
        }),
        "User",
      );

      const user = await ctx.db.user.update({
        where: { id: input.userId },
        data: { permissionOverrides: input.overrides ?? Prisma.DbNull },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: input.userId,
        entityName: `${before.name} (${before.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        before: { permissionOverrides: before.permissionOverrides } as unknown as Record<string, unknown>,
        after: { permissionOverrides: input.overrides } as unknown as Record<string, unknown>,
        source: "ui",
        summary: input.overrides
          ? `Set permission overrides (granted: ${input.overrides.granted?.length ?? 0}, denied: ${input.overrides.denied?.length ?? 0})`
          : "Cleared permission overrides",
      });

      return user;
    }),

  resetPermissions: adminProcedure
    .input(z.object({ userId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const before = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true, name: true, email: true, permissionOverrides: true },
        }),
        "User",
      );

      const updated = await ctx.db.user.update({
        where: { id: input.userId },
        data: { permissionOverrides: Prisma.DbNull },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: input.userId,
        entityName: `${before.name} (${before.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        before: { permissionOverrides: before.permissionOverrides } as unknown as Record<string, unknown>,
        after: { permissionOverrides: null } as unknown as Record<string, unknown>,
        source: "ui",
        summary: "Reset permission overrides to role defaults",
      });

      return updated;
    }),

  getColumnPreferences: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.db.user.findUnique({
      where: { id: ctx.dbUser!.id },
      select: { columnPreferences: true },
    });
    return (user?.columnPreferences ?? {}) as ColumnPreferences;
  }),

  setColumnPreferences: protectedProcedure
    .input(z.object({
      view:    z.enum(["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"]),
      visible: z.array(z.string()).optional(),
      sort:    z.object({ field: z.string(), dir: z.enum(["asc", "desc"]) }).nullable().optional(),
      rowOrder: z.array(z.string()).nullable().optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.db.user.findUnique({
        where: { id: ctx.dbUser!.id },
        select: { columnPreferences: true },
      });
      const prefs = (existing?.columnPreferences ?? {}) as ColumnPreferences;
      const prev = (prefs[input.view] as import("@capakraken/shared").ViewPreferences | undefined) ?? { visible: [] };

      // Merge: only overwrite fields that were explicitly provided
      const merged: import("@capakraken/shared").ViewPreferences = {
        visible: input.visible ?? prev.visible,
      };
      // sort: null = clear, undefined = keep existing, value = set
      if (input.sort !== null && input.sort !== undefined) {
        merged.sort = input.sort;
      } else if (input.sort === undefined && prev.sort != null) {
        merged.sort = prev.sort;
      }
      // rowOrder: null = clear, undefined = keep existing, value = set
      if (input.rowOrder !== null && input.rowOrder !== undefined) {
        merged.rowOrder = input.rowOrder;
      } else if (input.rowOrder === undefined && prev.rowOrder != null) {
        merged.rowOrder = prev.rowOrder;
      }

      prefs[input.view] = merged;
      await ctx.db.user.update({
        where: { id: ctx.dbUser!.id },
        data: { columnPreferences: prefs as Prisma.InputJsonValue },
      });
      return { ok: true };
    }),

  getEffectivePermissions: adminProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      const user = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { systemRole: true, permissionOverrides: true },
        }),
        "User",
      );
      const permissions = resolvePermissions(
        user.systemRole as SystemRole,
        user.permissionOverrides as PermissionOverrides | null,
      );
      return {
        systemRole: user.systemRole,
        effectivePermissions: Array.from(permissions),
        overrides: user.permissionOverrides as PermissionOverrides | null,
      };
    }),

  // ─── TOTP / MFA ─────────────────────────────────────────────────────────────

  /** Generate a new TOTP secret for the current user (not yet enabled). */
  generateTotpSecret: protectedProcedure.mutation(async ({ ctx }) => {
    const { TOTP, Secret } = await import("otpauth");
    const secret = new Secret({ size: 20 });
    const totp = new TOTP({
      issuer: "CapaKraken",
      label: ctx.session.user?.email ?? ctx.dbUser!.id,
      algorithm: "SHA1",
      digits: 6,
      period: 30,
      secret,
    });

    // Store the secret (not yet enabled)
    await ctx.db.user.update({
      where: { id: ctx.dbUser!.id },
      data: { totpSecret: secret.base32 },
    });

    const uri = totp.toString();
    return { secret: secret.base32, uri };
  }),

  /** Verify a TOTP token and enable MFA for the current user. */
  verifyAndEnableTotp: protectedProcedure
    .input(z.object({ token: z.string().length(6) }))
    .mutation(async ({ ctx, input }) => {
      const user = await ctx.db.user.findUniqueOrThrow({
        where: { id: ctx.dbUser!.id },
        select: { id: true, name: true, email: true, totpSecret: true, totpEnabled: true },
      });

      if (!user.totpSecret) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "No TOTP secret generated. Call generateTotpSecret first." });
      }
      if (user.totpEnabled) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "TOTP is already enabled." });
      }

      const { TOTP, Secret } = await import("otpauth");
      const totp = new TOTP({
        issuer: "CapaKraken",
        label: user.email,
        algorithm: "SHA1",
        digits: 6,
        period: 30,
        secret: Secret.fromBase32(user.totpSecret),
      });

      const delta = totp.validate({ token: input.token, window: 1 });
      if (delta === null) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid TOTP token." });
      }

      await ctx.db.user.update({
        where: { id: user.id },
        data: { totpEnabled: true },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: user.id,
        entityName: `${user.name} (${user.email})`,
        action: "UPDATE",
        userId: user.id,
        source: "ui",
        summary: "Enabled TOTP MFA",
      });

      return { enabled: true };
    }),

  /** Admin override: disable TOTP for a specific user. */
  disableTotp: adminProcedure
    .input(z.object({ userId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const user = await findUniqueOrThrow(
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true, name: true, email: true, totpEnabled: true },
        }),
        "User",
      );

      await ctx.db.user.update({
        where: { id: input.userId },
        data: { totpEnabled: false, totpSecret: null },
      });

      void createAuditEntry({
        db: ctx.db,
        entityType: "User",
        entityId: user.id,
        entityName: `${user.name} (${user.email})`,
        action: "UPDATE",
        ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
        source: "ui",
        summary: "Disabled TOTP MFA (admin override)",
      });

      return { disabled: true };
    }),

  /** Verify a TOTP token (used during the login flow — public procedure). */
  verifyTotp: publicProcedure
    .input(z.object({ userId: z.string(), token: z.string().length(6) }))
    .mutation(async ({ ctx, input }) => {
      const user = await ctx.db.user.findUniqueOrThrow({
        where: { id: input.userId },
        select: { id: true, totpSecret: true, totpEnabled: true },
      });

      if (!user.totpEnabled || !user.totpSecret) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "TOTP is not enabled for this user." });
      }

      const { TOTP, Secret } = await import("otpauth");
      const totp = new TOTP({
        issuer: "CapaKraken",
        label: user.id,
        algorithm: "SHA1",
        digits: 6,
        period: 30,
        secret: Secret.fromBase32(user.totpSecret),
      });

      const delta = totp.validate({ token: input.token, window: 1 });
      if (delta === null) {
        throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid TOTP token." });
      }

      return { valid: true };
    }),

  /** Get MFA status for the current user. */
  getMfaStatus: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.db.user.findUniqueOrThrow({
      where: { id: ctx.dbUser!.id },
      select: { totpEnabled: true },
    });
    return { totpEnabled: user.totpEnabled };
  }),
});
