import {
  ConflictException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AccountResponse } from './dto/account.response';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { PasswordService } from './password.service';
import { ParDeTokens, SessionService } from './session.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly passwords: PasswordService,
    private readonly sessions: SessionService,
  ) {}

  /**
   * Login. A resposta é **idêntica** para e-mail inexistente e senha errada —
   * critério de aceite de AC-2: revelar quem tem conta vazaria quem participa
   * dos bolões.
   */
  async login(dto: LoginDto): Promise<ParDeTokens> {
    const email = dto.email.trim().toLowerCase();
    const user = await this.prisma.user.findUnique({
      where: { email },
      select: { id: true, passwordHash: true },
    });

    const hashParaComparar = user?.passwordHash ?? this.hashFalso;
    const senhaConfere = await this.passwords.verify(
      dto.password,
      hashParaComparar,
    );

    if (!user || !senhaConfere) {
      throw new UnauthorizedException('e-mail ou senha inválidos');
    }

    return this.sessions.criar(user.id);
  }

  /**
   * Hash descartável usado quando o e-mail não existe. Faz o caminho gastar o
   * mesmo tempo do caminho real, senão a diferença de latência revela quais
   * e-mails têm conta — o mesmo vazamento que a mensagem genérica evita.
   */
  private readonly hashFalso =
    'scrypt$32768$8$1$AAAAAAAAAAAAAAAAAAAAAA==$' +
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==';

  async register(dto: RegisterDto): Promise<AccountResponse> {
    const email = dto.email.trim().toLowerCase();

    const passwordHash = await this.passwords.hash(dto.password);

    try {
      const user = await this.prisma.user.create({
        data: { email, passwordHash, displayName: dto.displayName.trim() },
        // Select explícito: o hash não sai daqui nem por acidente (BE-4).
        select: { id: true, email: true, displayName: true, createdAt: true },
      });
      return AccountResponse.from(user);
    } catch (error) {
      if (this.isUniqueViolation(error)) {
        // A story pede apontar para o login. Isso revela que o e-mail existe —
        // inerente a cadastro, e decidido no critério de aceite de AC-1. O
        // login (AC-2) é que não pode revelar.
        throw new ConflictException(
          'este e-mail já tem conta. Faça login em vez de cadastrar',
        );
      }
      throw error;
    }
  }

  async contaPorId(id: string): Promise<AccountResponse> {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: { id: true, email: true, displayName: true, createdAt: true },
    });
    if (!user) throw new UnauthorizedException('conta não encontrada');
    return AccountResponse.from(user);
  }

  /** P2002 = violação de constraint única no Prisma. */
  private isUniqueViolation(error: unknown): boolean {
    return (
      typeof error === 'object' &&
      error !== null &&
      'code' in error &&
      (error as { code?: unknown }).code === 'P2002'
    );
  }
}
