import {
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
  INVITATION_TTL_DAYS,
  type InvitationCreate,
  type InvitationResponse,
  type RoleName,
  type RolesResponse,
  type UserAdmin,
  type UsersResponse,
  type UserUpdate,
} from '@siop/shared';
import { randomBytes } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';

const userInclude = {
  role: true,
  teams: { orderBy: { name: 'asc' } },
} satisfies Prisma.UserInclude;

type UserRow = Prisma.UserGetPayload<{ include: typeof userInclude }>;

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

  async list(): Promise<UsersResponse> {
    const rows = await this.prisma.user.findMany({
      include: userInclude,
      orderBy: { displayName: 'asc' },
    });
    return { users: rows.map((u) => this.toDto(u)) };
  }

  async roles(): Promise<RolesResponse> {
    const roles = await this.prisma.role.findMany({ orderBy: { name: 'asc' } });
    return { roles: roles.map((r) => ({ id: r.id, name: r.name as RoleName })) };
  }

  /** Invitation : compte créé SANS mot de passe + lien d'activation 7 jours.
   *  L'envoi d'email viendra plus tard — le web affiche le lien à copier. */
  async invite(dto: InvitationCreate): Promise<InvitationResponse> {
    const role = await this.prisma.role.findUnique({ where: { id: dto.roleId } });
    if (!role) throw new NotFoundException('Rôle inconnu');
    try {
      const user = await this.prisma.user.create({
        data: {
          email: dto.email,
          displayName: dto.displayName,
          phone: dto.phone,
          roleId: dto.roleId,
          teams: dto.teamIds?.length
            ? { connect: dto.teamIds.map((id) => ({ id })) }
            : undefined,
          ...this.freshToken(),
        },
      });
      return {
        userId: user.id,
        activationToken: user.activationToken!,
        expiresAt: user.activationExpiresAt!.toISOString(),
      };
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException('Cet email a déjà un compte');
      }
      throw e;
    }
  }

  async resendInvitation(userId: string): Promise<InvitationResponse> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) throw new NotFoundException('Personne inconnue');
    if (user.passwordHash) {
      throw new ConflictException('Ce compte est déjà activé');
    }
    const updated = await this.prisma.user.update({
      where: { id: userId },
      data: this.freshToken(),
    });
    return {
      userId: updated.id,
      activationToken: updated.activationToken!,
      expiresAt: updated.activationExpiresAt!.toISOString(),
    };
  }

  async update(userId: string, dto: UserUpdate): Promise<UserAdmin> {
    try {
      const updated = await this.prisma.user.update({
        where: { id: userId },
        data: {
          displayName: dto.displayName,
          phone: dto.phone,
          roleId: dto.roleId,
          isActive: dto.isActive,
          hourlyRate: dto.hourlyRate,
          teams: dto.teamIds
            ? { set: dto.teamIds.map((id) => ({ id })) }
            : undefined,
        },
        include: userInclude,
      });
      return this.toDto(updated);
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
        throw new NotFoundException('Personne inconnue');
      }
      throw e;
    }
  }

  private freshToken() {
    return {
      activationToken: randomBytes(32).toString('base64url'),
      activationExpiresAt: new Date(
        Date.now() + INVITATION_TTL_DAYS * 24 * 60 * 60 * 1000,
      ),
    };
  }

  private toDto(row: UserRow): UserAdmin {
    return {
      id: row.id,
      email: row.email,
      displayName: row.displayName,
      phone: row.phone,
      role: { id: row.role.id, name: row.role.name as RoleName },
      teams: row.teams.map((t) => ({ id: t.id, name: t.name })),
      status: !row.isActive
        ? 'disabled'
        : row.passwordHash
          ? 'active'
          : 'invited',
      isDemo: row.isDemo,
      hourlyRate: row.hourlyRate === null ? null : Number(row.hourlyRate),
    };
  }
}
