import {
  ConflictException,
  ForbiddenException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import type { JwtPayload } from './strategies/jwt.strategy';

export interface PublicUser {
  id: string;
  username: string;
  role: string;
  createdAt: string;
}

export interface LoginResult {
  accessToken: string;
  refreshToken: string;
  user: PublicUser;
}

const BCRYPT_ROUNDS = 10;
const ACCESS_TOKEN_EXPIRES_IN = process.env.TOKEN_EXPIRES_IN ?? '30m';
const REFRESH_TOKEN_EXPIRES_IN = process.env.REFRESH_TOKEN_EXPIRES_IN ?? '7d';

/**
 * Compared against when the username does not exist so that login takes
 * the same time either way (prevents user enumeration via timing).
 */
const DUMMY_HASH = '$2b$10$l232BFW3u63Mhfx0BatxUOLtw.qEofG9fNYjLsh2zce7MdIKDAIR6';

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

  /**
   * Bootstrap-only registration: allowed just while the instance has no
   * users. Once an admin exists the endpoint refuses to create accounts
   * (use database seeding / an operator flow instead).
   */
  async register(dto: RegisterDto): Promise<PublicUser> {
    const userCount = await this.prisma.user.count();
    if (userCount > 0) {
      throw new ForbiddenException('Registration is disabled');
    }
    const existing = await this.prisma.user.findUnique({
      where: { username: dto.username },
    });
    if (existing) {
      throw new ConflictException('Username already exists');
    }
    const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
    const created = await this.prisma.user.create({
      data: { username: dto.username, passwordHash },
    });
    return this.toPublic(created);
  }

  /**
   * Verifies credentials and returns signed access + refresh tokens.
   * Both tokens embed the user's tokenVersion so bumping it on the user
   * row (logout / revocation) invalidates them immediately.
   */
  async login(dto: LoginDto): Promise<LoginResult> {
    const user = await this.prisma.user.findUnique({
      where: { username: dto.username },
    });
    // Always run a bcrypt compare (against a dummy hash when the user is
    // unknown) so response timing cannot be used to enumerate usernames.
    const ok = await bcrypt.compare(dto.password, user?.passwordHash ?? DUMMY_HASH);
    if (!user || !ok) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return {
      accessToken: await this.signAccessToken(user),
      refreshToken: await this.signRefreshToken(user),
      user: this.toPublic(user),
    };
  }

  /**
   * Rotates a refresh token: the old refresh token becomes invalid as
   * soon as tokenVersion is bumped (logout, revocation).
   */
  async refresh(refreshToken: string): Promise<LoginResult> {
    let payload: JwtPayload;
    try {
      payload = await this.jwt.verifyAsync(refreshToken);
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }
    if (payload.typ !== 'refresh') {
      throw new UnauthorizedException('Invalid refresh token');
    }
    const user = await this.prisma.user
      .findUnique({ where: { id: BigInt(payload.sub) } })
      .catch(() => null);
    if (!user || user.tokenVersion !== payload.tv) {
      throw new UnauthorizedException('Invalid refresh token');
    }
    return {
      accessToken: await this.signAccessToken(user),
      refreshToken: await this.signRefreshToken(user),
      user: this.toPublic(user),
    };
  }

  /**
   * Revokes all tokens of a user by bumping tokenVersion.
   */
  async logout(userId: bigint): Promise<void> {
    await this.prisma.user.update({
      where: { id: userId },
      data: { tokenVersion: { increment: 1 } },
    });
  }

  async me(userId: bigint): Promise<PublicUser> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new UnauthorizedException();
    }
    return this.toPublic(user);
  }

  private async signAccessToken(user: {
    id: bigint;
    username: string;
    role: string;
    tokenVersion: number;
  }): Promise<string> {
    const payload: JwtPayload = {
      sub: user.id.toString(),
      username: user.username,
      role: user.role,
      tv: user.tokenVersion,
      typ: 'access',
    };
    return this.jwt.signAsync(payload, {
      expiresIn: ACCESS_TOKEN_EXPIRES_IN,
    });
  }

  private async signRefreshToken(user: {
    id: bigint;
    username: string;
    role: string;
    tokenVersion: number;
  }): Promise<string> {
    const payload: JwtPayload = {
      sub: user.id.toString(),
      username: user.username,
      role: user.role,
      tv: user.tokenVersion,
      typ: 'refresh',
    };
    return this.jwt.signAsync(payload, {
      expiresIn: REFRESH_TOKEN_EXPIRES_IN,
    });
  }

  private toPublic(user: {
    id: bigint;
    username: string;
    role: string;
    createdAt: Date;
  }): PublicUser {
    return {
      id: user.id.toString(),
      username: user.username,
      role: user.role,
      createdAt: user.createdAt.toISOString(),
    };
  }
}
