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

const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL_DAYS = 30;
const BCRYPT_ROUNDS = 12;

function hashToken(token: string): string {
  return crypto.createHash('sha256').update(token).digest('hex');
}

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

  private async issueTokenPair(userId: string) {
    const accessToken = await this.jwt.signAsync(
      { sub: userId },
      { secret: process.env.JWT_ACCESS_SECRET ?? 'CHANGE_ME_IN_PRODUCTION', expiresIn: ACCESS_TOKEN_TTL },
    );

    const refreshTokenRaw = crypto.randomBytes(48).toString('hex');
    const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
    await this.prisma.refreshToken.create({
      data: { userId, tokenHash: hashToken(refreshTokenRaw), expiresAt },
    });

    return { accessToken, refreshToken: refreshTokenRaw };
  }

  /**
   * Registration flow ported from inc/accounts-system.php: the activation
   * code is claimed atomically BEFORE the account is created, and released
   * back to `unused` if anything fails afterward — a code is only ever
   * consumed permanently alongside a successfully created account.
   */
  async register(dto: RegisterDto) {
    if (!dto.consent) {
      throw new ForbiddenException('يجب موافقة ولي الأمر على سياسة بيانات الطفل.');
    }

    const existing = await this.prisma.user.findUnique({ where: { parentEmail: dto.parentEmail } });
    if (existing) {
      throw new ConflictException('هذا البريد الإلكتروني مستخدم بالفعل.');
    }

    const claimed = await this.activation.claimCode(dto.activationCode);

    try {
      const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
      const user = await this.prisma.user.create({
        data: {
          displayName: dto.childName,
          parentEmail: dto.parentEmail,
          age: dto.age,
          passwordHash,
          notifyCertificateEmail: dto.notifyCertificate,
          consentChildDataPolicyAt: new Date(),
        },
      });

      await this.activation.finalizeCode(claimed.id, user.id);

      // Unlock the very first chapter of level 1 for the new user.
      const firstChapter = await this.prisma.chapter.findFirst({
        where: { level: { order: 1 }, orderInLevel: 1 },
      });
      if (firstChapter) {
        await this.prisma.userProgress.create({
          data: { userId: user.id, chapterId: firstChapter.id, status: 'unlocked' },
        });
      }

      const tokens = await this.issueTokenPair(user.id);
      return { user: this.publicUser(user), ...tokens };
    } catch (e) {
      // Account creation failed after the code was claimed — never lose the code.
      await this.activation.releaseCode(claimed.id);
      throw e;
    }
  }

  async login(dto: LoginDto) {
    const user = await this.prisma.user.findUnique({ where: { parentEmail: dto.parentEmail } });
    if (!user) throw new UnauthorizedException('البريد الإلكتروني أو كلمة المرور غير صحيحة.');

    const valid = await bcrypt.compare(dto.password, user.passwordHash);
    if (!valid) throw new UnauthorizedException('البريد الإلكتروني أو كلمة المرور غير صحيحة.');

    if (user.status !== 'active') throw new ForbiddenException('هذا الحساب موقوف حاليًا.');

    const tokens = await this.issueTokenPair(user.id);
    return { user: this.publicUser(user), ...tokens };
  }

  async refresh(refreshTokenRaw: string) {
    const tokenHash = hashToken(refreshTokenRaw);
    const stored = await this.prisma.refreshToken.findFirst({
      where: { tokenHash, revokedAt: null, expiresAt: { gt: new Date() } },
    });
    if (!stored) throw new UnauthorizedException('جلسة غير صالحة، الرجاء تسجيل الدخول مجددًا.');

    // Rotate: revoke the used refresh token, issue a brand new pair.
    await this.prisma.refreshToken.update({
      where: { id: stored.id },
      data: { revokedAt: new Date() },
    });

    return this.issueTokenPair(stored.userId);
  }

  async logout(refreshTokenRaw: string) {
    const tokenHash = hashToken(refreshTokenRaw);
    await this.prisma.refreshToken.updateMany({
      where: { tokenHash, revokedAt: null },
      data: { revokedAt: new Date() },
    });
    return { success: true };
  }

  private publicUser(user: { id: string; displayName: string; parentEmail: string; age: number }) {
    return { id: user.id, displayName: user.displayName, parentEmail: user.parentEmail, age: user.age };
  }
}
