import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import {
  EmailVerificationCode,
  EmailVerificationCodeDocument,
} from './schemas/email-verification-code.schema';
import {
  PasswordResetCode,
  PasswordResetCodeDocument,
} from './schemas/password-reset-code.schema';
import { RefreshToken, RefreshTokenDocument } from './schemas/refresh-token.schema';
import { SignupDraft, SignupDraftDocument } from './schemas/signup-draft.schema';
import {
  SuperAdminRefreshToken,
  SuperAdminRefreshTokenDocument,
} from './schemas/super-admin-refresh-token.schema';

@Injectable()
export class AuthRepository {
  constructor(
    @InjectModel(RefreshToken.name)
    private readonly refreshTokenModel: Model<RefreshTokenDocument>,
    @InjectModel(SuperAdminRefreshToken.name)
    private readonly superAdminRefreshTokenModel: Model<SuperAdminRefreshTokenDocument>,
    @InjectModel(PasswordResetCode.name)
    private readonly passwordResetCodeModel: Model<PasswordResetCodeDocument>,
    @InjectModel(EmailVerificationCode.name)
    private readonly emailVerificationCodeModel: Model<EmailVerificationCodeDocument>,
    @InjectModel(SignupDraft.name)
    private readonly signupDraftModel: Model<SignupDraftDocument>,
  ) {}

  async createRefreshToken(userId: string, jti: string, tokenHash: string, expiresAt: Date): Promise<void> {
    await this.refreshTokenModel.create({
      userId: new Types.ObjectId(userId),
      jti,
      tokenHash,
      expiresAt,
    });
  }

  async findActiveUserTokens(userId: string): Promise<RefreshTokenDocument[]> {
    return this.refreshTokenModel
      .find({ userId: new Types.ObjectId(userId), revoked: false })
      .select('+tokenHash')
      .exec();
  }

  async revokeAllUserTokens(userId: string): Promise<void> {
    await this.refreshTokenModel
      .updateMany({ userId: new Types.ObjectId(userId), revoked: false }, { revoked: true })
      .exec();
  }

  async revokeUserTokenByJti(userId: string, jti: string): Promise<void> {
    await this.refreshTokenModel
      .updateOne({ userId: new Types.ObjectId(userId), jti, revoked: false }, { revoked: true })
      .exec();
  }

  async consumeUserTokenByJti(userId: string, jti: string): Promise<boolean> {
    const consumed = await this.refreshTokenModel
      .findOneAndUpdate(
        { userId: new Types.ObjectId(userId), jti, revoked: false, expiresAt: { $gt: new Date() } },
        { revoked: true },
        { new: false },
      )
      .exec();
    return !!consumed;
  }

  async findActiveUserTokenByJti(userId: string, jti: string): Promise<RefreshTokenDocument | null> {
    return this.refreshTokenModel
      .findOne({ userId: new Types.ObjectId(userId), jti, revoked: false })
      .select('+tokenHash')
      .exec();
  }

  async markCompromisedAndRevokeAll(userId: string): Promise<void> {
    await this.refreshTokenModel
      .updateMany(
        { userId: new Types.ObjectId(userId), revoked: false },
        { revoked: true, compromised: true },
      )
      .exec();
  }

  async listUserSessions(userId: string): Promise<RefreshTokenDocument[]> {
    return this.refreshTokenModel
      .find({ userId: new Types.ObjectId(userId), revoked: false, expiresAt: { $gt: new Date() } })
      .select('jti expiresAt createdAt')
      .sort({ createdAt: -1 })
      .exec();
  }

  async removeExpiredAndRevoked(userId: string): Promise<void> {
    await this.refreshTokenModel
      .deleteMany({
        userId: new Types.ObjectId(userId),
        $or: [{ revoked: true }, { expiresAt: { $lt: new Date() } }],
      })
      .exec();
  }

  async createSuperAdminRefreshToken(
    adminEmail: string,
    jti: string,
    tokenHash: string,
    expiresAt: Date,
  ): Promise<void> {
    await this.superAdminRefreshTokenModel.create({
      adminEmail: adminEmail.toLowerCase(),
      jti,
      tokenHash,
      expiresAt,
    });
  }

  async findActiveSuperAdminTokens(adminEmail: string): Promise<SuperAdminRefreshTokenDocument[]> {
    return this.superAdminRefreshTokenModel
      .find({ adminEmail: adminEmail.toLowerCase(), revoked: false })
      .select('+tokenHash')
      .exec();
  }

  async findActiveSuperAdminTokenByJti(
    adminEmail: string,
    jti: string,
  ): Promise<SuperAdminRefreshTokenDocument | null> {
    return this.superAdminRefreshTokenModel
      .findOne({ adminEmail: adminEmail.toLowerCase(), jti, revoked: false })
      .select('+tokenHash')
      .exec();
  }

  async revokeAllSuperAdminTokens(adminEmail: string): Promise<void> {
    await this.superAdminRefreshTokenModel
      .updateMany({ adminEmail: adminEmail.toLowerCase(), revoked: false }, { revoked: true })
      .exec();
  }

  async revokeSuperAdminTokenByJti(adminEmail: string, jti: string): Promise<void> {
    await this.superAdminRefreshTokenModel
      .updateOne({ adminEmail: adminEmail.toLowerCase(), jti, revoked: false }, { revoked: true })
      .exec();
  }

  async consumeSuperAdminTokenByJti(adminEmail: string, jti: string): Promise<boolean> {
    const consumed = await this.superAdminRefreshTokenModel
      .findOneAndUpdate(
        {
          adminEmail: adminEmail.toLowerCase(),
          jti,
          revoked: false,
          expiresAt: { $gt: new Date() },
        },
        { revoked: true },
        { new: false },
      )
      .exec();
    return !!consumed;
  }

  async removeExpiredAndRevokedSuperAdmin(adminEmail: string): Promise<void> {
    await this.superAdminRefreshTokenModel
      .deleteMany({
        adminEmail: adminEmail.toLowerCase(),
        $or: [{ revoked: true }, { expiresAt: { $lt: new Date() } }],
      })
      .exec();
  }

  async listSuperAdminSessions(adminEmail: string): Promise<SuperAdminRefreshTokenDocument[]> {
    return this.superAdminRefreshTokenModel
      .find({
        adminEmail: adminEmail.toLowerCase(),
        revoked: false,
        expiresAt: { $gt: new Date() },
      })
      .select('adminEmail jti expiresAt createdAt')
      .sort({ createdAt: -1 })
      .exec();
  }

  async revokeSuperAdminSessionById(adminEmail: string, sessionId: string): Promise<boolean> {
    const updated = await this.superAdminRefreshTokenModel
      .findOneAndUpdate(
        {
          _id: new Types.ObjectId(sessionId),
          adminEmail: adminEmail.toLowerCase(),
          revoked: false,
        },
        { revoked: true },
        { new: false },
      )
      .exec();

    return !!updated;
  }

  async invalidateActivePasswordResetCodes(userId: string): Promise<void> {
    await this.passwordResetCodeModel
      .updateMany(
        { userId: new Types.ObjectId(userId), used: false, expiresAt: { $gt: new Date() } },
        { used: true },
      )
      .exec();
  }

  async createPasswordResetCode(userId: string, codeHash: string, expiresAt: Date): Promise<void> {
    await this.passwordResetCodeModel.create({
      userId: new Types.ObjectId(userId),
      codeHash,
      expiresAt,
      attempts: 0,
      verified: false,
      used: false,
    });
  }

  async findLatestActivePasswordResetCode(userId: string): Promise<PasswordResetCodeDocument | null> {
    return this.passwordResetCodeModel
      .findOne({
        userId: new Types.ObjectId(userId),
        used: false,
        expiresAt: { $gt: new Date() },
      })
      .select('+codeHash')
      .sort({ createdAt: -1 })
      .exec();
  }

  async incrementPasswordResetAttempts(id: string): Promise<void> {
    await this.passwordResetCodeModel.findByIdAndUpdate(id, { $inc: { attempts: 1 } }).exec();
  }

  async markPasswordResetCodeVerified(id: string): Promise<void> {
    await this.passwordResetCodeModel.findByIdAndUpdate(id, { verified: true }).exec();
  }

  async markPasswordResetCodeUsed(id: string): Promise<void> {
    await this.passwordResetCodeModel.findByIdAndUpdate(id, { used: true }).exec();
  }

  async markPasswordResetCodeUsedByUser(userId: string): Promise<void> {
    await this.passwordResetCodeModel
      .updateMany({ userId: new Types.ObjectId(userId), used: false }, { used: true })
      .exec();
  }

  async findValidVerifiedPasswordResetCode(
    codeId: string,
    userId: string,
  ): Promise<PasswordResetCodeDocument | null> {
    return this.passwordResetCodeModel
      .findOne({
        _id: new Types.ObjectId(codeId),
        userId: new Types.ObjectId(userId),
        used: false,
        verified: true,
        expiresAt: { $gt: new Date() },
      })
      .exec();
  }

  async invalidateActiveEmailVerificationCodes(userId: string): Promise<void> {
    await this.emailVerificationCodeModel
      .updateMany(
        { userId: new Types.ObjectId(userId), used: false, expiresAt: { $gt: new Date() } },
        { used: true },
      )
      .exec();
  }

  async createEmailVerificationCode(userId: string, codeHash: string, expiresAt: Date): Promise<void> {
    await this.emailVerificationCodeModel.create({
      userId: new Types.ObjectId(userId),
      codeHash,
      expiresAt,
      attempts: 0,
      used: false,
    });
  }

  async findLatestActiveEmailVerificationCode(
    userId: string,
  ): Promise<EmailVerificationCodeDocument | null> {
    return this.emailVerificationCodeModel
      .findOne({
        userId: new Types.ObjectId(userId),
        used: false,
        expiresAt: { $gt: new Date() },
      })
      .select('+codeHash')
      .sort({ createdAt: -1 })
      .exec();
  }

  async incrementEmailVerificationAttempts(id: string): Promise<void> {
    await this.emailVerificationCodeModel.findByIdAndUpdate(id, { $inc: { attempts: 1 } }).exec();
  }

  async markEmailVerificationCodeUsed(id: string): Promise<void> {
    await this.emailVerificationCodeModel.findByIdAndUpdate(id, { used: true }).exec();
  }

  async markAllEmailVerificationCodesUsedByUser(userId: string): Promise<void> {
    await this.emailVerificationCodeModel
      .updateMany({ userId: new Types.ObjectId(userId), used: false }, { used: true })
      .exec();
  }

  async consumeValidVerifiedPasswordResetCode(
    codeId: string,
    userId: string,
  ): Promise<PasswordResetCodeDocument | null> {
    if (!Types.ObjectId.isValid(codeId) || !Types.ObjectId.isValid(userId)) {
      return null;
    }
    return this.passwordResetCodeModel
      .findOneAndUpdate(
        {
          _id: new Types.ObjectId(codeId),
          userId: new Types.ObjectId(userId),
          used: false,
          verified: true,
          expiresAt: { $gt: new Date() },
        },
        { used: true },
        { new: false },
      )
      .exec();
  }

  async upsertSignupDraft(params: {
    email: string;
    passwordHash: string;
    codeHash: string;
    codeExpiresAt: Date;
    expiresAt: Date;
  }): Promise<SignupDraftDocument> {
    return this.signupDraftModel
      .findOneAndUpdate(
        {
          email: params.email.toLowerCase(),
          consumed: false,
          expiresAt: { $gt: new Date() },
        },
        {
          $set: {
            email: params.email.toLowerCase(),
            passwordHash: params.passwordHash,
            codeHash: params.codeHash,
            codeExpiresAt: params.codeExpiresAt,
            expiresAt: params.expiresAt,
            verifiedAt: null,
            signupTokenHash: '',
            signupTokenExpiresAt: null,
            attempts: 0,
            consumed: false,
          },
        },
        { new: true, upsert: true, setDefaultsOnInsert: true },
      )
      .select('+passwordHash +codeHash +signupTokenHash')
      .exec();
  }

  async findSignupDraftWithSecrets(id: string): Promise<SignupDraftDocument | null> {
    if (!Types.ObjectId.isValid(id)) {
      return null;
    }

    return this.signupDraftModel
      .findById(id)
      .select('+passwordHash +codeHash +signupTokenHash')
      .exec();
  }

  async findSignupDraftByTokenHash(signupTokenHash: string): Promise<SignupDraftDocument | null> {
    return this.signupDraftModel
      .findOne({
        signupTokenHash,
        consumed: false,
      })
      .select('+passwordHash +codeHash +signupTokenHash')
      .exec();
  }

  async incrementSignupDraftAttempts(id: string): Promise<void> {
    await this.signupDraftModel.findByIdAndUpdate(id, { $inc: { attempts: 1 } }).exec();
  }

  async markSignupDraftVerified(
    id: string,
    signupTokenHash: string,
    signupTokenExpiresAt: Date,
  ): Promise<void> {
    await this.signupDraftModel
      .findByIdAndUpdate(id, {
        verifiedAt: new Date(),
        signupTokenHash,
        signupTokenExpiresAt,
      })
      .exec();
  }

  async markSignupDraftConsumed(id: string): Promise<void> {
    await this.signupDraftModel.findByIdAndUpdate(id, { consumed: true }).exec();
  }
}
