import { BadRequestException, ConflictException, Inject, Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import * as argon2 from "argon2";

import { DistributedLockService } from "@bff/infra/cache/distributed-lock.service.js";

import {
  ACCOUNT_STATUS,
  type AccountStatus,
  type SendVerificationCodeRequest,
  type SendVerificationCodeResponse,
  type VerifyCodeRequest,
  type VerifyCodeResponse,
  type BilingualEligibilityAddress,
  type GuestEligibilityRequest,
  type GuestEligibilityResponse,
  type CompleteAccountRequest,
  type SignupWithEligibilityRequest,
  type MigrateWhmcsAccountRequest,
} from "@customer-portal/domain/get-started";
import {
  getCustomFieldValue,
  serializeWhmcsKeyValueMap,
} from "@customer-portal/domain/customer/providers";

import { EmailService } from "@bff/infra/email/email.service.js";
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";
import { SalesforceAccountService } from "@bff/integrations/salesforce/services/salesforce-account.service.js";
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js";
import { WorkflowCaseManager } from "@bff/modules/shared/workflow/index.js";
import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/whmcs-account-discovery.service.js";
import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-client.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";

import { OtpService } from "../otp/otp.service.js";
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
import { AuthTokenService } from "../token/token.service.js";
import { SignupWhmcsService } from "./signup/signup-whmcs.service.js";
import { SignupUserCreationService } from "./signup/signup-user-creation.service.js";
import {
  PORTAL_SOURCE_NEW_SIGNUP,
  PORTAL_SOURCE_INTERNET_ELIGIBILITY,
  PORTAL_SOURCE_MIGRATED,
  PORTAL_STATUS_ACTIVE,
  PORTAL_STATUS_NOT_YET,
} from "@bff/modules/auth/constants/portal.constants.js";
import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforce.facade.js";

/** Error message for failed user load - used in multiple places */
const ERROR_FAILED_TO_LOAD_USER = "Failed to load created user";

/**
 * Remove undefined properties from an object (for exactOptionalPropertyTypes compatibility)
 */
function removeUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
  return Object.fromEntries(
    Object.entries(obj).filter(([, value]) => value !== undefined)
  ) as Partial<T>;
}

/**
 * Get Started Workflow Service
 *
 * Orchestrates the unified "Get Started" flow:
 * 1. Email verification via OTP
 * 2. Account status detection (Portal, WHMCS, SF)
 * 3. Guest eligibility check (creates SF Account + Case without OTP)
 * 4. Account completion for SF-only users
 * 5. Full signup with eligibility (SF + Case + WHMCS + Portal)
 */
@Injectable()
export class GetStartedWorkflowService {
  // eslint-disable-next-line max-params -- NestJS DI requires individual constructor parameters
  constructor(
    private readonly config: ConfigService,
    private readonly otpService: OtpService,
    private readonly sessionService: GetStartedSessionService,
    private readonly emailService: EmailService,
    private readonly usersService: UsersService,
    private readonly mappingsService: MappingsService,
    private readonly auditService: AuditService,
    private readonly salesforceAccountService: SalesforceAccountService,
    private readonly salesforceService: SalesforceFacade,
    private readonly opportunityResolution: OpportunityResolutionService,
    private readonly workflowCases: WorkflowCaseManager,
    private readonly whmcsDiscovery: WhmcsAccountDiscoveryService,
    private readonly whmcsClientService: WhmcsClientService,
    private readonly whmcsSignup: SignupWhmcsService,
    private readonly userCreation: SignupUserCreationService,
    private readonly tokenService: AuthTokenService,
    private readonly lockService: DistributedLockService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  // ============================================================================
  // Email Verification
  // ============================================================================

  /**
   * Send OTP verification code to email
   *
   * @param request - The request containing the email
   * @param fingerprint - Optional request fingerprint for session binding
   */
  async sendVerificationCode(
    request: SendVerificationCodeRequest,
    fingerprint?: string
  ): Promise<SendVerificationCodeResponse> {
    const { email } = request;
    const normalizedEmail = email.toLowerCase().trim();

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

      // Create session for this verification flow (token returned in verifyCode)
      await this.sessionService.create(normalizedEmail);

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

      this.logger.log({ email: normalizedEmail }, "OTP verification code sent");

      return {
        sent: true,
        message: "Verification code sent to your email",
      };
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: normalizedEmail },
        "Failed to send verification code"
      );

      return {
        sent: false,
        message: "Failed to send verification code. Please try again.",
      };
    }
  }

  /**
   * Verify OTP code and determine account status
   *
   * @param request - The request containing email and code
   * @param fingerprint - Optional request fingerprint for session binding check
   */
  async verifyCode(request: VerifyCodeRequest, fingerprint?: string): Promise<VerifyCodeResponse> {
    const { email, code, handoffToken } = request;
    const normalizedEmail = email.toLowerCase().trim();

    // Verify OTP (with fingerprint check - logs warning if different context)
    const otpResult = await this.otpService.verify(normalizedEmail, code, fingerprint);

    if (!otpResult.valid) {
      return this.buildOtpErrorResponse(otpResult);
    }

    // Create verified session
    const sessionToken = await this.sessionService.create(normalizedEmail);

    // Check account status across all systems
    const accountStatus = await this.determineAccountStatus(normalizedEmail);

    // Get prefill data (including handoff token data if provided)
    const prefill = await this.resolvePrefillData(normalizedEmail, accountStatus, handoffToken);

    // Update session with verified status and account info
    const prefillData = this.buildSessionPrefillData(prefill, accountStatus);
    await this.sessionService.markEmailVerified(
      sessionToken,
      accountStatus.status,
      Object.keys(prefillData).length > 0 ? prefillData : undefined
    );

    this.logger.log(
      { email: normalizedEmail, accountStatus: accountStatus.status, hasHandoff: !!handoffToken },
      "Email verified and account status determined"
    );

    return {
      verified: true,
      sessionToken,
      accountStatus: accountStatus.status,
      prefill,
    };
  }

  // ============================================================================
  // Guest Eligibility Check (No OTP Required)
  // ============================================================================

  /**
   * Guest eligibility check - NO email verification required
   *
   * Allows users to check availability without verifying email first.
   * Creates SF Account + eligibility case immediately.
   * Email verification happens later when user creates an account.
   *
   * Security:
   * - Email-level lock prevents concurrent requests creating duplicate SF accounts
   *
   * @param request - Guest eligibility request with name, email, address
   * @param fingerprint - Request fingerprint for logging/abuse detection
   */
  async guestEligibilityCheck(
    request: GuestEligibilityRequest,
    fingerprint?: string
  ): Promise<GuestEligibilityResponse> {
    const { email, firstName, lastName, address, phone, continueToAccount } = request;
    const normalizedEmail = email.toLowerCase().trim();

    this.logger.log(
      { email: normalizedEmail, continueToAccount, fingerprint },
      "Guest eligibility check initiated"
    );

    // Email-level lock to prevent concurrent requests for the same email
    const lockKey = `guest-eligibility:${normalizedEmail}`;

    try {
      return await this.lockService.withLock(
        lockKey,
        async () => {
          // Check if SF account already exists for this email
          let sfAccountId: string;

          const existingSf = await this.salesforceAccountService.findByEmail(normalizedEmail);

          if (existingSf) {
            sfAccountId = existingSf.id;
            this.logger.log(
              { email: normalizedEmail, sfAccountId },
              "Using existing SF account for guest eligibility check"
            );

            // Set portal source for existing accounts going through eligibility
            // This ensures we can track that they came through internet eligibility
            await this.salesforceService.updateAccountPortalFields(sfAccountId, {
              status: PORTAL_STATUS_NOT_YET,
              source: PORTAL_SOURCE_INTERNET_ELIGIBILITY,
            });
          } else {
            // Create new SF Account with portal fields set to "Not Yet" + "Internet Eligibility"
            const { accountId } = await this.salesforceAccountService.createAccount({
              firstName,
              lastName,
              email: normalizedEmail,
              phone: phone ?? "",
              portalSource: PORTAL_SOURCE_INTERNET_ELIGIBILITY,
            });
            sfAccountId = accountId;
            this.logger.log(
              { email: normalizedEmail, sfAccountId },
              "Created SF account for guest eligibility check"
            );
          }

          // Save Japanese address to SF Contact (if Japanese address fields provided)
          if (address.prefectureJa || address.cityJa || address.townJa) {
            await this.salesforceAccountService.updateContactAddress(sfAccountId, {
              mailingStreet: `${address.townJa || ""}${address.streetAddress || ""}`.trim(),
              mailingCity: address.cityJa || address.city,
              mailingState: address.prefectureJa || address.state,
              mailingPostalCode: address.postcode,
              mailingCountry: "Japan",
              buildingName: address.buildingName ?? null,
              roomNumber: address.roomNumber ?? null,
            });
            this.logger.debug({ sfAccountId }, "Updated SF Contact with Japanese address");
          }

          // Create eligibility case
          const { caseId } = await this.createEligibilityCase(sfAccountId, address);

          // Update Account eligibility status to Pending
          this.updateAccountEligibilityStatus(sfAccountId);

          // If user wants to continue to account creation, generate a handoff token
          let handoffToken: string | undefined;
          if (continueToAccount) {
            handoffToken = await this.sessionService.createGuestHandoffToken(normalizedEmail, {
              firstName,
              lastName,
              address: removeUndefined(address),
              ...(phone && { phone }),
              sfAccountId,
            });
            this.logger.debug(
              { email: normalizedEmail, handoffToken },
              "Created handoff token for account creation"
            );
          }

          return {
            submitted: true,
            requestId: caseId,
            sfAccountId,
            handoffToken,
            message: "Eligibility check submitted. We'll notify you of the results.",
          };
        },
        { ttlMs: 30_000 } // 30 second lock timeout
      );
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: normalizedEmail },
        "Guest eligibility check failed"
      );

      return {
        submitted: false,
        message: "Failed to submit eligibility check. Please try again.",
      };
    }
  }

  // ============================================================================
  // Account Completion (SF-Only Users)
  // ============================================================================

  /**
   * Complete account for users with SF account but no WHMCS/Portal
   * Creates WHMCS client and Portal user, links to existing SF account
   *
   * Security:
   * - Session is locked to prevent double submissions
   * - Email-level lock prevents concurrent account creation
   */
  async completeAccount(request: CompleteAccountRequest): Promise<AuthResultInternal> {
    const sessionResult = await this.sessionService.acquireAndMarkAsUsed(
      request.sessionToken,
      "complete_account"
    );

    if (!sessionResult.success) {
      throw new BadRequestException(sessionResult.reason);
    }

    const session = sessionResult.session;
    this.validateCompleteAccountRequest(request, session);

    const lockKey = `complete-account:${session.email}`;

    try {
      return await this.lockService.withLock(
        lockKey,
        async () => this.executeCompleteAccount(request, session),
        { ttlMs: 60_000 }
      );
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: session.email },
        "Account completion failed"
      );
      throw error;
    }
  }

  /**
   * Validate complete account request
   */
  private validateCompleteAccountRequest(
    request: CompleteAccountRequest,
    session: { sfAccountId?: string | undefined }
  ): void {
    const isNewCustomer = !session.sfAccountId;

    if (!isNewCustomer) {
      return;
    }

    if (!request.firstName || !request.lastName) {
      throw new BadRequestException("First name and last name are required for new accounts.");
    }
    if (!request.address) {
      throw new BadRequestException("Address is required for new accounts.");
    }
  }

  /**
   * Execute the account completion within the lock
   */
  private async executeCompleteAccount(
    request: CompleteAccountRequest,
    session: {
      email: string;
      sfAccountId?: string | undefined;
      firstName?: string | undefined;
      lastName?: string | undefined;
      address?: Record<string, string | undefined> | undefined;
    }
  ): Promise<AuthResultInternal> {
    const {
      password,
      phone,
      dateOfBirth,
      gender,
      firstName,
      lastName,
      address: requestAddress,
    } = request;
    const isNewCustomer = !session.sfAccountId;

    // Check for existing accounts
    await this.ensureNoExistingAccountsForCompletion(session.email);

    // Resolve SF account
    const { sfAccountId, customerNumber } = await this.resolveSalesforceAccount({
      email: session.email,
      existingAccountId: session.sfAccountId,
      firstName,
      lastName,
      phone,
      portalSource: PORTAL_SOURCE_NEW_SIGNUP,
    });

    // Validate and resolve address/name
    const address = this.validateAndResolveAddress(requestAddress, session.address);
    const { finalFirstName, finalLastName } = this.resolveNames(firstName, lastName, session);

    // Create WHMCS client
    const whmcsClient = await this.whmcsSignup.createClient({
      firstName: finalFirstName,
      lastName: finalLastName,
      email: session.email,
      password,
      phone,
      address: {
        address1: address.address1,
        ...(address.address2 && { address2: address.address2 }),
        city: address.city,
        state: address.state,
        postcode: address.postcode,
        country: address.country ?? "Japan",
      },
      customerNumber: customerNumber ?? null,
      dateOfBirth,
      gender,
    });

    // Create user and get auth result
    const passwordHash = await argon2.hash(password);
    const authResult = await this.createUserWithAuthResult({
      email: session.email,
      passwordHash,
      whmcsClientId: whmcsClient.clientId,
      sfAccountId,
      auditSource: isNewCustomer ? "get_started_new_customer" : "get_started_complete_account",
    });

    // Finalize
    await this.updateSalesforcePortalFlags(sfAccountId, whmcsClient.clientId);
    await this.sessionService.invalidate(request.sessionToken);

    this.logger.log(
      { email: session.email, userId: authResult.user.id, isNewCustomer },
      "Account completed successfully"
    );

    return authResult;
  }

  /**
   * Ensure no existing Portal or WHMCS accounts exist (throws on conflict)
   */
  private async ensureNoExistingAccountsForCompletion(email: string): Promise<void> {
    const { portalExists, whmcsExists } = await this.checkExistingAccounts(email);

    if (whmcsExists) {
      throw new ConflictException(
        "A billing account already exists. Please use the account migration flow."
      );
    }
    if (portalExists) {
      throw new ConflictException("An account already exists. Please log in.");
    }
  }

  /**
   * Validate and resolve address from request or session
   */
  private validateAndResolveAddress(
    requestAddress: CompleteAccountRequest["address"] | undefined,
    sessionAddress: Record<string, string | undefined> | undefined
  ): NonNullable<CompleteAccountRequest["address"]> {
    const address = requestAddress ?? sessionAddress;

    if (!address || !address.address1 || !address.city || !address.state || !address.postcode) {
      throw new BadRequestException(
        "Address information is incomplete. Please ensure all required fields (address, city, prefecture, postcode) are provided."
      );
    }

    // After validation, we know all required fields are present
    return address as NonNullable<CompleteAccountRequest["address"]>;
  }

  /**
   * Resolve names from request or session
   */
  private resolveNames(
    firstName: string | undefined,
    lastName: string | undefined,
    session: { firstName?: string | undefined; lastName?: string | undefined }
  ): { finalFirstName: string; finalLastName: string } {
    const finalFirstName = firstName ?? session.firstName;
    const finalLastName = lastName ?? session.lastName;

    if (!finalFirstName || !finalLastName) {
      throw new BadRequestException("Name information is missing. Please provide your name.");
    }

    return { finalFirstName, finalLastName };
  }

  // ============================================================================
  // Full Signup with Eligibility (Inline Flow)
  // ============================================================================

  /**
   * Full signup with eligibility check - creates everything in one operation
   *
   * This is the primary signup path from the eligibility check page.
   * Creates SF Account + Case + WHMCS + Portal after email verification.
   *
   * Security:
   * - Session is locked to prevent double submissions (race condition protection)
   * - Email-level lock prevents concurrent signups for the same email
   * - Session is invalidated on success, cleared on partial failure for retry
   *
   * @param request - Signup request with all required data
   */
  async signupWithEligibility(request: SignupWithEligibilityRequest): Promise<{
    success: boolean;
    message?: string;
    eligibilityRequestId?: string;
    authResult?: AuthResultInternal;
  }> {
    const sessionResult = await this.sessionService.acquireAndMarkAsUsed(
      request.sessionToken,
      "signup_with_eligibility"
    );

    if (!sessionResult.success) {
      return { success: false, message: sessionResult.reason };
    }

    const normalizedEmail = sessionResult.session.email;
    this.logger.log({ email: normalizedEmail }, "Starting signup with eligibility");

    const lockKey = `signup-email:${normalizedEmail}`;

    try {
      return await this.lockService.withLock(
        lockKey,
        async () => this.executeSignupWithEligibility(request, normalizedEmail),
        { ttlMs: 60_000 }
      );
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: normalizedEmail },
        "Signup with eligibility failed"
      );
      return {
        success: false,
        message: "Account creation failed. Please verify your email again to retry.",
      };
    }
  }

  /**
   * Execute signup with eligibility within the lock
   */
  private async executeSignupWithEligibility(
    request: SignupWithEligibilityRequest,
    email: string
  ): Promise<{
    success: boolean;
    message?: string;
    eligibilityRequestId?: string;
    authResult?: AuthResultInternal;
  }> {
    const { firstName, lastName, address, phone, password, dateOfBirth, gender } = request;

    // Check for existing accounts
    const existingCheck = await this.checkExistingAccountsForSignup(email);
    if (existingCheck) {
      return existingCheck;
    }

    // Resolve SF account
    const { sfAccountId, customerNumber } = await this.resolveSalesforceAccount({
      email,
      firstName,
      lastName,
      phone,
      portalSource: PORTAL_SOURCE_INTERNET_ELIGIBILITY,
      updateSourceIfExists: true,
    });

    // Create eligibility case
    const { caseId: eligibilityRequestId } = await this.createEligibilityCase(sfAccountId, address);

    // Create WHMCS client
    const whmcsClient = await this.whmcsSignup.createClient({
      firstName,
      lastName,
      email,
      password,
      phone,
      address: {
        address1: address.address1,
        ...(address.address2 && { address2: address.address2 }),
        city: address.city,
        state: address.state ?? "",
        postcode: address.postcode,
        country: address.country ?? "Japan",
      },
      customerNumber: customerNumber ?? null,
      dateOfBirth,
      gender,
    });

    // Create user and get auth result
    const passwordHash = await argon2.hash(password);
    const authResult = await this.createUserWithAuthResult({
      email,
      passwordHash,
      whmcsClientId: whmcsClient.clientId,
      sfAccountId,
      auditSource: "signup_with_eligibility",
    });

    // Finalize
    await this.updateSalesforcePortalFlags(sfAccountId, whmcsClient.clientId);
    await this.sessionService.invalidate(request.sessionToken);
    await this.sendWelcomeWithEligibilityEmail(email, firstName, eligibilityRequestId);

    this.logger.log(
      { email, userId: authResult.user.id, eligibilityRequestId },
      "Signup with eligibility completed successfully"
    );

    return {
      success: true,
      eligibilityRequestId,
      authResult,
    };
  }

  /**
   * Check for existing accounts and return error response if found
   */
  private async checkExistingAccountsForSignup(
    email: string
  ): Promise<{ success: false; message: string } | null> {
    const { portalExists, whmcsExists } = await this.checkExistingAccounts(email);

    if (portalExists) {
      return {
        success: false,
        message: "An account already exists with this email. Please log in.",
      };
    }

    if (whmcsExists) {
      return {
        success: false,
        message:
          "A billing account already exists with this email. Please use account linking instead.",
      };
    }

    return null;
  }

  // ============================================================================
  // WHMCS Migration (Passwordless)
  // ============================================================================

  /**
   * Migrate WHMCS account to portal without legacy password validation
   *
   * For whmcs_unmapped users after email verification.
   * Email verification serves as identity proof - no legacy password needed.
   *
   * Flow:
   * 1. Validate session has WHMCS client ID
   * 2. Verify WHMCS client still exists
   * 3. Find Salesforce account (by email or customer number)
   * 4. Update WHMCS client with new password + DOB + gender
   * 5. Create portal user with hashed password
   * 6. Create ID mapping
   * 7. Update Salesforce portal flags
   * 8. Return auth tokens
   *
   * Security:
   * - Session is locked to prevent double submissions
   * - Email-level lock prevents concurrent migrations
   */
  async migrateWhmcsAccount(request: MigrateWhmcsAccountRequest): Promise<AuthResultInternal> {
    // Atomically acquire session lock and mark as used
    const sessionResult = await this.sessionService.acquireAndMarkAsUsed(
      request.sessionToken,
      "migrate_whmcs_account"
    );

    if (!sessionResult.success) {
      throw new BadRequestException(sessionResult.reason);
    }

    const session = sessionResult.session;

    // Verify session has WHMCS client ID (should be set during verifyCode for whmcs_unmapped)
    if (!session.whmcsClientId) {
      throw new BadRequestException(
        "No WHMCS account found in session. Please verify your email again."
      );
    }

    const { password, dateOfBirth, gender } = request;
    const lockKey = `migrate-whmcs:${session.email}`;

    try {
      return await this.lockService.withLock(
        lockKey,
        async () => {
          // Verify WHMCS client still exists and matches session
          const whmcsClient = await this.whmcsDiscovery.findAccountByEmail(session.email);
          if (!whmcsClient || whmcsClient.id !== session.whmcsClientId) {
            throw new BadRequestException("WHMCS account verification failed. Please start over.");
          }

          // Check for existing portal user
          const existingPortalUser = await this.usersService.findByEmailInternal(session.email);
          if (existingPortalUser) {
            throw new ConflictException("An account already exists. Please log in.");
          }

          // Find Salesforce account for mapping (required for ID mapping)
          const sfAccount = await this.findSalesforceAccountForMigration(
            session.email,
            whmcsClient.id
          );

          if (!sfAccount) {
            throw new BadRequestException(
              "Unable to find your Salesforce account. Please contact support."
            );
          }

          // Hash password for portal storage
          const passwordHash = await argon2.hash(password);

          // Update WHMCS client with new password + DOB + gender
          await this.updateWhmcsClientForMigration(whmcsClient.id, password, dateOfBirth, gender);

          // Create portal user and ID mapping
          const { userId } = await this.userCreation.createUserWithMapping({
            email: session.email,
            passwordHash,
            whmcsClientId: whmcsClient.id,
            sfAccountId: sfAccount.id,
          });

          // Fetch fresh user and generate tokens
          const freshUser = await this.usersService.findByIdInternal(userId);
          if (!freshUser) {
            throw new Error(ERROR_FAILED_TO_LOAD_USER);
          }

          await this.auditService.logAuthEvent(AuditAction.SIGNUP, userId, {
            email: session.email,
            whmcsClientId: whmcsClient.id,
            source: "whmcs_migration",
          });

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

          // Update Salesforce portal flags if SF account exists
          if (sfAccount) {
            await this.updateSalesforcePortalFlags(
              sfAccount.id,
              whmcsClient.id,
              PORTAL_SOURCE_MIGRATED
            );
          }

          // Invalidate session
          await this.sessionService.invalidate(request.sessionToken);

          this.logger.log(
            { email: session.email, userId, whmcsClientId: whmcsClient.id },
            "WHMCS account migrated successfully"
          );

          return {
            user: profile,
            tokens,
          };
        },
        { ttlMs: 60_000 }
      );
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: session.email },
        "WHMCS migration failed"
      );
      throw error;
    }
  }

  /**
   * Find Salesforce account for WHMCS migration
   * Tries by email first, then by customer number from WHMCS
   */
  private async findSalesforceAccountForMigration(
    email: string,
    whmcsClientId: number
  ): Promise<{ id: string } | null> {
    try {
      // First try to find SF account by email
      const sfAccount = await this.salesforceAccountService.findByEmail(email);
      if (sfAccount) {
        return { id: sfAccount.id };
      }

      // If no SF account found by email, try by customer number from WHMCS
      const whmcsClient = await this.whmcsClientService.getClientDetails(whmcsClientId);

      const customerNumber =
        getCustomFieldValue(whmcsClient.customfields, "198")?.trim() ??
        getCustomFieldValue(whmcsClient.customfields, "Customer Number")?.trim();

      if (!customerNumber) {
        this.logger.warn(
          { whmcsClientId, email },
          "No customer number found in WHMCS for SF lookup"
        );
        return null;
      }

      const sfAccountByNumber =
        await this.salesforceService.findAccountByCustomerNumber(customerNumber);
      if (sfAccountByNumber) {
        return { id: sfAccountByNumber.id };
      }

      this.logger.warn(
        { whmcsClientId, email, customerNumber },
        "No Salesforce account found for WHMCS migration"
      );
      return null;
    } catch (error) {
      this.logger.warn(
        { error: extractErrorMessage(error), email, whmcsClientId },
        "Failed to find Salesforce account for migration"
      );
      return null;
    }
  }

  /**
   * Update WHMCS client with new password and profile data
   */
  private async updateWhmcsClientForMigration(
    clientId: number,
    password: string,
    dateOfBirth: string,
    gender: string
  ): Promise<void> {
    const dobFieldId = this.config.get<string>("WHMCS_DOB_FIELD_ID");
    const genderFieldId = this.config.get<string>("WHMCS_GENDER_FIELD_ID");

    const customfieldsMap: Record<string, string> = {};
    if (dobFieldId) customfieldsMap[dobFieldId] = dateOfBirth;
    if (genderFieldId) customfieldsMap[genderFieldId] = gender;

    const updateData: Record<string, unknown> = {
      password2: password,
    };

    if (Object.keys(customfieldsMap).length > 0) {
      updateData["customfields"] = serializeWhmcsKeyValueMap(customfieldsMap);
    }

    await this.whmcsClientService.updateClient(clientId, updateData);

    this.logger.log({ clientId }, "Updated WHMCS client with new password and profile data");
  }

  // ============================================================================
  // Private Helpers
  // ============================================================================

  private async sendOtpEmail(email: string, code: string): Promise<void> {
    const templateId = this.config.get<string>("EMAIL_TEMPLATE_OTP_VERIFICATION");

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

  private async sendWelcomeWithEligibilityEmail(
    email: string,
    firstName: string,
    eligibilityRequestId: string
  ): Promise<void> {
    const appBase = this.config.get<string>("APP_BASE_URL", "http://localhost:3000");
    const templateId = this.config.get<string>("EMAIL_TEMPLATE_WELCOME_WITH_ELIGIBILITY");

    if (templateId) {
      await this.emailService.sendEmail({
        to: email,
        subject: "Welcome! Your account is ready",
        templateId,
        dynamicTemplateData: {
          firstName,
          portalUrl: appBase,
          dashboardUrl: `${appBase}/account/dashboard`,
          eligibilityRequestId,
        },
      });
    } else {
      await this.emailService.sendEmail({
        to: email,
        subject: "Welcome! Your account is ready",
        html: `
          <p>Hi ${firstName},</p>
          <p>Welcome! Your account has been created successfully.</p>
          <p>We're also checking internet availability at your address. We'll notify you of the results within 1-2 business days.</p>
          <p>Reference ID: ${eligibilityRequestId}</p>
          <p>Log in to your dashboard: <a href="${appBase}/account/dashboard">${appBase}/account/dashboard</a></p>
        `,
      });
    }
  }

  private async determineAccountStatus(
    email: string
  ): Promise<{ status: AccountStatus; sfAccountId?: string; whmcsClientId?: number }> {
    // Check Portal user first
    const portalUser = await this.usersService.findByEmailInternal(email);
    if (portalUser) {
      const hasMapping = await this.mappingsService.hasMapping(portalUser.id);
      if (hasMapping) {
        return { status: ACCOUNT_STATUS.PORTAL_EXISTS };
      }
    }

    // Check WHMCS client or user (sub-account)
    const whmcsClient = await this.whmcsDiscovery.findAccountByEmail(email);
    if (whmcsClient) {
      // Check if WHMCS is already mapped
      const mapping = await this.mappingsService.findByWhmcsClientId(whmcsClient.id);
      if (mapping) {
        return { status: ACCOUNT_STATUS.PORTAL_EXISTS };
      }
      return { status: ACCOUNT_STATUS.WHMCS_UNMAPPED, whmcsClientId: whmcsClient.id };
    }

    // Check Salesforce account
    const sfAccount = await this.salesforceAccountService.findByEmail(email);
    if (sfAccount) {
      // Check if SF is already mapped
      const mapping = await this.mappingsService.findBySfAccountId(sfAccount.id);
      if (mapping) {
        return { status: ACCOUNT_STATUS.PORTAL_EXISTS };
      }
      return { status: ACCOUNT_STATUS.SF_UNMAPPED, sfAccountId: sfAccount.id };
    }

    // No account exists
    return { status: ACCOUNT_STATUS.NEW_CUSTOMER };
  }

  private getPrefillData(
    email: string,
    accountStatus: { status: AccountStatus; sfAccountId?: string; whmcsClientId?: number }
  ): VerifyCodeResponse["prefill"] {
    // For SF-only accounts, we could query SF for the stored address/name
    // For now, return minimal data - frontend will handle input
    if (accountStatus.status === ACCOUNT_STATUS.SF_UNMAPPED && accountStatus.sfAccountId) {
      // TODO: Query SF for stored account details
      return {
        email,
      };
    }

    return undefined;
  }

  /**
   * Build error response for invalid OTP
   */
  private buildOtpErrorResponse(otpResult: {
    reason?: "expired" | "invalid" | "max_attempts";
    attemptsRemaining?: number;
  }): VerifyCodeResponse {
    let error: string;
    switch (otpResult.reason) {
      case "expired":
        error = "Code expired. Please request a new one.";
        break;
      case "max_attempts":
        error = "Too many failed attempts. Please request a new code.";
        break;
      default:
        error = "Invalid code. Please try again.";
    }

    return {
      verified: false,
      error,
      attemptsRemaining: otpResult.attemptsRemaining,
    };
  }

  /**
   * Resolve prefill data including handoff token data if provided
   */
  private async resolvePrefillData(
    email: string,
    accountStatus: { status: AccountStatus; sfAccountId?: string; whmcsClientId?: number },
    handoffToken?: string
  ): Promise<VerifyCodeResponse["prefill"]> {
    let prefill = this.getPrefillData(email, accountStatus);

    if (!handoffToken) {
      return prefill;
    }

    const handoffData = await this.sessionService.getGuestHandoffToken(handoffToken, email);

    if (!handoffData) {
      return prefill;
    }

    this.logger.debug({ email, handoffToken }, "Applying handoff token data to session");

    // Merge handoff data with prefill (handoff takes precedence as it's more recent)
    prefill = {
      ...prefill,
      firstName: handoffData.firstName,
      lastName: handoffData.lastName,
      phone: handoffData.phone ?? prefill?.phone,
      address: handoffData.address,
    };

    // Update account status SF ID if not already set
    if (!accountStatus.sfAccountId && handoffData.sfAccountId) {
      accountStatus.sfAccountId = handoffData.sfAccountId;
    }

    // Invalidate the handoff token after use (one-time use)
    await this.sessionService.invalidateHandoffToken(handoffToken);

    return prefill;
  }

  /**
   * Build session prefill data object without undefined values
   */
  private buildSessionPrefillData(
    prefill: VerifyCodeResponse["prefill"],
    accountStatus: { status: AccountStatus; sfAccountId?: string; whmcsClientId?: number }
  ): Record<string, unknown> {
    return {
      ...(prefill?.firstName && { firstName: prefill.firstName }),
      ...(prefill?.lastName && { lastName: prefill.lastName }),
      ...(prefill?.phone && { phone: prefill.phone }),
      ...(prefill?.address && { address: prefill.address }),
      ...(accountStatus.sfAccountId && { sfAccountId: accountStatus.sfAccountId }),
      ...(accountStatus.whmcsClientId !== undefined && {
        whmcsClientId: accountStatus.whmcsClientId,
      }),
      ...(prefill?.eligibilityStatus && { eligibilityStatus: prefill.eligibilityStatus }),
    };
  }

  private async createEligibilityCase(
    sfAccountId: string,
    address: BilingualEligibilityAddress | SignupWithEligibilityRequest["address"]
  ): Promise<{ caseId: string; caseNumber: string }> {
    // Find or create Opportunity for Internet eligibility
    const { opportunityId, wasCreated: opportunityCreated } =
      await this.opportunityResolution.findOrCreateForInternetEligibility(sfAccountId);

    // Create eligibility case via workflow manager
    // Pass streetAddress for duplicate detection (available on BilingualEligibilityAddress)
    const streetAddress =
      "streetAddress" in address && address.streetAddress ? address.streetAddress : undefined;

    await this.workflowCases.notifyEligibilityCheck({
      accountId: sfAccountId,
      opportunityId,
      opportunityCreated,
      address: {
        address1: address.address1,
        ...(address.address2 ? { address2: address.address2 } : {}),
        city: address.city,
        state: address.state,
        postcode: address.postcode,
        ...(address.country ? { country: address.country } : {}),
      },
      ...(streetAddress ? { streetAddress } : {}),
    });

    // Update Account eligibility status to Pending
    this.updateAccountEligibilityStatus(sfAccountId);

    // Generate a reference ID for the eligibility request
    // (WorkflowCaseManager.notifyEligibilityCheck doesn't return case ID as it's non-critical)
    const caseId = `eligibility:${sfAccountId}:${Date.now()}`;
    const caseNumber = `ELG-${Date.now().toString(36).toUpperCase()}`;

    return { caseId, caseNumber };
  }

  private updateAccountEligibilityStatus(sfAccountId: string): void {
    // TODO: Implement SF Account eligibility status update
    // This would update the SF Account eligibility fields using salesforceService
    this.logger.debug({ sfAccountId }, "Updating account eligibility status to Pending");
  }

  private async updateSalesforcePortalFlags(
    accountId: string,
    whmcsClientId: number,
    /** Optional source override - if not provided, source is not updated (preserves existing) */
    source?:
      | typeof PORTAL_SOURCE_NEW_SIGNUP
      | typeof PORTAL_SOURCE_INTERNET_ELIGIBILITY
      | typeof PORTAL_SOURCE_MIGRATED
  ): Promise<void> {
    try {
      await this.salesforceService.updateAccountPortalFields(accountId, {
        status: PORTAL_STATUS_ACTIVE,
        // Only update source if explicitly provided - preserves existing source for eligibility flows
        ...(source && { source }),
        lastSignedInAt: new Date(),
        whmcsAccountId: whmcsClientId,
      });
    } catch (error) {
      this.logger.warn(
        { error: extractErrorMessage(error), accountId },
        "Failed to update Salesforce portal flags"
      );
    }
  }

  // ============================================================================
  // Shared Account Creation Helpers
  // ============================================================================

  /**
   * Ensure no existing Portal or WHMCS accounts exist for the email
   * Returns error message if account exists, undefined if clear to proceed
   */
  private async checkExistingAccounts(
    email: string
  ): Promise<{ portalExists: boolean; whmcsExists: boolean }> {
    const [portalUser, whmcsClient] = await Promise.all([
      this.usersService.findByEmailInternal(email),
      this.whmcsDiscovery.findClientByEmail(email),
    ]);

    return {
      portalExists: !!portalUser,
      whmcsExists: !!whmcsClient,
    };
  }

  /**
   * Find or create Salesforce account
   */
  private async resolveSalesforceAccount(params: {
    email: string;
    existingAccountId?: string | undefined;
    firstName?: string | undefined;
    lastName?: string | undefined;
    phone?: string | undefined;
    portalSource: string;
    updateSourceIfExists?: boolean | undefined;
  }): Promise<{ sfAccountId: string; customerNumber: string | undefined }> {
    const {
      email,
      existingAccountId,
      firstName,
      lastName,
      phone,
      portalSource,
      updateSourceIfExists,
    } = params;

    // If existing SF account ID provided, verify it still exists
    if (existingAccountId) {
      const existingSf = await this.salesforceAccountService.findByEmail(email);
      if (!existingSf || existingSf.id !== existingAccountId) {
        throw new BadRequestException("Account verification failed. Please start over.");
      }
      return { sfAccountId: existingSf.id, customerNumber: existingSf.accountNumber };
    }

    // Check if SF account exists by email
    const existingSf = await this.salesforceAccountService.findByEmail(email);
    if (existingSf) {
      this.logger.log({ email, sfAccountId: existingSf.id }, "Using existing SF account");

      if (updateSourceIfExists) {
        await this.salesforceService.updateAccountPortalFields(existingSf.id, {
          source: portalSource,
        });
      }

      return { sfAccountId: existingSf.id, customerNumber: existingSf.accountNumber };
    }

    // Create new SF Account
    if (!firstName || !lastName) {
      throw new BadRequestException("Name is required to create an account.");
    }

    const { accountId, accountNumber } = await this.salesforceAccountService.createAccount({
      firstName,
      lastName,
      email,
      phone: phone ?? "",
      portalSource,
    });

    this.logger.log({ email, sfAccountId: accountId }, "Created new SF account");
    return { sfAccountId: accountId, customerNumber: accountNumber };
  }

  /**
   * Create portal user with mapping and generate auth tokens
   */
  private async createUserWithAuthResult(params: {
    email: string;
    passwordHash: string;
    whmcsClientId: number;
    sfAccountId: string;
    auditSource: string;
  }): Promise<AuthResultInternal> {
    const { email, passwordHash, whmcsClientId, sfAccountId, auditSource } = params;

    const { userId } = await this.userCreation.createUserWithMapping({
      email,
      passwordHash,
      whmcsClientId,
      sfAccountId,
    });

    const freshUser = await this.usersService.findByIdInternal(userId);
    if (!freshUser) {
      throw new Error(ERROR_FAILED_TO_LOAD_USER);
    }

    await this.auditService.logAuthEvent(AuditAction.SIGNUP, userId, {
      email,
      whmcsClientId,
      source: auditSource,
    });

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

    return { user: profile, tokens };
  }
}
