import { Prisma } from "@capakraken/db";
import { PermissionOverrides, SystemRole, resolvePermissions } from "@capakraken/shared/types";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js";
import type { TRPCContext } from "../trpc.js";

export const CreateUserInputSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
  systemRole: z.nativeEnum(SystemRole).default(SystemRole.USER),
  password: z.string().min(8),
});

export const SetUserPasswordInputSchema = z.object({
  userId: z.string(),
  password: z.string().min(8, "Password must be at least 8 characters"),
});

export const UpdateUserRoleInputSchema = z.object({
  id: z.string(),
  systemRole: z.nativeEnum(SystemRole),
});

export const UpdateUserNameInputSchema = z.object({
  id: z.string(),
  name: z.string().min(1, "Name is required").max(200),
});

export const LinkUserResourceInputSchema = z.object({
  userId: z.string(),
  resourceId: z.string().nullable(),
});

export const SetUserPermissionsInputSchema = 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(),
});

export const UserIdInputSchema = z.object({
  userId: z.string(),
});

type UserReadContext = Pick<TRPCContext, "db" | "dbUser">;
type UserMutationContext = UserReadContext;

function withAuditUser(userId: string | undefined) {
  return userId ? { userId } : {};
}

export async function listAssignableUsers(ctx: UserReadContext) {
  return ctx.db.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
    },
    orderBy: { name: "asc" },
  });
}

export async function listUsers(ctx: UserReadContext) {
  return ctx.db.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      systemRole: true,
      createdAt: true,
      lastLoginAt: true,
      lastActiveAt: true,
      permissionOverrides: true,
      totpEnabled: true,
      isActive: true,
    },
    orderBy: { name: "asc" },
  });
}

export async function countActiveUsers(ctx: UserReadContext) {
  const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000);
  const count = await ctx.db.user.count({
    where: { lastActiveAt: { gte: fiveMinAgo } },
  });
  return { count };
}

export async function getCurrentUserProfile(ctx: UserReadContext) {
  return findUniqueOrThrow(
    ctx.db.user.findUnique({
      where: { id: ctx.dbUser!.id },
      select: {
        id: true,
        name: true,
        email: true,
        systemRole: true,
        permissionOverrides: true,
        createdAt: true,
      },
    }),
    "User",
  );
}

export async function createUser(
  ctx: UserMutationContext,
  input: z.infer<typeof CreateUserInputSchema>,
) {
  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 },
  });

  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",
    ...withAuditUser(ctx.dbUser?.id),
    after: user as unknown as Record<string, unknown>,
    source: "ui",
  });

  return user;
}

export async function setUserPassword(
  ctx: UserMutationContext,
  input: z.infer<typeof SetUserPasswordInputSchema>,
) {
  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",
    ...withAuditUser(ctx.dbUser?.id),
    source: "ui",
    summary: "Password reset by admin",
  });

  return { success: true };
}

export async function updateUserRole(
  ctx: UserMutationContext,
  input: z.infer<typeof UpdateUserRoleInputSchema>,
) {
  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",
    ...withAuditUser(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;
}

export async function updateUserName(
  ctx: UserMutationContext,
  input: z.infer<typeof UpdateUserNameInputSchema>,
) {
  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",
    ...withAuditUser(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;
}

export async function linkUserResource(
  ctx: UserMutationContext,
  input: z.infer<typeof LinkUserResourceInputSchema>,
) {
  await findUniqueOrThrow(
    ctx.db.user.findUnique({
      where: { id: input.userId },
      select: { id: true },
    }),
    "User",
  );

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

    if (resource.userId && resource.userId !== input.userId) {
      throw new TRPCError({
        code: "CONFLICT",
        message: "Resource is already linked to another user",
      });
    }

    await ctx.db.resource.updateMany({
      where: {
        userId: input.userId,
        NOT: { id: input.resourceId },
      },
      data: { userId: null },
    });

    const linkResult = await ctx.db.resource.updateMany({
      where: {
        id: input.resourceId,
        OR: [
          { userId: null },
          { userId: input.userId },
        ],
      },
      data: { userId: input.userId },
    });

    if (linkResult.count !== 1) {
      const [userStillExists, resourceStillExists] = await Promise.all([
        ctx.db.user.findUnique({
          where: { id: input.userId },
          select: { id: true },
        }),
        ctx.db.resource.findUnique({
          where: { id: input.resourceId },
          select: { id: true, userId: true },
        }),
      ]);

      if (!userStillExists) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      if (!resourceStillExists) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "Resource not found",
        });
      }

      if (resourceStillExists.userId && resourceStillExists.userId !== input.userId) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "Resource is already linked to another user",
        });
      }

      throw new TRPCError({
        code: "CONFLICT",
        message: "Resource link changed during update. Please retry.",
      });
    }
  } else {
    await ctx.db.resource.updateMany({
      where: { userId: input.userId },
      data: { userId: null },
    });
  }

  return { success: true };
}

export async function autoLinkUsersByEmail(ctx: UserMutationContext) {
  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 };
}

export async function setUserPermissions(
  ctx: UserMutationContext,
  input: z.infer<typeof SetUserPermissionsInputSchema>,
) {
  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",
    ...withAuditUser(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;
}

export async function resetUserPermissions(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  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",
    ...withAuditUser(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;
}

export async function getEffectiveUserPermissions(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  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,
  };
}

export async function deactivateUser(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  if (ctx.dbUser!.id === input.userId) {
    throw new TRPCError({ code: "BAD_REQUEST", message: "You cannot deactivate your own account." });
  }

  const user = await findUniqueOrThrow(
    ctx.db.user.findUnique({
      where: { id: input.userId },
      select: { id: true, name: true, email: true, isActive: true },
    }),
    "User",
  );

  if (!user.isActive) {
    throw new TRPCError({ code: "BAD_REQUEST", message: "User is already inactive." });
  }

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

  // Invalidate all existing sessions so the user is logged out immediately
  await ctx.db.activeSession.deleteMany({ where: { userId: input.userId } });

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

  return { success: true };
}

export async function reactivateUser(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  const user = await findUniqueOrThrow(
    ctx.db.user.findUnique({
      where: { id: input.userId },
      select: { id: true, name: true, email: true, isActive: true },
    }),
    "User",
  );

  if (user.isActive) {
    throw new TRPCError({ code: "BAD_REQUEST", message: "User is already active." });
  }

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

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

  return { success: true };
}

export async function deleteUser(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  if (ctx.dbUser!.id === input.userId) {
    throw new TRPCError({ code: "BAD_REQUEST", message: "You cannot delete your own account." });
  }

  const user = await findUniqueOrThrow(
    ctx.db.user.findUnique({
      where: { id: input.userId },
      select: { id: true, name: true, email: true },
    }),
    "User",
  );

  // These tables have required (non-nullable) FKs to User — must be removed first
  await ctx.db.vacation.deleteMany({ where: { requestedById: input.userId } });
  await ctx.db.notificationBroadcast.deleteMany({ where: { senderId: input.userId } });
  await ctx.db.inviteToken.deleteMany({ where: { createdById: input.userId } });

  // Unlink resource (nullable FK — belt-and-suspenders)
  await ctx.db.resource.updateMany({ where: { userId: input.userId }, data: { userId: null } });

  void createAuditEntry({
    db: ctx.db,
    entityType: "User",
    entityId: user.id,
    entityName: `${user.name} (${user.email})`,
    action: "DELETE",
    ...withAuditUser(ctx.dbUser?.id),
    source: "ui",
    summary: "User account permanently deleted",
  });

  // Delete user — Prisma cascade covers: Account, Session, ActiveSession,
  // Notification, ReportTemplate, AssistantApproval, Comment.
  // Nullable FKs (AuditLog.userId, etc.) become NULL via Prisma SET NULL default.
  await ctx.db.user.delete({ where: { id: input.userId } });

  return { success: true };
}

export async function disableTotp(
  ctx: UserMutationContext,
  input: z.infer<typeof UserIdInputSchema>,
) {
  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",
    ...withAuditUser(ctx.dbUser?.id),
    source: "ui",
    summary: "Disabled TOTP MFA (admin override)",
  });

  return { disabled: true };
}
