import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import * as argon2 from "argon2";
import { Prisma } from "@jorgecuadros/database";
import type { User } from "@jorgecuadros/database";
import { PrismaService } from "../prisma/prisma.service";
import { CreateUserDto } from "./create-user.dto";
import { UpdateUserDto } from "./update-user.dto";

/** Shape returned to the UI — never carries passwordHash. */
const safeSelect = {
  id: true,
  name: true,
  email: true,
  role: true,
  active: true,
  uiScale: true,
  createdAt: true,
  updatedAt: true,
} satisfies Prisma.UserSelect;

export type SafeUserRow = Prisma.UserGetPayload<{ select: typeof safeSelect }>;

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  // --- used by auth (need the hash / full row) -----------------------------

  findByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { email } });
  }

  findById(id: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { id } });
  }

  // --- admin CRUD (safe rows only) -----------------------------------------

  list(): Promise<SafeUserRow[]> {
    return this.prisma.user.findMany({
      orderBy: [{ active: "desc" }, { name: "asc" }],
      select: safeSelect,
    });
  }

  async create(dto: CreateUserDto): Promise<SafeUserRow> {
    const passwordHash = await argon2.hash(dto.password);
    try {
      return await this.prisma.user.create({
        data: {
          name: dto.name,
          email: dto.email,
          passwordHash,
          role: dto.role,
          active: dto.active ?? true,
        },
        select: safeSelect,
      });
    } catch (e) {
      throw this.mapError(e);
    }
  }

  /**
   * `actingUserId` is the admin making the change — used to stop an admin from
   * locking themselves out (deactivating or demoting their own account).
   */
  async update(
    id: string,
    dto: UpdateUserDto,
    actingUserId: string,
  ): Promise<SafeUserRow> {
    await this.ensureExists(id);

    if (id === actingUserId) {
      if (dto.active === false) {
        throw new BadRequestException("No puede desactivar su propia cuenta");
      }
      if (dto.role && dto.role !== "ADMIN") {
        throw new BadRequestException("No puede quitarse su propio rol de administrador");
      }
    }

    try {
      return await this.prisma.user.update({
        where: { id },
        data: {
          name: dto.name,
          email: dto.email,
          role: dto.role,
          active: dto.active,
        },
        select: safeSelect,
      });
    } catch (e) {
      throw this.mapError(e);
    }
  }

  /**
   * Self-service preference write — no ability check, because the only account
   * it can touch is the caller's own (the controller passes the session id).
   */
  updatePreferences(id: string, uiScale: number): Promise<SafeUserRow> {
    return this.prisma.user.update({
      where: { id },
      data: { uiScale },
      select: safeSelect,
    });
  }

  async resetPassword(id: string, password: string): Promise<SafeUserRow> {
    await this.ensureExists(id);
    const passwordHash = await argon2.hash(password);
    return this.prisma.user.update({
      where: { id },
      data: { passwordHash },
      select: safeSelect,
    });
  }

  /**
   * Hard-delete a user. The schema's ActivityLog.userId FK would otherwise
   * block the row (default `Restrict`), so null it out in the same
   * transaction. Rows + the actor id captured in the `message` JSON stay
   * intact for the audit trail.
   */
  async remove(id: string, actingUserId: string): Promise<void> {
    if (id === actingUserId) {
      throw new BadRequestException("No puede eliminar su propia cuenta");
    }
    await this.ensureExists(id);
    try {
      await this.prisma.$transaction([
        this.prisma.activityLog.updateMany({
          where: { userId: id },
          data: { userId: null },
        }),
        this.prisma.user.delete({ where: { id } }),
      ]);
    } catch (e) {
      throw this.mapError(e);
    }
  }

  private async ensureExists(id: string): Promise<void> {
    const found = await this.prisma.user.findUnique({ where: { id }, select: { id: true } });
    if (!found) throw new NotFoundException(`Usuario ${id} no encontrado`);
  }

  private mapError(e: unknown): Error {
    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
      return new ConflictException("Ya existe un usuario con ese correo");
    }
    return e as Error;
  }
}
