import {
  BadRequestException,
  Inject,
  Injectable,
  InternalServerErrorException,
  UnauthorizedException,
  NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import * as argon2 from "argon2";
import type { Request } from "express";
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";
import { EmailService } from "@bff/infra/email/email.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { AuthTokenService } from "../token/token.service.js";
import { AuthRateLimitService } from "../rate-limiting/auth-rate-limit.service.js";
import { PasswordResetTokenService } from "../token/password-reset-token.service.js";
import { TrustedDeviceService } from "../trusted-device/trusted-device.service.js";
import {
  type ChangePasswordRequest,
  changePasswordRequestSchema,
} from "@customer-portal/domain/auth";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";

@Injectable()
export class PasswordWorkflowService {
  constructor(
    private readonly usersService: UsersService,
    private readonly auditService: AuditService,
    private readonly configService: ConfigService,
    private readonly emailService: EmailService,
    private readonly passwordResetTokenService: PasswordResetTokenService,
    private readonly tokenService: AuthTokenService,
    private readonly authRateLimitService: AuthRateLimitService,
    private readonly trustedDeviceService: TrustedDeviceService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async checkPasswordNeeded(email: string) {
    const user = await this.usersService.findByEmailInternal(email);
    if (!user) {
      return { needsPasswordSet: false, userExists: false };
    }

    return {
      needsPasswordSet: !user.passwordHash,
      userExists: true,
      email: user.email,
    };
  }

  async setPassword(email: string, password: string) {
    const user = await this.usersService.findByEmailInternal(email);
    if (!user) {
      throw new UnauthorizedException("User not found");
    }

    if (user.passwordHash) {
      throw new BadRequestException("User already has a password set");
    }

    const passwordHash = await argon2.hash(password);

    return safeOperation(
      async () => {
        try {
          await this.usersService.update(user.id, { passwordHash });
        } catch (error) {
          const message = extractErrorMessage(error);
          // Avoid surfacing downstream WHMCS mapping lookups as system errors during setup
          if (
            error instanceof NotFoundException &&
            /whmcs client mapping not found/i.test(message)
          ) {
            this.logger.warn(
              "Password set succeeded but WHMCS mapping missing; returning auth user",
              {
                userId: user.id,
                email,
              }
            );
          } else {
            throw error;
          }
        }
        const prismaUser = await this.usersService.findByIdInternal(user.id);
        if (!prismaUser) {
          throw new InternalServerErrorException("Failed to load user after password setup");
        }
        const userProfile = mapPrismaUserToDomain(prismaUser);

        const tokens = await this.tokenService.generateTokenPair({
          id: userProfile.id,
          email: userProfile.email,
        });

        return {
          user: userProfile,
          tokens,
        };
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Set password for user ${user.id}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException, InternalServerErrorException],
        fallbackMessage: "Failed to set password",
      }
    );
  }
  async requestPasswordReset(email: string, request?: Request): Promise<void> {
    if (request) {
      await this.authRateLimitService.consumePasswordReset(request);
    }
    const user = await this.usersService.findByEmailInternal(email);
    if (!user) {
      // Don't reveal whether user exists
      return;
    }

    // Create single-use password reset token
    const token = await this.passwordResetTokenService.create(user.id);

    const appBase = this.configService.get<string>("APP_BASE_URL", "http://localhost:3000");
    const resetUrl = `${appBase}/auth/reset-password?token=${encodeURIComponent(token)}`;
    const templateId = this.configService.get<string>("EMAIL_TEMPLATE_RESET");

    if (templateId) {
      await this.emailService.sendEmail({
        to: email,
        subject: "Reset your password",
        templateId,
        dynamicTemplateData: { resetUrl },
      });
    } else {
      await this.emailService.sendEmail({
        to: email,
        subject: "Reset your Assist Solutions password",
        html: `
          <p>We received a request to reset your password.</p>
          <p><a href="${resetUrl}">Click here to reset your password</a>. This link expires in 15 minutes.</p>
          <p>If you didn't request this, you can safely ignore this email.</p>
        `,
      });
    }
  }

  async resetPassword(token: string, newPassword: string): Promise<void> {
    // Consume the token (single-use enforcement)
    // This will throw BadRequestException if token is invalid, expired, or already used
    const { userId } = await this.passwordResetTokenService.consume(token);

    return safeOperation(
      async () => {
        const prismaUser = await this.usersService.findByIdInternal(userId);
        if (!prismaUser) throw new BadRequestException("Invalid token");

        const passwordHash = await argon2.hash(newPassword);

        await this.usersService.update(prismaUser.id, { passwordHash });
        const freshUser = await this.usersService.findByIdInternal(prismaUser.id);
        if (!freshUser) {
          throw new InternalServerErrorException("Failed to load user after password reset");
        }
        // Force re-login everywhere after password reset
        await this.tokenService.revokeAllUserTokens(freshUser.id);
        // Revoke all trusted devices - attacker could have set up a trusted device
        await this.trustedDeviceService.revokeAllUserDevices(freshUser.id);
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: "Reset password",
        logger: this.logger,
        rethrow: [BadRequestException, InternalServerErrorException],
        fallbackMessage: "Failed to reset password",
      }
    );
  }

  async changePassword(
    userId: string,
    data: ChangePasswordRequest,
    request?: Request
  ): Promise<AuthResultInternal> {
    const user = await this.usersService.findByIdInternal(userId);

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

    if (!user.passwordHash) {
      throw new BadRequestException("No password set. Please set a password first.");
    }

    const parsed = changePasswordRequestSchema.parse(data);
    const { currentPassword, newPassword } = parsed;

    const isCurrentValid = await argon2.verify(user.passwordHash, currentPassword);
    if (!isCurrentValid) {
      await this.auditService.logAuthEvent(
        AuditAction.PASSWORD_CHANGE,
        user.id,
        { action: "change_password", reason: "invalid_current_password" },
        request,
        false,
        "Invalid current password"
      );
      throw new BadRequestException("Current password is incorrect");
    }

    // Password validation is handled by changePasswordRequestSchema (uses passwordSchema from domain)
    // No need for duplicate validation here

    const passwordHash = await argon2.hash(newPassword);

    return safeOperation(
      async () => {
        await this.usersService.update(user.id, { passwordHash });
        const prismaUser = await this.usersService.findByIdInternal(user.id);
        if (!prismaUser) {
          throw new InternalServerErrorException("Failed to load user after password change");
        }
        const userProfile = mapPrismaUserToDomain(prismaUser);

        await this.auditService.logAuthEvent(
          AuditAction.PASSWORD_CHANGE,
          user.id,
          { action: "change_password" },
          request,
          true
        );

        // Revoke existing refresh tokens before issuing new pair (logout other sessions)
        await this.tokenService.revokeAllUserTokens(userProfile.id);
        // Revoke all trusted devices - user is changing password, should re-verify devices
        await this.trustedDeviceService.revokeAllUserDevices(userProfile.id);

        const tokens = await this.tokenService.generateTokenPair({
          id: userProfile.id,
          email: userProfile.email,
        });
        return {
          user: userProfile,
          tokens,
        };
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Change password for user ${user.id}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException, InternalServerErrorException],
        fallbackMessage: "Failed to change password",
      }
    );
  }
}
