import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OtpVerificationModel } from '../models/otp-verification.model';
import {
  OTP_ACTION_TYPE,
  OtpRequestEntity,
  OtpVerificationEntity,
  OtpVerifierEntity,
  // OtpVerifierEntity,
  OtpVerifyEntity,
} from '../../domain/entities/otp-verification.entity';
import * as moment from 'moment';
import { OtpService } from 'src/core/helpers/otp/otp-service';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { WhatsappService } from 'src/services/whatsapp/whatsapp.service';
import { OtpVerifierModel } from '../models/otp-verifier.model';

@Injectable()
export class OtpVerificationService {
  constructor(
    @InjectRepository(OtpVerificationModel)
    private readonly otpVerificationRepo: Repository<OtpVerificationModel>,

    @InjectRepository(OtpVerifierModel)
    private readonly otpVerifierRepo: Repository<OtpVerifierModel>,
  ) {}

  private generateOtpExpiration(minutes = 5): number {
    return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time
  }

  private generateResendAvailableAt(seconds = 60): number {
    return moment().add(seconds, 'seconds').valueOf(); // epoch millis
  }

  private generateTimestamp(): number {
    return moment().valueOf(); // epoch millis verification time (now)
  }

  private generateHeaderTemplate(payload) {
    const label = payload.action_type;

    // Optional logic to override label based on action type.
    // e.g., if (payload.action_type === OTP_ACTION_TYPE.CONFIRM_TRANSACTION) label = 'CONFIRM_BOOKING_TRANSACTION'

    const header = label.split('_').join(' ');
    const type = label
      .split('_')
      .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(' ');

    return { header, type };
  }

  private generateOTPMsgTemplate(payload) {
    const { userRequest, newOtp } = payload;
    const { header, type } = this.generateHeaderTemplate(newOtp);

    const otpCode = newOtp?.otp_code;
    const username = userRequest?.username;

    return {
      name: 'general_flow',
      language: { code: 'id' },
      components: [
        {
          type: 'header',
          parameters: [
            {
              type: 'text',
              parameter_name: 'header',
              text: header,
            },
          ],
        },
        {
          type: 'body',
          parameters: [
            {
              type: 'text',
              parameter_name: 'name',
              text: username,
            },
            {
              type: 'text',
              parameter_name: 'code',
              text: otpCode,
            },
            {
              type: 'text',
              parameter_name: 'type',
              text: type,
            },
          ],
        },
        {
          type: 'footer',
          parameters: [
            {
              type: 'text',
              text: 'Kode berlaku selama 5 menit.',
            },
          ],
        },
      ],
    };
  }

  async requestOTP(payload: OtpRequestEntity, req: any) {
    const otpService = new OtpService({ length: 4 });
    const otpCode = otpService.generateSecureOTP();
    const dateNow = this.generateTimestamp();
    const expiredAt = this.generateOtpExpiration();
    const userRequest = req?.user;

    const newOtp: OtpVerificationEntity = {
      otp_code: otpCode,
      action_type: payload.action_type,
      target_id: payload.target_id,
      reference: payload.reference,
      source: payload.source,
      is_used: false,
      is_replaced: false,
      expired_at: expiredAt,

      creator_id: userRequest?.id,
      creator_name: userRequest?.name,
      created_at: dateNow,
      verified_at: null,

      editor_id: userRequest?.id,
      editor_name: userRequest?.name,
      updated_at: dateNow,
    };

    const activeOTP = await this.getActiveOtp(
      payload.target_id ? payload.target_id : payload.reference,
    );

    if (activeOTP) {
      const createdAtMoment = moment(Number(activeOTP.created_at));
      const nowMoment = moment(Number(dateNow));
      const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds');
      const isProduction = process.env.NODE_ENV === 'true';

      if (diffSeconds < 60 && isProduction) {
        throw new BadRequestException(
          'An active OTP request was made recently. Please try again later.',
        );
      } else {
        // Update data is_replaced on database
        this.otpVerificationRepo.save({
          ...activeOTP,
          is_replaced: true,
        });
      }
    }

    // save otp to database
    await this.otpVerificationRepo.save(newOtp);
    const verifiers: OtpVerifierEntity[] = await this.getVerifier([
      payload.action_type,
    ]);
    const notificationService = new WhatsappService();

    verifiers?.map((v) => {
      notificationService.sendTemplateMessage({
        phone: v.phone_number,
        templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }),
      });
    });

    return {
      message: `OTP has been sent to the admin's WhatsApp.`,
      updated_at: expiredAt,
      resend_available_at: this.generateResendAvailableAt(),
    };
  }

  async verifyOTP(payload: OtpVerifyEntity, req: any) {
    const userRequest = req?.user;
    const { otp_code, action_type, target_id, reference, source } = payload;
    const dateNow = this.generateTimestamp();

    if (!target_id && !reference) {
      throw new BadRequestException(
        'Either target_id or reference must be provided.',
      );
    }

    let otp: any;

    // Build a where condition with OR between target_id and reference

    if (target_id) {
      otp = await this.otpVerificationRepo.findOne({
        where: {
          otp_code,
          action_type,
          target_id,
          source,
          is_used: false,
          is_replaced: false,
        },
      });
    } else if (reference) {
      otp = await this.otpVerificationRepo.findOne({
        where: {
          otp_code,
          action_type,
          reference,
          source,
          is_used: false,
          is_replaced: false,
        },
      });
    }

    if (!otp) {
      throw new BadRequestException('Invalid or expired OTP.');
    } else if (otp.expired_at <= dateNow) {
      throw new BadRequestException('OTP has expired.');
    }

    otp.is_used = true;
    otp.verified_at = dateNow;
    otp.editor_id = userRequest?.id;
    otp.editor_name = userRequest?.name;
    otp.updated_at = dateNow;

    // update otp to database
    await this.otpVerificationRepo.save(otp);
    return { message: 'OTP verified successfully.' };
  }

  async getActiveOtp(payload: string) {
    const now = this.generateTimestamp();
    const tableName = TABLE_NAME.OTP_VERIFICATIONS;

    return this.otpVerificationRepo
      .createQueryBuilder(tableName)
      .where(
        `(${tableName}.target_id = :payload OR ${tableName}.reference = :payload)
         AND ${tableName}.is_used = false
         AND ${tableName}.is_replaced = false
         AND ${tableName}.expired_at > :now`,
        { payload, now },
      )
      .orderBy(
        `CASE WHEN ${tableName}.target_id = :payload THEN 0 ELSE 1 END`,
        'ASC',
      )
      .getOne();
  }

  async getVerifier(actions: OTP_ACTION_TYPE[]) {
    const tableALias = TABLE_NAME.OTP_VERIFIER;
    const results = await this.otpVerifierRepo
      .createQueryBuilder(tableALias)
      .where(`${tableALias}.is_all_action = :isAll`, { isAll: true })
      .orWhere(`${tableALias}.action_types && :actions`, { actions })
      .getMany();
    return results ?? [];
  }
}
