import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type {
  AuthResponse,
  DemoAccountsResponse,
  RoleName,
} from '@siop/shared';
import * as argon2 from 'argon2';
import { PrismaService } from '../prisma/prisma.service';

type UserWithRole = {
  id: string;
  email: string;
  displayName: string;
  isDemo: boolean;
  role: { id: string; name: string };
};

@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jwtService: JwtService,
  ) {}

  async login(email: string, password: string): Promise<AuthResponse> {
    const user = await this.prisma.user.findUnique({
      where: { email },
      include: { role: true },
    });
    // argon2.verify même sur compte inconnu = temps de réponse homogène
    const valid =
      user?.passwordHash != null &&
      (await argon2.verify(user.passwordHash, password));
    if (!user || !user.isActive || !valid) {
      throw new UnauthorizedException('Identifiants invalides');
    }
    return this.issueToken(user);
  }

  /** ADR-002 — liste publique en DEMO_MODE, jamais de secret. */
  async listDemoAccounts(): Promise<DemoAccountsResponse> {
    const users = await this.prisma.user.findMany({
      where: { isDemo: true, isActive: true },
      include: { role: true },
      orderBy: { createdAt: 'asc' },
    });
    return {
      accounts: users.map((u) => ({
        id: u.id,
        displayName: u.displayName,
        roleName: u.role.name as RoleName,
        initials: this.initials(u.displayName),
      })),
    };
  }

  /** ADR-002 — refuse tout compte isDemo=false, quoi qu'il arrive. */
  async demoLogin(userId: string): Promise<AuthResponse> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: { role: true },
    });
    if (!user || !user.isActive) throw new NotFoundException('Compte inconnu');
    if (!user.isDemo) {
      throw new ForbiddenException(
        'Ce compte n’est pas un compte de démonstration',
      );
    }
    return this.issueToken(user);
  }

  /** R1 — activation d'un compte invité : lien 7 jours, usage unique,
   *  choisit le mot de passe et connecte directement. */
  async activate(token: string, password: string): Promise<AuthResponse> {
    const user = await this.prisma.user.findUnique({
      where: { activationToken: token },
      include: { role: true },
    });
    if (
      !user ||
      !user.isActive ||
      !user.activationExpiresAt ||
      user.activationExpiresAt < new Date()
    ) {
      throw new BadRequestException(
        'Ce lien d’activation est invalide ou a expiré — demandez un nouveau lien',
      );
    }
    const activated = await this.prisma.user.update({
      where: { id: user.id },
      data: {
        passwordHash: await argon2.hash(password),
        activationToken: null,
        activationExpiresAt: null,
      },
      include: { role: true },
    });
    return this.issueToken(activated);
  }

  private async issueToken(user: UserWithRole): Promise<AuthResponse> {
    // Identité seule — les droits restent en base (invariant R0)
    const accessToken = await this.jwtService.signAsync({
      sub: user.id,
      roleId: user.role.id,
    });
    return {
      accessToken,
      user: {
        id: user.id,
        email: user.email,
        displayName: user.displayName,
        role: { id: user.role.id, name: user.role.name as RoleName },
        isDemo: user.isDemo,
      },
    };
  }

  private initials(displayName: string): string {
    return displayName
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 2)
      .map((w) => w[0]!.toUpperCase())
      .join('');
  }
}
