import { type SecretReference, type User, type PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { createRandomStringGenerator } from "@better-auth/utils/random";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { createHash } from "@better-auth/utils/hash";
import { createOTP } from "@better-auth/utils/otp";
import { base32 } from "@better-auth/utils/base32";
import { z } from "zod";
import { scheduleEmail } from "../scheduleEmail.server";

const generateRandomString = createRandomStringGenerator("A-Z", "0-9");

const SecretSchema = z.object({
  secret: z.string(),
});

export class MultiFactorAuthenticationService {
  #prismaClient: PrismaClient;

  constructor(prismaClient: PrismaClient = prisma) {
    this.#prismaClient = prismaClient;
  }

  public async disableTotp(userId: string, params: { totpCode?: string; recoveryCode?: string }) {
    const user = await this.#prismaClient.user.findFirst({
      where: { id: userId },
      include: {
        mfaSecretReference: true,
      },
    });

    if (!user) {
      return {
        success: false,
      };
    }

    if (!user.mfaEnabledAt) {
      return {
        success: false,
      };
    }

    if (!user.mfaSecretReference) {
      return {
        success: false,
      };
    }

    // validate the TOTP code
    const secretStore = getSecretStore(user.mfaSecretReference.provider);
    const secretResult = await secretStore.getSecret(SecretSchema, user.mfaSecretReference.key);

    if (!secretResult) {
      return {
        success: false,
      };
    }

    const isValid = await this.#verifyTotpCodeOrRecoveryCode(
      user,
      user.mfaSecretReference,
      params.totpCode,
      params.recoveryCode
    );

    if (!isValid) {
      return {
        success: false,
      };
    }

    // Delete the MFA secret
    await secretStore.deleteSecret(user.mfaSecretReference.key);

    // Delete the MFA backup codes
    await this.#prismaClient.mfaBackupCode.deleteMany({
      where: {
        userId,
      },
    });

    await this.#prismaClient.user.update({
      where: { id: userId },
      data: {
        mfaEnabledAt: null,
        mfaSecretReference: {
          delete: true,
        },
      },
    });

    await scheduleEmail({
      email: "mfa-disabled",
      to: user.email,
      userEmail: user.email,
    });

    return {
      success: true,
    };
  }

  public async enableTotp(userId: string) {
    const user = await this.#prismaClient.user.findFirst({
      where: { id: userId },
    });

    if (!user) {
      throw new ServiceValidationError("User not found");
    }

    const secretStore = getSecretStore("DATABASE");

    // Generate a new secret
    const secret = generateRandomString(24);
    const secretKey = `mfa:${userId}:${generateRandomString(8)}`;

    // Store the secret in the SecretStore
    await secretStore.setSecret(secretKey, {
      secret,
    });

    // Update the user's secret reference to the secret store
    await this.#prismaClient.user.update({
      where: { id: userId },
      data: {
        mfaSecretReference: {
          create: {
            provider: "DATABASE",
            key: secretKey,
          },
        },
      },
    });

    // Return the secret and the recovery codes
    const otpAuthUrl = createOTP(secret).url("trigger.dev", user.email);

    const displaySecret = base32.encode(secret, {
      padding: false,
    });

    return {
      secret: displaySecret,
      otpAuthUrl,
    };
  }

  public async validateTotpSetup(userId: string, totpCode: string) {
    const user = await this.#prismaClient.user.findFirst({
      where: { id: userId },
      include: {
        mfaSecretReference: true,
      },
    });

    if (!user) {
      throw new ServiceValidationError("User not found");
    }

    if (!user.mfaSecretReference) {
      throw new ServiceValidationError("User has not enabled MFA");
    }

    const secretStore = getSecretStore(user.mfaSecretReference.provider);
    const secretResult = await secretStore.getSecret(SecretSchema, user.mfaSecretReference.key);

    if (!secretResult) {
      throw new ServiceValidationError("User has not enabled MFA");
    }

    const secret = secretResult.secret;

    const otp = createOTP(secret, {
      digits: 6,
      period: 30,
    });

    const isValid = await otp.verify(totpCode);

    if (!isValid) {
      // Return the secret and the recovery codes
      const otpAuthUrl = createOTP(secret).url("trigger.dev", user.email);

      const displaySecret = base32.encode(secret, {
        padding: false,
      });

      return {
        success: false,
        otpAuthUrl,
        secret: displaySecret,
      };
    }

    // Now that we've validated the TOTP code, we can enable MFA for the user
    await this.#prismaClient.user.update({
      where: { id: userId },
      data: {
        mfaEnabledAt: new Date(),
      },
    });

    // Generate a new set of recovery codes
    const recoveryCodes = Array.from({ length: 9 }, () => generateRandomString(16, "a-z", "0-9"));

    // Delete any existing recovery codes
    await this.#prismaClient.mfaBackupCode.deleteMany({
      where: {
        userId,
      },
    });

    // Hash and store the recovery codes
    for (const code of recoveryCodes) {
      const hashedCode = await createHash("SHA-512", "hex").digest(code);
      await this.#prismaClient.mfaBackupCode.create({
        data: {
          userId,
          code: hashedCode,
        },
      });
    }

    await scheduleEmail({
      email: "mfa-enabled",
      to: user.email,
      userEmail: user.email,
    });

    return {
      success: true,
      recoveryCodes,
    };
  }

  async #verifyTotpCodeOrRecoveryCode(
    user: User,
    secretReference: SecretReference,
    totpCode?: string,
    recoveryCode?: string
  ) {
    if (!totpCode && !recoveryCode) {
      return false;
    }

    if (typeof totpCode === "string" && totpCode.length === 6) {
      return this.#verifyTotpCode(user, secretReference, totpCode);
    }

    if (typeof recoveryCode === "string") {
      return this.#verifyRecoveryCode(user, recoveryCode);
    }

    return false;
  }

  async #verifyTotpCode(user: User, secretReference: SecretReference, totpCode: string) {
    const secretStore = getSecretStore(secretReference.provider);
    const secretResult = await secretStore.getSecret(SecretSchema, secretReference.key);

    if (!secretResult) {
      return false;
    }

    const secret = secretResult.secret;

    const isValid = await createOTP(secret, {
      digits: 6,
      period: 30,
    }).verify(totpCode);

    return isValid;
  }

  async #verifyRecoveryCode(user: User, recoveryCode: string) {
    const hashedCode = await createHash("SHA-512", "hex").digest(recoveryCode);

    const backupCode = await this.#prismaClient.mfaBackupCode.findFirst({
      where: { userId: user.id, code: hashedCode, usedAt: null },
    });

    return !!backupCode;
  }

  // Public methods for login flow with security measures
  public async verifyTotpForLogin(userId: string, totpCode: string) {
    const user = await this.#prismaClient.user.findFirst({
      where: { id: userId },
      include: {
        mfaSecretReference: true,
      },
    });

    if (!user || !user.mfaEnabledAt || !user.mfaSecretReference) {
      return {
        success: false,
        error: "Invalid authentication code",
      };
    }

    // Check for replay attack - if this code was already used
    const hashedCode = await createHash("SHA-512", "hex").digest(totpCode);
    if (user.mfaLastUsedCode === hashedCode) {
      return {
        success: false,
        error: "Invalid authentication code",
      };
    }

    // Verify the TOTP code
    const isValid = await this.#verifyTotpCode(user, user.mfaSecretReference, totpCode);

    if (!isValid) {
      return {
        success: false,
        error: "Invalid authentication code",
      };
    }

    // Mark this code as used to prevent replay
    await this.#prismaClient.user.update({
      where: { id: userId },
      data: {
        mfaLastUsedCode: hashedCode,
      },
    });

    return {
      success: true,
    };
  }

  public async verifyRecoveryCodeForLogin(userId: string, recoveryCode: string) {
    const user = await this.#prismaClient.user.findFirst({
      where: { id: userId },
    });

    if (!user || !user.mfaEnabledAt) {
      return {
        success: false,
        error: "Invalid authentication code",
      };
    }

    const hashedCode = await createHash("SHA-512", "hex").digest(recoveryCode);

    // Find an unused recovery code
    const backupCode = await this.#prismaClient.mfaBackupCode.findFirst({
      where: {
        userId: user.id,
        code: hashedCode,
        usedAt: null,
      },
    });

    if (!backupCode) {
      return {
        success: false,
        error: "Invalid authentication code",
      };
    }

    // Mark this recovery code as used
    await this.#prismaClient.mfaBackupCode.update({
      where: { id: backupCode.id },
      data: {
        usedAt: new Date(),
      },
    });

    return {
      success: true,
    };
  }
}
