import Auth from '@/entities/core/auth';
import AppResponse from '@/system/response/ktq-response';
import {
  BadRequestException,
  HttpStatus,
  Res,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { plainToClass } from 'class-transformer';
import { Request, Response } from 'express';
import { Repository } from 'typeorm';
import { ChangePasswordDto } from '../dto/change-password.dto';
import { Constant } from '../../../ultils/constant';
import { SystemLang } from '@/system/lang/system.lang';
import { Cache } from 'cache-manager';

export class AuthService<T extends Auth> {
  constructor(
    protected jwtService: JwtService,
    protected readonly authRepo: Repository<T>,
    protected readonly redis: Cache,
    private readonly classRef: new (...args: any[]) => T,
    private readonly searchFields: (keyof T)[] = ['phone'] as (keyof T)[],
  ) {}

  async validate(
    input: string,
    password: string,
    message = SystemLang.getText('messages', 'login_error_input'),
  ) {
    const where = this.searchFields.map((field) => ({
      [field]: input,
      is_login_enabled: true,
      active: true,
    })) as any;

    const user = await this.authRepo.findOne({ where });

    if (!user || !(await bcrypt.compare(password, user.password))) {
      throw new UnauthorizedException(
        AppResponse.toResponse(null, {
          message,
          status_code: HttpStatus.UNAUTHORIZED,
        }),
      );
    }

    return user;
  }

  async login(user: T, response: Response) {
    const payload = { _id: user.id };
    const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
    const refreshToken = this.jwtService.sign(
      { refresh_key: true, ...payload },
      { expiresIn: '7d' },
    );

    response.cookie(Constant.ACCESS_TOKEN, accessToken, {
      httpOnly: true,
      maxAge: 15 * 60 * 1000,
      sameSite: 'lax',
    });

    response.cookie(Constant.REFRESH_TOKEN, refreshToken, {
      httpOnly: true,
      maxAge: 7 * 24 * 60 * 60 * 1000,
      sameSite: 'lax',
    });

    return AppResponse.toResponse(true, {
      message: SystemLang.getText('messages', 'login_success'),
    });
  }

  async blacklistJwtToken(
    token: string,
    userId: string | number,
    tokenType: 'access' | 'refresh',
  ): Promise<boolean> {
    try {
      if (!token) return false;

      const decoded: any = this.jwtService.decode(token);
      if (!decoded?.exp) return false;

      const now = Math.floor(Date.now() / 1000);
      const ttl = decoded.exp - now;
      if (ttl <= 0) return false;

      const tokenTail = token.slice(-10);
      const tokenValue = token.slice(-20);
      const key = `blacklist_token:${userId}:${tokenType}:${tokenTail}`;

      await this.redis.set(key, { token: tokenValue, ttl }, ttl * 1000);
      return true;
    } catch (error) {
      return false;
    }
  }

  async isValidAccessToken(access_token: string): Promise<boolean> {
    try {
      const data: any = this.jwtService.decode(access_token);

      // Không decode được hoặc không có _id => token không hợp lệ
      if (!data || !data?._id) return false;

      const tokenTail = access_token.slice(-10);
      const key = `blacklist_token:${data._id}:access:${tokenTail}`;

      const result = await this.redis.get(key);

      // Nếu không có trong Redis -> chưa bị revoke => hợp lệ
      return !result;
    } catch (error) {
      return false;
    }
  }

  async isValidRefreshToken(refresh_token: string): Promise<boolean> {
    try {
      const data: any = this.jwtService.decode(refresh_token);

      // Không decode được hoặc không có _id hoặc không phải refresh_token => token không hợp lệ
      if (!data || !data?._id || !data?.refresh_key) return false;

      const tokenTail = refresh_token.slice(-10);
      const key = `blacklist_token:${data._id}:access:${tokenTail}`;

      const result = await this.redis.get(key);

      // Nếu không có trong Redis -> chưa bị revoke => hợp lệ
      return !result;
    } catch (error) {
      return false;
    }
  }

  async isValidTokens({
    refresh_token,
    access_token,
  }: {
    access_token: string;
    refresh_token: string;
  }) {
    return (
      (await this.isValidAccessToken(access_token)) ||
      (await this.isValidRefreshToken(refresh_token))
    );
  }

  async logout(request: Request, @Res() response: Response) {
    const user = request[Constant.ATTR_REQ_DATA];

    if (user) {
      const access_result = await this.blacklistJwtToken(
        request.cookies[Constant.ACCESS_TOKEN],
        user.id,
        'access',
      );

      const refresh_result = await this.blacklistJwtToken(
        request.cookies[Constant.REFRESH_TOKEN],
        user.id,
        'refresh',
      );
    }

    response.cookie(Constant.ACCESS_TOKEN, '', {
      httpOnly: true,
      maxAge: 0,
      sameSite: 'lax',
    });

    response.cookie(Constant.REFRESH_TOKEN, '', {
      httpOnly: true,
      maxAge: 0,
      sameSite: 'lax',
    });

    return AppResponse.toResponse(true, {
      message: SystemLang.getText('messages', 'logout_success'),
    });
  }

  async changePassword(
    data: ChangePasswordDto,
    request: Request,
    response: Response,
  ) {
    const admin = request[Constant.ATTR_REQ_DATA];

    if (!admin) throw new UnauthorizedException(AppResponse.toResponse(false));

    await this.validate(
      admin.username,
      data.password,
      SystemLang.getText('messages', 'password_invalid'),
    );

    const newPassword = await bcrypt.hash(data.newPassword, 10);
    const result = await this.authRepo.update(admin.id, {
      password: newPassword,
    } as any);

    if (!result.affected)
      throw new BadRequestException(AppResponse.toResponse(false));

    await this.logout(request, response);

    return AppResponse.toResponse(true, {
      message: SystemLang.getText('messages', 'change_pass_success'),
    });
  }

  async me(request: Request) {
    const admin = request[Constant.ATTR_REQ_DATA];

    if (!admin) throw new UnauthorizedException(AppResponse.toResponse(null));

    return AppResponse.toResponse(plainToClass(this.classRef, admin));
  }
}
