import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";

import { EmailService } from "@/infra/email/email.service.js";
import { AuditService, AuditAction } from "@/infra/audit/audit.service.js";
import { extractErrorMessage } from "@/core/utils/error.util.js";

import { OtpService, type OtpVerifyResult } from "../otp/otp.service.js";
import { LoginSessionService } from "../login/login-session.service.js";

/**
 * Result of OTP verification during login
 */
export interface LoginOtpVerifyResult {
  success: true;
  userId: string;
  email: string;
  role: string;
}

export interface LoginOtpVerifyError {
  success: false;
  error: string;
  attemptsRemaining?: number;
}

/**
 * Login OTP Workflow Service
 *
 * Orchestrates the OTP verification flow for login:
 * 1. After credential validation: creates session, generates OTP, sends email
 * 2. On OTP verification: validates code, returns user info for token generation
 *
 * Security features:
 * - OTP codes are 6 digits, cryptographically random
 * - 10 minute expiry (same as session)
 * - Maximum 3 verification attempts per code
 * - Session invalidated after successful verification
 */
@Injectable()
export class LoginOtpWorkflowService {
  constructor(
    private readonly config: ConfigService,
    private readonly otpService: OtpService,
    private readonly sessionService: LoginSessionService,
    private readonly emailService: EmailService,
    private readonly auditService: AuditService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Initiate OTP verification after successful credential validation
   *
   * @param user - The validated user info
   * @param fingerprint - Optional request fingerprint for session binding
   * @returns Session info for the frontend
   */
  async initiateOtp(
    user: { id: string; email: string; role: string },
    fingerprint?: string
  ): Promise<{
    sessionToken: string;
    maskedEmail: string;
    expiresAt: string;
  }> {
    const normalizedEmail = user.email.toLowerCase().trim();

    try {
      // Generate OTP and store in Redis (with fingerprint for binding)
      const code = await this.otpService.generateAndStore(normalizedEmail, fingerprint);

      // Create login session
      const { sessionToken, expiresAt } = await this.sessionService.create(user, fingerprint);

      // Send OTP email
      await this.sendLoginOtpEmail(normalizedEmail, code);

      // Audit log
      await this.auditService.logAuthEvent(AuditAction.LOGIN_OTP_SENT, user.id, {
        email: normalizedEmail,
      });

      const maskedEmail = this.maskEmail(normalizedEmail);

      this.logger.log(
        { userId: user.id, email: normalizedEmail },
        "Login OTP initiated successfully"
      );

      return {
        sessionToken,
        maskedEmail,
        expiresAt,
      };
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), userId: user.id },
        "Failed to initiate login OTP"
      );
      throw error;
    }
  }

  /**
   * Verify OTP code and return user info for token generation
   *
   * @param sessionToken - The session token from initiateOtp
   * @param code - The 6-digit OTP code entered by user
   * @param fingerprint - Optional request fingerprint for validation
   * @returns User info for token generation, or error details
   */
  async verifyOtp(
    sessionToken: string,
    code: string,
    fingerprint?: string
  ): Promise<LoginOtpVerifyResult | LoginOtpVerifyError> {
    // Validate session first
    const userInfo = await this.sessionService.validate(sessionToken, fingerprint);

    if (!userInfo) {
      this.logger.warn(
        { sessionToken },
        "Login OTP verification failed: session expired or invalid"
      );
      return {
        success: false,
        error: "Session expired. Please log in again.",
      };
    }

    // Verify OTP code
    const otpResult: OtpVerifyResult = await this.otpService.verify(
      userInfo.email,
      code,
      fingerprint
    );

    if (!otpResult.valid) {
      const errorMessage = this.getOtpErrorMessage(otpResult);

      // Invalidate session when max attempts reached for security
      if (otpResult.reason === "max_attempts") {
        // Log audit event BEFORE invalidation to ensure security event is captured
        // even if invalidation fails
        await this.auditService.logAuthEvent(
          AuditAction.LOGIN_SESSION_INVALIDATED,
          userInfo.userId,
          { email: userInfo.email, reason: "max_otp_attempts" },
          undefined,
          false
        );

        this.logger.warn(
          { userId: userInfo.userId, email: userInfo.email },
          "Login session invalidated due to max OTP attempts"
        );

        await Promise.all([
          this.sessionService.invalidate(sessionToken),
          this.otpService.invalidate(userInfo.email),
        ]);
      }

      // Audit log failed attempt
      await this.auditService.logAuthEvent(
        AuditAction.LOGIN_OTP_FAILED,
        userInfo.userId,
        {
          email: userInfo.email,
          reason: otpResult.reason,
          attemptsRemaining: otpResult.attemptsRemaining,
        },
        undefined,
        false
      );

      this.logger.warn(
        {
          userId: userInfo.userId,
          email: userInfo.email,
          reason: otpResult.reason,
          attemptsRemaining: otpResult.attemptsRemaining,
        },
        "Login OTP verification failed"
      );

      const errorResult: LoginOtpVerifyError = {
        success: false,
        error: errorMessage,
      };

      if (otpResult.attemptsRemaining !== undefined) {
        errorResult.attemptsRemaining = otpResult.attemptsRemaining;
      }

      return errorResult;
    }

    // Success - invalidate session (one-time use)
    await this.sessionService.invalidate(sessionToken);

    // Audit log success
    await this.auditService.logAuthEvent(AuditAction.LOGIN_OTP_VERIFIED, userInfo.userId, {
      email: userInfo.email,
    });

    this.logger.log(
      { userId: userInfo.userId, email: userInfo.email },
      "Login OTP verified successfully"
    );

    return {
      success: true,
      userId: userInfo.userId,
      email: userInfo.email,
      role: userInfo.role,
    };
  }

  /**
   * Send login OTP email
   */
  private async sendLoginOtpEmail(email: string, code: string): Promise<void> {
    const templateId = this.config.get<string>("EMAIL_TEMPLATE_LOGIN_OTP");

    if (templateId) {
      await this.emailService.sendEmail({
        to: email,
        subject: "Your login verification code",
        templateId,
        dynamicTemplateData: {
          code,
          expiresMinutes: "10",
        },
      });
    } else {
      // Fallback to plain HTML
      await this.emailService.sendEmail({
        to: email,
        subject: "Your login verification code",
        html: `
          <p>Your login verification code is: <strong>${code}</strong></p>
          <p>This code expires in 10 minutes.</p>
          <p>If you didn't attempt to log in, please secure your account immediately.</p>
        `,
      });
    }
  }

  /**
   * Mask email for display (e.g., j***e@e***.com)
   *
   * Handles various email lengths:
   * - 1-2 chars: all asterisks (a@b.com -> **@***.com)
   * - 3-4 chars: first char visible (abc@d.com -> a***@***.com)
   * - 5+ chars: first and last visible (abcde@example.com -> a***e@e***.com)
   */
  private maskEmail(email: string): string {
    const [local, domain] = email.split("@");
    if (!local || !domain) return "***@***.***";

    // Mask local part based on length
    let maskedLocal: string;
    if (local.length <= 2) {
      maskedLocal = "*".repeat(local.length);
    } else if (local.length <= 4) {
      maskedLocal = `${local[0]}${"*".repeat(local.length - 1)}`;
    } else {
      maskedLocal = `${local[0]}***${local.at(-1)}`;
    }

    // Mask domain (before TLD)
    const lastDot = domain.lastIndexOf(".");
    const domainName = lastDot > 0 ? domain.slice(0, lastDot) : domain;
    const tld = lastDot > 0 ? domain.slice(lastDot) : "";

    const maskedDomain = domainName.length <= 2 ? "***" : `${domainName[0]}***`;

    return `${maskedLocal}@${maskedDomain}${tld}`;
  }

  /**
   * Get user-friendly error message for OTP verification failure
   */
  private getOtpErrorMessage(result: OtpVerifyResult): string {
    switch (result.reason) {
      case "expired":
        return "Code expired. Please log in again to receive a new code.";
      case "max_attempts":
        return "Too many failed attempts. Please log in again to receive a new code.";
      case "invalid":
        return result.attemptsRemaining !== undefined && result.attemptsRemaining > 0
          ? `Invalid code. ${result.attemptsRemaining} attempt${result.attemptsRemaining === 1 ? "" : "s"} remaining.`
          : "Invalid code. Please try again.";
      default:
        return "Verification failed. Please try again.";
    }
  }
}
