import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import { User } from './entities/user.entity';
import { MailService } from '../mail/mail.service';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
    private readonly jwtService: JwtService,
    private readonly mailService: MailService,
  ) {}

  // ---------------------------------------------------------------------------
  // Registration & email verification
  // ---------------------------------------------------------------------------

  async register(email: string, password: string): Promise<void> {
    const existing = await this.usersRepo.findOne({ where: { email } });
    if (existing) {
      throw new ConflictException('Email already in use');
    }

    const passwordHash = await bcrypt.hash(password, 10);
    const { code, expiresAt } = this.generateOtp();

    const user = this.usersRepo.create({
      email,
      passwordHash,
      isVerified: false,
      otpCode: code,
      otpExpiresAt: expiresAt,
    });
    await this.usersRepo.save(user);

    await this.mailService.sendOtp(email, code, 'LexiBrain — Confirm your email');
  }

  async verifyEmail(email: string, code: string): Promise<{ accessToken: string }> {
    const user = await this.findUserByEmail(email);

    if (user.isVerified) {
      throw new BadRequestException('Email already verified');
    }
    this.checkOtp(user, code);

    user.isVerified = true;
    user.otpCode = null;
    user.otpExpiresAt = null;
    await this.usersRepo.save(user);

    return { accessToken: this.signToken(user) };
  }

  async resendOtp(email: string): Promise<void> {
    const user = await this.findUserByEmail(email);

    if (user.isVerified) {
      throw new BadRequestException('Email already verified');
    }

    const { code, expiresAt } = this.generateOtp();
    user.otpCode = code;
    user.otpExpiresAt = expiresAt;
    await this.usersRepo.save(user);

    await this.mailService.sendOtp(email, code, 'LexiBrain — New verification code');
  }

  // ---------------------------------------------------------------------------
  // Login
  // ---------------------------------------------------------------------------

  async login(email: string, password: string): Promise<{ accessToken: string }> {
    // Use same error for wrong email and wrong password to avoid user enumeration
    const user = await this.usersRepo.findOne({ where: { email } });
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const passwordMatch = await bcrypt.compare(password, user.passwordHash);
    if (!passwordMatch) {
      throw new UnauthorizedException('Invalid credentials');
    }

    if (!user.isVerified) {
      throw new ForbiddenException('Please verify your email before logging in');
    }

    return { accessToken: this.signToken(user) };
  }

  // ---------------------------------------------------------------------------
  // Password reset
  // ---------------------------------------------------------------------------

  async forgotPassword(email: string): Promise<void> {
    // Always succeed — no user enumeration
    const user = await this.usersRepo.findOne({ where: { email } });
    if (!user) return;

    const { code, expiresAt } = this.generateOtp();
    user.otpCode = code;
    user.otpExpiresAt = expiresAt;
    await this.usersRepo.save(user);

    await this.mailService.sendOtp(email, code, 'LexiBrain — Password reset code');
  }

  async resetPassword(email: string, code: string, newPassword: string): Promise<void> {
    const user = await this.findUserByEmail(email);
    this.checkOtp(user, code);

    user.passwordHash = await bcrypt.hash(newPassword, 10);
    user.otpCode = null;
    user.otpExpiresAt = null;
    await this.usersRepo.save(user);
  }

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  private signToken(user: User): string {
    return this.jwtService.sign({ sub: user.id, email: user.email });
  }

  private generateOtp(): { code: string; expiresAt: Date } {
    const code = String(Math.floor(100000 + Math.random() * 900000));
    const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
    return { code, expiresAt };
  }

  private async findUserByEmail(email: string): Promise<User> {
    const user = await this.usersRepo.findOne({ where: { email } });
    if (!user) throw new NotFoundException('User not found');
    return user;
  }

  private checkOtp(user: User, code: string): void {
    if (!user.otpCode || !user.otpExpiresAt) {
      throw new UnauthorizedException('No active code — request a new one');
    }
    if (user.otpCode !== code) {
      throw new UnauthorizedException('Invalid code');
    }
    if (new Date() > user.otpExpiresAt) {
      throw new UnauthorizedException('Code has expired');
    }
  }
}
