import {
  BadRequestException,
  Inject,
  Injectable,
  UnauthorizedException,
  NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Logger } from "nestjs-pino";
import * as bcrypt from "bcrypt";
import type { Request } from "express";
import { UsersFacade } from "@bff/modules/users/application/users.facade";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service";
import { EmailService } from "@bff/infra/email/email.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { AuthTokenService } from "../../token/token.service";
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service";
import {
  type PasswordChangeResult,
  type ChangePasswordRequest,
  changePasswordRequestSchema,
} from "@customer-portal/domain/auth";
import { mapPrismaUserToDomain } from "@bff/infra/mappers";

@Injectable()
export class PasswordWorkflowService {
  constructor(
    private readonly usersFacade: UsersFacade,
    private readonly auditService: AuditService,
    private readonly configService: ConfigService,
    private readonly emailService: EmailService,
    private readonly jwtService: JwtService,
    private readonly tokenService: AuthTokenService,
    private readonly authRateLimitService: AuthRateLimitService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async checkPasswordNeeded(email: string) {
    const user = await this.usersFacade.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.usersFacade.findByEmailInternal(email);
    if (!user) {
      throw new UnauthorizedException("User not found");
    }

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

    const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 12);
    const saltRounds =
      typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
    const passwordHash = await bcrypt.hash(password, saltRounds);
    try {
      await this.usersFacade.update(user.id, { passwordHash });
    } catch (error) {
      const message = getErrorMessage(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.usersFacade.findByIdInternal(user.id);
    if (!prismaUser) {
      throw new Error("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,
    };
  }
  async requestPasswordReset(email: string, request?: Request): Promise<void> {
    if (request) {
      await this.authRateLimitService.consumePasswordReset(request);
    }
    const user = await this.usersFacade.findByEmailInternal(email);
    if (!user) {
      return;
    }

    const token = this.jwtService.sign(
      { sub: user.id, purpose: "password_reset" },
      { expiresIn: "15m" }
    );

    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<PasswordChangeResult> {
    try {
      const payload = this.jwtService.verify<{ sub: string; purpose: string }>(token);
      if (payload.purpose !== "password_reset") {
        throw new BadRequestException("Invalid token");
      }

      const prismaUser = await this.usersFacade.findByIdInternal(payload.sub);
      if (!prismaUser) throw new BadRequestException("Invalid token");

      const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 12);
      const saltRounds =
        typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
      const passwordHash = await bcrypt.hash(newPassword, saltRounds);

      await this.usersFacade.update(prismaUser.id, { passwordHash });
      const freshUser = await this.usersFacade.findByIdInternal(prismaUser.id);
      if (!freshUser) {
        throw new Error("Failed to load user after password reset");
      }
      const userProfile = mapPrismaUserToDomain(freshUser);

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

      return {
        user: userProfile,
        tokens,
      };
    } catch (error: unknown) {
      this.logger.error("Reset password failed", { error: getErrorMessage(error) });
      throw new BadRequestException("Invalid or expired token");
    }
  }

  async changePassword(
    userId: string,
    data: ChangePasswordRequest,
    request?: Request
  ): Promise<PasswordChangeResult> {
    const user = await this.usersFacade.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 bcrypt.compare(currentPassword, user.passwordHash);
    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 saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 14);
    const saltRounds =
      typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
    const passwordHash = await bcrypt.hash(newPassword, saltRounds);

    await this.usersFacade.update(user.id, { passwordHash });
    const prismaUser = await this.usersFacade.findByIdInternal(user.id);
    if (!prismaUser) {
      throw new Error("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
    );

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