import {
  BadRequestException,
  ConflictException,
  Inject,
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import * as bcrypt from "bcrypt";
import type { Request } from "express";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service";
import { UsersFacade } from "@bff/modules/users/application/users.facade";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
import { PrismaService } from "@bff/infra/database/prisma.service";
import { AuthTokenService } from "../../token/token.service";
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import {
  signupRequestSchema,
  type SignupRequest,
  type SignupResult,
  type ValidateSignupRequest,
} from "@customer-portal/domain/auth";
import { mapPrismaUserToDomain } from "@bff/infra/mappers";
import type { User as PrismaUser } from "@prisma/client";
import { CacheService } from "@bff/infra/cache/cache.service";
import {
  PORTAL_SOURCE_NEW_SIGNUP,
  PORTAL_STATUS_ACTIVE,
  type PortalRegistrationSource,
} from "@bff/modules/auth/constants/portal.constants";

type _SanitizedPrismaUser = Omit<
  PrismaUser,
  "passwordHash" | "failedLoginAttempts" | "lockedUntil"
>;

interface SignupAccountSnapshot {
  id: string;
  Name?: string | null;
  WH_Account__c?: string | null;
}

@Injectable()
export class SignupWorkflowService {
  private readonly accountCacheTtlSeconds = 30;
  private readonly accountCachePrefix = "auth:signup:account:";

  constructor(
    private readonly usersFacade: UsersFacade,
    private readonly mappingsService: MappingsService,
    private readonly whmcsService: WhmcsService,
    private readonly salesforceService: SalesforceService,
    private readonly configService: ConfigService,
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
    private readonly tokenService: AuthTokenService,
    private readonly authRateLimitService: AuthRateLimitService,
    private readonly cache: CacheService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async validateSignup(validateData: ValidateSignupRequest, request?: Request) {
    const { sfNumber } = validateData;

    try {
      const accountSnapshot = await this.getAccountSnapshot(sfNumber);
      if (!accountSnapshot) {
        await this.auditService.logAuthEvent(
          AuditAction.SIGNUP,
          undefined,
          { sfNumber, reason: "SF number not found" },
          request,
          false,
          "Customer number not found in Salesforce"
        );
        throw new BadRequestException("Customer number not found in Salesforce");
      }

      const existingMapping = await this.mappingsService.findBySfAccountId(accountSnapshot.id);
      if (existingMapping) {
        await this.auditService.logAuthEvent(
          AuditAction.SIGNUP,
          undefined,
          { sfNumber, sfAccountId: accountSnapshot.id, reason: "Already has mapping" },
          request,
          false,
          "Customer number already registered"
        );
        throw new ConflictException(
          "You already have an account. Please use the login page to access your existing account."
        );
      }

      if (accountSnapshot.WH_Account__c && accountSnapshot.WH_Account__c.trim() !== "") {
        await this.auditService.logAuthEvent(
          AuditAction.SIGNUP,
          undefined,
          {
            sfNumber,
            sfAccountId: accountSnapshot.id,
            whAccount: accountSnapshot.WH_Account__c,
            reason: "WH Account not empty",
          },
          request,
          false,
          "Account already has WHMCS integration"
        );
        throw new ConflictException(
          "You already have an account. Please use the login page to access your existing account."
        );
      }

      await this.auditService.logAuthEvent(
        AuditAction.SIGNUP,
        undefined,
        { sfNumber, sfAccountId: accountSnapshot.id, step: "validation" },
        request,
        true
      );

      return {
        valid: true,
        sfAccountId: accountSnapshot.id,
        message: "Customer number validated successfully",
      };
    } catch (error) {
      if (error instanceof BadRequestException || error instanceof ConflictException) {
        throw error;
      }

      await this.auditService.logAuthEvent(
        AuditAction.SIGNUP,
        undefined,
        { sfNumber, error: getErrorMessage(error) },
        request,
        false,
        getErrorMessage(error)
      );

      this.logger.error("Signup validation error", { error: getErrorMessage(error) });
      throw new BadRequestException("Validation failed");
    }
  }

  async signup(signupData: SignupRequest, request?: Request): Promise<SignupResult> {
    if (request) {
      await this.authRateLimitService.consumeSignupAttempt(request);
    }

    // Validate signup data using schema (throws on validation error)
    signupRequestSchema.parse(signupData);

    const {
      email,
      password,
      firstName,
      lastName,
      company,
      phone,
      sfNumber,
      address,
      nationality,
      dateOfBirth,
      gender,
    } = signupData;

    const existingUser = await this.usersFacade.findByEmailInternal(email);
    if (existingUser) {
      const mapped = await this.mappingsService.hasMapping(existingUser.id);
      const message = mapped
        ? "You already have an account. Please sign in."
        : "You already have an account with us. Please sign in to continue setup.";
      await this.auditService.logAuthEvent(
        AuditAction.SIGNUP,
        existingUser.id,
        { email, reason: mapped ? "mapped_user_exists" : "unmapped_user_exists" },
        request,
        false,
        message
      );
      throw new ConflictException(message);
    }

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

    try {
      const accountSnapshot = await this.getAccountSnapshot(sfNumber);
      if (!accountSnapshot) {
        throw new BadRequestException(
          `Salesforce account not found for Customer Number: ${sfNumber}`
        );
      }

      if (accountSnapshot.WH_Account__c && accountSnapshot.WH_Account__c.trim() !== "") {
        throw new ConflictException(
          "You already have an account. Please use the login page to access your existing account."
        );
      }

      let whmcsClient: { clientId: number };
      try {
        try {
          const existingWhmcs = await this.whmcsService.getClientDetailsByEmail(email);
          if (existingWhmcs) {
            const existingMapping = await this.mappingsService.findByWhmcsClientId(
              existingWhmcs.id
            );
            if (existingMapping) {
              throw new ConflictException("You already have an account. Please sign in.");
            }

            throw new ConflictException(
              "We found an existing billing account for this email. Please link your account instead."
            );
          }
        } catch (pre) {
          if (!(pre instanceof NotFoundException)) {
            throw pre;
          }
        }

        const customerNumberFieldId = this.configService.get<string>(
          "WHMCS_CUSTOMER_NUMBER_FIELD_ID",
          "198"
        );
        const dobFieldId = this.configService.get<string>("WHMCS_DOB_FIELD_ID");
        const genderFieldId = this.configService.get<string>("WHMCS_GENDER_FIELD_ID");
        const nationalityFieldId = this.configService.get<string>("WHMCS_NATIONALITY_FIELD_ID");

        const customfields: Record<string, string> = {};
        if (customerNumberFieldId) customfields[customerNumberFieldId] = sfNumber;
        if (dobFieldId && dateOfBirth) customfields[dobFieldId] = dateOfBirth;
        if (genderFieldId && gender) customfields[genderFieldId] = gender;
        if (nationalityFieldId && nationality) customfields[nationalityFieldId] = nationality;

        if (
          !address?.address1 ||
          !address?.city ||
          !address?.state ||
          !address?.postcode ||
          !address?.country
        ) {
          throw new BadRequestException(
            "Complete address information is required for billing account creation"
          );
        }

        if (!phone) {
          throw new BadRequestException("Phone number is required for billing account creation");
        }

        this.logger.log("Creating WHMCS client", { email, firstName, lastName, sfNumber });

        whmcsClient = await this.whmcsService.addClient({
          firstname: firstName,
          lastname: lastName,
          email,
          companyname: company || "",
          phonenumber: phone,
          address1: address.address1,
          address2: address.address2 || "",
          city: address.city,
          state: address.state,
          postcode: address.postcode,
          country: address.country,
          password2: password,
          customfields,
        });

        this.logger.log("WHMCS client created successfully", {
          clientId: whmcsClient.clientId,
          email,
        });
      } catch (whmcsError) {
        this.logger.error("Failed to create WHMCS client", {
          error: getErrorMessage(whmcsError),
          email,
          firstName,
          lastName,
        });
        throw new BadRequestException(
          `Failed to create billing account: ${getErrorMessage(whmcsError)}`
        );
      }

      // Use a transaction with compensation to prevent WHMCS orphans
      let createdUserId: string;
      try {
        const result = await this.prisma.$transaction(async tx => {
          const created = await tx.user.create({
            data: {
              email,
              passwordHash,
              emailVerified: false,
              failedLoginAttempts: 0,
              lockedUntil: null,
              lastLoginAt: null,
            },
            select: { id: true, email: true },
          });

          await tx.idMapping.create({
            data: {
              userId: created.id,
              whmcsClientId: whmcsClient.clientId,
              sfAccountId: accountSnapshot.id,
            },
          });

          return { createdUserId: created.id };
        });

        createdUserId = result.createdUserId;
      } catch (dbError) {
        // Compensation: Clean up the orphaned WHMCS client
        this.logger.error("Database transaction failed, cleaning up WHMCS client", {
          whmcsClientId: whmcsClient.clientId,
          email,
          error: getErrorMessage(dbError),
        });

        try {
          // WHMCS doesn't have a delete client API, so we mark it for manual cleanup
          // Update the client status to indicate it's orphaned
          await this.whmcsService.updateClient(whmcsClient.clientId, {
            status: "Inactive",
          });

          this.logger.warn("Marked orphaned WHMCS client for manual cleanup", {
            whmcsClientId: whmcsClient.clientId,
            email,
            action: "marked_for_cleanup",
          });
        } catch (cleanupError) {
          // Log but don't fail - the original error is more important
          this.logger.error("Failed to mark orphaned WHMCS client for cleanup", {
            whmcsClientId: whmcsClient.clientId,
            email,
            cleanupError: getErrorMessage(cleanupError),
            recommendation: "Manual cleanup required in WHMCS admin",
          });
        }

        throw new BadRequestException(`Failed to create user account: ${getErrorMessage(dbError)}`);
      }

      const freshUser = await this.usersFacade.findByIdInternal(createdUserId);

      await this.auditService.logAuthEvent(
        AuditAction.SIGNUP,
        createdUserId,
        { email, whmcsClientId: whmcsClient.clientId },
        request,
        true
      );

      const prismaUser = freshUser ?? (await this.usersFacade.findByIdInternal(createdUserId));

      if (!prismaUser) {
        throw new Error("Failed to load created user");
      }

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

      await this.updateSalesforcePortalFlags(accountSnapshot.id, PORTAL_SOURCE_NEW_SIGNUP);

      return {
        user: profile,
        tokens,
      };
    } catch (error) {
      await this.auditService.logAuthEvent(
        AuditAction.SIGNUP,
        undefined,
        { email, error: getErrorMessage(error) },
        request,
        false,
        getErrorMessage(error)
      );

      this.logger.error("Signup error", { error: getErrorMessage(error) });
      throw new BadRequestException("Failed to create user account");
    }
  }

  async signupPreflight(signupData: SignupRequest) {
    const { email, sfNumber } = signupData;
    const normalizedEmail = email.toLowerCase().trim();

    const result: {
      ok: boolean;
      canProceed: boolean;
      nextAction: "proceed_signup" | "link_whmcs" | "login" | "fix_input" | "blocked";
      messages: string[];
      normalized: { email: string };
      portal: { userExists: boolean; needsPasswordSet?: boolean };
      salesforce: { accountId?: string; alreadyMapped: boolean };
      whmcs: { clientExists: boolean; clientId?: number };
    } = {
      ok: true,
      canProceed: false,
      nextAction: "blocked",
      messages: [],
      normalized: { email: normalizedEmail },
      portal: { userExists: false },
      salesforce: { alreadyMapped: false },
      whmcs: { clientExists: false },
    };

    const portalUserAuth = await this.usersFacade.findByEmailInternal(normalizedEmail);
    if (portalUserAuth) {
      result.portal.userExists = true;
      const mapped = await this.mappingsService.hasMapping(portalUserAuth.id);
      if (mapped) {
        result.nextAction = "login";
        result.messages.push("An account already exists. Please sign in.");
        return result;
      }

      result.portal.needsPasswordSet = !portalUserAuth.passwordHash;
      result.nextAction = portalUserAuth.passwordHash ? "login" : "fix_input";
      result.messages.push(
        portalUserAuth.passwordHash
          ? "An account exists without billing link. Please sign in to continue setup."
          : "An account exists and needs password setup. Please set a password to continue."
      );
      return result;
    }

    const accountSnapshot = await this.getAccountSnapshot(sfNumber);
    if (!accountSnapshot) {
      result.nextAction = "fix_input";
      result.messages.push("Customer number not found in Salesforce");
      return result;
    }
    result.salesforce.accountId = accountSnapshot.id;

    const existingMapping = await this.mappingsService.findBySfAccountId(accountSnapshot.id);
    if (existingMapping) {
      result.salesforce.alreadyMapped = true;
      result.nextAction = "login";
      result.messages.push("This customer number is already registered. Please sign in.");
      return result;
    }

    try {
      const client = await this.whmcsService.getClientDetailsByEmail(normalizedEmail);
      if (client) {
        result.whmcs.clientExists = true;
        result.whmcs.clientId = client.id;

        try {
          const mapped = await this.mappingsService.findByWhmcsClientId(client.id);
          if (mapped) {
            result.nextAction = "login";
            result.messages.push("This billing account is already linked. Please sign in.");
            return result;
          }
        } catch {
          // ignore; treat as unmapped
        }

        result.nextAction = "link_whmcs";
        result.messages.push(
          "We found an existing billing account for this email. Please link your account."
        );
        return result;
      }
    } catch (err) {
      if (!(err instanceof NotFoundException)) {
        this.logger.warn("WHMCS preflight check failed", { error: getErrorMessage(err) });
        result.messages.push("Unable to verify billing system. Please try again later.");
        result.nextAction = "blocked";
        return result;
      }
    }

    result.canProceed = true;
    result.nextAction = "proceed_signup";
    result.messages.push("All checks passed. Ready to create your account.");
    return result;
  }

  private async getAccountSnapshot(sfNumber: string): Promise<SignupAccountSnapshot | null> {
    const normalized = this.normalizeCustomerNumber(sfNumber);
    if (!normalized) {
      return null;
    }

    const cacheKey = this.buildAccountCacheKey(normalized);
    const cached = await this.cache.get<SignupAccountCacheEntry | null>(cacheKey);
    const unwrapped = this.unwrapAccountCacheEntry(cached);

    if (unwrapped.hit) {
      return unwrapped.value;
    }

    const resolved =
      await this.salesforceService.findAccountWithDetailsByCustomerNumber(normalized);
    await this.cache.set(
      cacheKey,
      this.wrapAccountCacheEntry(resolved ?? null),
      this.accountCacheTtlSeconds
    );

    return resolved;
  }

  private normalizeCustomerNumber(sfNumber: string): string | null {
    if (typeof sfNumber !== "string") {
      return null;
    }
    const trimmed = sfNumber.trim();
    return trimmed.length > 0 ? trimmed : null;
  }

  private buildAccountCacheKey(customerNumber: string): string {
    return `${this.accountCachePrefix}${customerNumber}`;
  }

  private unwrapAccountCacheEntry(cached: SignupAccountCacheEntry | null): {
    hit: boolean;
    value: SignupAccountSnapshot | null;
  } {
    if (!cached) {
      return { hit: false, value: null };
    }

    if (typeof cached === "object" && cached.__signupCache === true) {
      return { hit: true, value: cached.value ?? null };
    }

    return { hit: true, value: (cached as unknown as SignupAccountSnapshot) ?? null };
  }

  private wrapAccountCacheEntry(snapshot: SignupAccountSnapshot | null): SignupAccountCacheEntry {
    return {
      value: snapshot ?? null,
      __signupCache: true,
    };
  }

  private async updateSalesforcePortalFlags(
    accountId: string,
    source: PortalRegistrationSource
  ): Promise<void> {
    try {
      await this.salesforceService.updateAccountPortalFields(accountId, {
        status: PORTAL_STATUS_ACTIVE,
        source,
        lastSignedInAt: new Date(),
      });
    } catch (error) {
      this.logger.warn("Failed to update Salesforce portal flags after signup", {
        accountId,
        error: getErrorMessage(error),
      });
    }
  }
}

interface SignupAccountCacheEntry {
  value: SignupAccountSnapshot | null;
  __signupCache: true;
}
