import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { createHash, randomBytes } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';

export interface ParDeTokens {
  accessToken: string;
  refreshToken: string;
}

/** Conteúdo do access token. `sub` é o id da conta. */
export interface PayloadAcesso {
  sub: string;
  sid: string;
}

@Injectable()
export class SessionService {
  /**
   * Access token curto + refresh longo. O refresh é o que sustenta a "sessão
   * persistente" de AC-3: o participante não vê tela de login por 90 dias, mas
   * um token roubado só vale 15 minutos sem o refresh.
   */
  private readonly acessoExpiraEm = '15m';
  private readonly refreshDias = 90;

  constructor(
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
  ) {}

  async criar(userId: string): Promise<ParDeTokens> {
    const refreshToken = randomBytes(48).toString('base64url');
    const expiresAt = new Date(
      Date.now() + this.refreshDias * 24 * 60 * 60 * 1000,
    );

    const session = await this.prisma.session.create({
      data: {
        userId,
        refreshTokenHash: this.hashRefresh(refreshToken),
        expiresAt,
      },
      select: { id: true },
    });

    return {
      accessToken: await this.assinarAcesso(userId, session.id),
      refreshToken,
    };
  }

  /** Rotaciona o refresh: o antigo é revogado no mesmo passo (BE-3 de AC-3). */
  async renovar(refreshToken: string): Promise<ParDeTokens> {
    const session = await this.prisma.session.findUnique({
      where: { refreshTokenHash: this.hashRefresh(refreshToken) },
      select: { id: true, userId: true, expiresAt: true, revokedAt: true },
    });

    if (!session || session.revokedAt || session.expiresAt < new Date()) {
      throw new UnauthorizedException('sessão inválida ou expirada');
    }

    await this.prisma.session.update({
      where: { id: session.id },
      data: { revokedAt: new Date() },
    });

    return this.criar(session.userId);
  }

  /** Logout do device. Idempotente: revogar duas vezes não é erro. */
  async revogar(refreshToken: string): Promise<void> {
    await this.prisma.session.updateMany({
      where: {
        refreshTokenHash: this.hashRefresh(refreshToken),
        revokedAt: null,
      },
      data: { revokedAt: new Date() },
    });
  }

  /** Usado quando a senha muda: derruba todas as sessões (AC-4 BE-4). */
  async revogarTodasDoUsuario(userId: string): Promise<number> {
    const { count } = await this.prisma.session.updateMany({
      where: { userId, revokedAt: null },
      data: { revokedAt: new Date() },
    });
    return count;
  }

  async sessaoAtiva(sessionId: string): Promise<boolean> {
    const s = await this.prisma.session.findUnique({
      where: { id: sessionId },
      select: { revokedAt: true, expiresAt: true },
    });
    return !!s && !s.revokedAt && s.expiresAt > new Date();
  }

  private assinarAcesso(sub: string, sid: string): Promise<string> {
    return this.jwt.signAsync({ sub, sid }, { expiresIn: this.acessoExpiraEm });
  }

  /**
   * O refresh é aleatório de 48 bytes, não uma senha — SHA-256 basta e mantém
   * a busca por índice O(1). KDF lento aqui inviabilizaria o lookup.
   */
  private hashRefresh(token: string): string {
    return createHash('sha256').update(token).digest('hex');
  }
}
