const UsersDBApi = require('../db/api/users');
const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
const EmailOtpEmail = require('./email/list/emailOtp');
const InvitationEmail = require("./email/list/invitation");
const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config');
const helpers = require('../helpers');
const db = require('../db/models');

class Auth {
  static normalizeEmail(email) {
    if (!email || typeof email !== 'string' || !email.includes('@')) {
      throw new ValidationError('auth.invalidEmail');
    }

    return email.trim().toLowerCase();
  }

  static async findUserModelByEmail(email, transaction) {
    return db.users.findOne({
      where: {
        email: {
          [db.Sequelize.Op.iLike]: email,
        },
      },
      transaction,
    });
  }

  static async ensureDefaultMemberRole(user, transaction) {
    const currentRole = await user.getApp_role({ transaction });

    if (currentRole?.id) {
      return currentRole;
    }

    const role = await db.roles.findOne({
      where: { name: config.roles?.user || 'Registered User' },
      transaction,
    });

    if (role?.id) {
      await user.setApp_role(role.id, { transaction });
    }

    return role;
  }

  static async createOtpUser(email, transaction) {
    const randomPassword = crypto.randomBytes(20).toString('hex');
    const hashedPassword = await bcrypt.hash(
      randomPassword,
      config.bcrypt.saltRounds,
    );

    const user = await db.users.create(
      {
        email,
        firstName: email.split('@')[0],
        password: hashedPassword,
        provider: config.providers.LOCAL,
        emailVerified: false,
      },
      { transaction },
    );

    await this.ensureDefaultMemberRole(user, transaction);

    return user;
  }

  static async requestEmailOtp(email) {
    const normalizedEmail = this.normalizeEmail(email);

    if (!EmailSender.isConfigured) {
      throw new ValidationError('auth.emailOtp.emailNotConfigured');
    }

    const transaction = await db.sequelize.transaction();

    try {
      let user = await this.findUserModelByEmail(normalizedEmail, transaction);

      if (user?.disabled) {
        throw new ValidationError('auth.userDisabled');
      }

      if (!user) {
        user = await this.createOtpUser(normalizedEmail, transaction);
      }

      if (user.emailOtpLastSentAt) {
        const secondsSinceLastOtp = (Date.now() - new Date(user.emailOtpLastSentAt).getTime()) / 1000;

        if (secondsSinceLastOtp < 60) {
          throw new ValidationError('auth.emailOtp.tooManyRequests');
        }
      }

      const otp = String(crypto.randomInt(100000, 1000000));
      const expiresInMinutes = 10;
      const emailOtpHash = await bcrypt.hash(otp, config.bcrypt.saltRounds);

      await user.update(
        {
          emailOtpHash,
          emailOtpExpiresAt: new Date(Date.now() + expiresInMinutes * 60 * 1000),
          emailOtpLastSentAt: new Date(),
          emailOtpAttempts: 0,
        },
        { transaction },
      );

      const emailOtpEmail = new EmailOtpEmail(normalizedEmail, otp, expiresInMinutes);
      await new EmailSender(emailOtpEmail).send();

      await transaction.commit();

      return {
        success: true,
        email: normalizedEmail,
        expiresInMinutes,
      };
    } catch (error) {
      if (!transaction.finished) {
        await transaction.rollback();
      }
      console.error('Email OTP request failed:', error);
      throw error;
    }
  }

  static async verifyEmailOtp(email, otp) {
    const normalizedEmail = this.normalizeEmail(email);
    const normalizedOtp = String(otp || '').trim();

    if (!/^\d{6}$/.test(normalizedOtp)) {
      throw new ValidationError('auth.emailOtp.invalid');
    }

    const transaction = await db.sequelize.transaction();

    try {
      const user = await this.findUserModelByEmail(normalizedEmail, transaction);

      if (!user || user.disabled) {
        throw new ValidationError(user?.disabled ? 'auth.userDisabled' : 'auth.emailOtp.invalid');
      }

      const attempts = Number(user.emailOtpAttempts || 0);

      if (attempts >= 5) {
        throw new ValidationError('auth.emailOtp.tooManyAttempts');
      }

      if (!user.emailOtpHash || !user.emailOtpExpiresAt || new Date(user.emailOtpExpiresAt).getTime() < Date.now()) {
        throw new ValidationError('auth.emailOtp.invalid');
      }

      const otpMatches = await bcrypt.compare(normalizedOtp, user.emailOtpHash);

      if (!otpMatches) {
        await user.update(
          {
            emailOtpAttempts: attempts + 1,
          },
          { transaction },
        );
        await transaction.commit();
        throw new ValidationError('auth.emailOtp.invalid');
      }

      await this.ensureDefaultMemberRole(user, transaction);

      await user.update(
        {
          emailVerified: true,
          emailOtpHash: null,
          emailOtpExpiresAt: null,
          emailOtpAttempts: 0,
        },
        { transaction },
      );

      await transaction.commit();

      const data = {
        user: {
          id: user.id,
          email: user.email,
        },
      };

      return helpers.jwtSign(data);
    } catch (error) {
      if (!transaction.finished) {
        await transaction.rollback();
      }
      console.error('Email OTP verification failed:', error);
      throw error;
    }
  }

  static async socialSignin(email, profile, provider) {
    const normalizedEmail = this.normalizeEmail(email);
    const transaction = await db.sequelize.transaction();

    try {
      let user = await this.findUserModelByEmail(normalizedEmail, transaction);

      if (user?.disabled) {
        throw new ValidationError('auth.userDisabled');
      }

      if (!user) {
        const displayName = profile.displayName || profile.name?.givenName || normalizedEmail.split('@')[0];

        user = await db.users.create(
          {
            email: normalizedEmail,
            firstName: displayName,
            provider,
            emailVerified: true,
          },
          { transaction },
        );

        await this.ensureDefaultMemberRole(user, transaction);
      } else {
        const updatePayload = {
          emailVerified: true,
        };

        if (!user.provider) {
          updatePayload.provider = provider;
        }

        await user.update(updatePayload, { transaction });
      }

      await transaction.commit();

      const data = {
        user: {
          id: user.id,
          email: user.email,
          name: profile.displayName,
        },
      };

      return helpers.jwtSign(data);
    } catch (error) {
      if (!transaction.finished) {
        await transaction.rollback();
      }
      console.error('Social sign-in failed:', error);
      throw error;
    }
  }

  static async signup(email, password, options = {}, host) {
    email = this.normalizeEmail(email);
    const user = await UsersDBApi.findBy({email});

    const hashedPassword = await bcrypt.hash(
      password,
      config.bcrypt.saltRounds,
    );

    if (user) {
      if (user.authenticationUid) {
        throw new ValidationError(
          'auth.emailAlreadyInUse',
        );
      }

      if (user.disabled) {
        throw new ValidationError(
          'auth.userDisabled',
        );
      }

      await UsersDBApi.updatePassword(
        user.id,
        hashedPassword,
        options,
      );

      if (EmailSender.isConfigured) {
        await this.sendEmailAddressVerificationEmail(
          user.email,
          host,
        );
      }

      const data = {
        user: {
          id: user.id,
          email: user.email
        }
      };

      return helpers.jwtSign(data);
    }

    const newUser = await UsersDBApi.createFromAuth(
      {
        firstName: email.split('@')[0],
        password: hashedPassword,
        email: email,
        
      },
      options,
    );

    if (EmailSender.isConfigured) {
      await this.sendEmailAddressVerificationEmail(
        newUser.email,
        host,
      );
    }

    const data = {
      user: {
        id: newUser.id,
        email: newUser.email
      }
    };

    return helpers.jwtSign(data);
  }

  static async signin(email, password) {
    email = this.normalizeEmail(email);
    const user = await UsersDBApi.findBy({email});

    if (!user) {
      throw new ValidationError(
        'auth.userNotFound',
      );
    }

    if (user.disabled) {
      throw new ValidationError(
        'auth.userDisabled',
      );
    }

    if (!user.password) {
      throw new ValidationError(
        'auth.wrongPassword',
      );
    }

    if (!EmailSender.isConfigured) {
      user.emailVerified = true;
    }

    if (!user.emailVerified) {
      throw new ValidationError(
        'auth.userNotVerified',
      );
    }

    const passwordsMatch = await bcrypt.compare(
      password,
      user.password,
    );

    if (!passwordsMatch) {
      throw new ValidationError(
        'auth.wrongPassword',
      );
    }

    const data = {
      user: {
        id: user.id,
        email: user.email
      }
    };

    return helpers.jwtSign(data);
  }

  static async sendEmailAddressVerificationEmail(
    email,
    host,
  ) {


    let link;
    try {
      const token = await UsersDBApi.generateEmailVerificationToken(
        email,
      );
      link = `${host}/verify-email?token=${token}`;
    } catch (error) {
      console.error(error);
      throw new ValidationError(
        'auth.emailAddressVerificationEmail.error',
      );
    }

    const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
      email,
      link,
    );

    return new EmailSender(
      emailAddressVerificationEmail,
    ).send();
  }

  static async sendPasswordResetEmail(email, type = 'register', host) {


    let link;

    try {
      const token = await UsersDBApi.generatePasswordResetToken(
        email,
      );
      link = `${host}/password-reset?token=${token}`;
    } catch (error) {
      console.error(error);
      throw new ValidationError(
        'auth.passwordReset.error',
      );
    }

    let passwordResetEmail;
    if (type === 'register') {
      passwordResetEmail = new PasswordResetEmail(
        email,
        link,
      );
    }
    if (type === 'invitation') {
      passwordResetEmail = new InvitationEmail(
        email,
        link,
      );
    }

    return new EmailSender(passwordResetEmail).send();
  }

  static async verifyEmail(token, options = {}) {
    const user = await UsersDBApi.findByEmailVerificationToken(
      token,
      options,
    );

    if (!user) {
      throw new ValidationError(
        'auth.emailAddressVerificationEmail.invalidToken',
      );
    }

    return UsersDBApi.markEmailVerified(
      user.id,
      options,
    );
  }

  static async passwordUpdate(currentPassword, newPassword, options) {
    const currentUser = options.currentUser || null;
    if (!currentUser) {
      throw new ForbiddenError();
    }

    const currentPasswordMatch = await bcrypt.compare(
      currentPassword,
      currentUser.password,
    );

    if (!currentPasswordMatch) {
      throw new ValidationError(
        'auth.wrongPassword'
      )
    }

    const newPasswordMatch = await bcrypt.compare(
      newPassword,
      currentUser.password,
    );

    if (newPasswordMatch) {
      throw new ValidationError(
        'auth.passwordUpdate.samePassword'
      )
    }

    const hashedPassword = await bcrypt.hash(
      newPassword,
      config.bcrypt.saltRounds,
    );

    return UsersDBApi.updatePassword(
      currentUser.id,
      hashedPassword,
      options,
    );
  }

  static async passwordReset(
    token,
    password,
    options = {},
  ) {
    const user = await UsersDBApi.findByPasswordResetToken(
      token,
      options,
    );

    if (!user) {
      throw new ValidationError(
        'auth.passwordReset.invalidToken',
      );
    }

    const hashedPassword = await bcrypt.hash(
      password,
      config.bcrypt.saltRounds,
    );

    return UsersDBApi.updatePassword(
      user.id,
      hashedPassword,
      options,
    );
  }

  static async updateProfile(data, currentUser) {
    let transaction = await db.sequelize.transaction();

    try {
      await UsersDBApi.findBy(
        {id: currentUser.id},
        {transaction},
      );

      await UsersDBApi.update(
        currentUser.id,
        data,
        {
          currentUser,
          transaction
        },
      );


      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
}

module.exports = Auth;
