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

import {
  type CompleteAccountRequest,
  type SignupWithEligibilityRequest,
  type GetStartedSession,
} from "@customer-portal/domain/get-started";
import type { BilingualAddress } from "@customer-portal/domain/address";

import { DistributedLockService } from "@bff/infra/cache/distributed-lock.service.js";
import { EmailService } from "@bff/infra/email/email.service.js";
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { SalesforceAccountService } from "@bff/integrations/salesforce/services/salesforce-account.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
import {
  PORTAL_SOURCE_NEW_SIGNUP,
  PORTAL_SOURCE_INTERNET_ELIGIBILITY,
} from "@bff/modules/auth/constants/portal.constants.js";
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";

import { GetStartedSessionService } from "../otp/get-started-session.service.js";
import {
  ResolveSalesforceAccountStep,
  CreateEligibilityCaseStep,
  CreateWhmcsClientStep,
  CreatePortalUserStep,
  UpdateSalesforceFlagsStep,
  GenerateAuthResultStep,
} from "./steps/index.js";
import { classifyError } from "./workflow-error.util.js";

/** Result type for the signupWithEligibility path */
export interface SignupWithEligibilityResult {
  success: boolean;
  message?: string;
  errorCategory?: string;
  eligibilityRequestId?: string;
  authResult?: AuthResultInternal;
}

/**
 * Account Creation Workflow Service
 *
 * Merged workflow that handles both:
 * - completeAccount (SF_UNMAPPED / NEW_CUSTOMER without eligibility)
 * - signupWithEligibility (NEW_CUSTOMER with eligibility case)
 *
 * Replaces SfCompletionWorkflowService and NewCustomerSignupWorkflowService.
 * Also fixes the Birthdate/Sex__c gap by calling createContact on the SF PersonContact.
 */
@Injectable()
export class AccountCreationWorkflowService {
  constructor(
    private readonly config: ConfigService,
    private readonly sessionService: GetStartedSessionService,
    private readonly lockService: DistributedLockService,
    private readonly usersService: UsersService,
    private readonly emailService: EmailService,
    private readonly salesforceAccountService: SalesforceAccountService,
    private readonly addressWriter: AddressWriterService,
    private readonly sfStep: ResolveSalesforceAccountStep,
    private readonly caseStep: CreateEligibilityCaseStep,
    private readonly whmcsStep: CreateWhmcsClientStep,
    private readonly portalUserStep: CreatePortalUserStep,
    private readonly sfFlagsStep: UpdateSalesforceFlagsStep,
    private readonly authResultStep: GenerateAuthResultStep,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Execute account creation.
   *
   * - Without options: completeAccount path (exceptions propagate)
   * - With { withEligibility: true }: signupWithEligibility path (errors caught and returned)
   */
  async execute(
    request: CompleteAccountRequest | SignupWithEligibilityRequest,
    options?: { withEligibility?: boolean }
  ): Promise<AuthResultInternal | SignupWithEligibilityResult> {
    if (options?.withEligibility) {
      return this.executeWithEligibility(request as SignupWithEligibilityRequest);
    }

    return this.executeCompleteAccount(request as CompleteAccountRequest);
  }

  // ---------------------------------------------------------------------------
  // completeAccount path — exceptions propagate directly
  // ---------------------------------------------------------------------------

  private async executeCompleteAccount(
    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.validateRequest(request, session);

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

    try {
      // Type assertion safe: executeCreation returns AuthResultInternal when withEligibility=false
      return (await this.lockService.withLock(
        lockKey,
        async () => this.executeCreation(request, session, false),
        { ttlMs: 60_000 }
      )) as AuthResultInternal;
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: session.email },
        "Account completion failed"
      );
      throw error;
    }
  }

  // ---------------------------------------------------------------------------
  // signupWithEligibility path — errors caught and returned as result objects
  // ---------------------------------------------------------------------------

  private async executeWithEligibility(
    request: SignupWithEligibilityRequest
  ): Promise<SignupWithEligibilityResult> {
    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 {
      // Type assertion safe: executeCreation returns SignupWithEligibilityResult when withEligibility=true
      return (await this.lockService.withLock(
        lockKey,
        async () => this.executeCreation(request, sessionResult.session, true),
        { ttlMs: 60_000 }
      )) as SignupWithEligibilityResult;
    } catch (error) {
      this.logger.error(
        { error: extractErrorMessage(error), email: normalizedEmail },
        "Signup with eligibility failed"
      );

      const classified = classifyError(error);
      return {
        success: false,
        errorCategory: classified.errorCategory,
        message: classified.message,
      };
    }
  }

  // ---------------------------------------------------------------------------
  // Shared creation logic
  // ---------------------------------------------------------------------------

  private async executeCreation(
    request: CompleteAccountRequest | SignupWithEligibilityRequest,
    session: Pick<
      GetStartedSession,
      "email" | "sfAccountId" | "firstName" | "lastName" | "address"
    >,
    withEligibility: boolean
  ): Promise<AuthResultInternal | SignupWithEligibilityResult> {
    const { password, phone, dateOfBirth, gender } = request;
    const email = session.email;
    const isNewCustomer = !session.sfAccountId;

    // Check for existing accounts
    if (withEligibility) {
      const existingCheck = await this.checkExistingAccountsSafe(email);
      if (existingCheck) {
        return existingCheck;
      }
    } else {
      await this.ensureNoExistingAccounts(email);
    }

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

    // Resolve address and names based on path
    let finalFirstName: string;
    let finalLastName: string;
    let address:
      | NonNullable<CompleteAccountRequest["address"]>
      | BilingualAddress
      | GetStartedSession["address"];

    if (withEligibility) {
      const eligibilityRequest = request as SignupWithEligibilityRequest;
      finalFirstName = eligibilityRequest.firstName;
      finalLastName = eligibilityRequest.lastName;
      address = eligibilityRequest.address;
    } else {
      const completeRequest = request as CompleteAccountRequest;
      address = this.resolveAddress(completeRequest.address, session.address);
      const names = this.resolveNames(completeRequest.firstName, completeRequest.lastName, session);
      finalFirstName = names.finalFirstName;
      finalLastName = names.finalLastName;
    }

    // Step 1: Resolve SF Account (CRITICAL)
    const sfResult = await this.sfStep.execute({
      email,
      ...(session.sfAccountId != null && { existingAccountId: session.sfAccountId }),
      firstName: finalFirstName,
      lastName: finalLastName,
      phone,
      source: withEligibility ? PORTAL_SOURCE_INTERNET_ELIGIBILITY : PORTAL_SOURCE_NEW_SIGNUP,
      ...(withEligibility && { updateSourceIfExists: true }),
    });

    // Step 1.5: Write SF Address (DEGRADABLE) + Step 1.6: Create SF Contact (DEGRADABLE)
    // + resolve WHMCS address (varies by path)
    let eligibilityRequestId: string | undefined;

    if (withEligibility) {
      const bilingualAddress = address as BilingualAddress;

      // SF address write + eligibility case creation (both DEGRADABLE, independent)
      const [, caseId] = await Promise.all([
        safeOperation(
          async () => this.addressWriter.writeToSalesforce(sfResult.sfAccountId, bilingualAddress),
          {
            criticality: OperationCriticality.OPTIONAL,
            fallback: undefined,
            context: "SF address write",
            logger: this.logger,
            metadata: { email },
          }
        ),
        safeOperation(
          async () => {
            const caseResult = await this.caseStep.execute({
              sfAccountId: sfResult.sfAccountId,
              address: this.addressWriter.toCaseAddress(bilingualAddress),
            });
            return caseResult.caseId;
          },
          {
            criticality: OperationCriticality.OPTIONAL,
            fallback: undefined as string | undefined,
            context: "Eligibility case creation",
            logger: this.logger,
            metadata: { email },
          }
        ),
      ]);

      eligibilityRequestId = caseId;
    } else {
      // SF address write for new customers and SF-only users who completed the address form
      const completeAddress = address as NonNullable<CompleteAccountRequest["address"]>;
      if (completeAddress.prefectureJa) {
        await safeOperation(
          async () =>
            this.addressWriter.writeToSalesforce(
              sfResult.sfAccountId,
              completeAddress as BilingualAddress
            ),
          {
            criticality: OperationCriticality.OPTIONAL,
            fallback: undefined,
            context: "SF address write",
            logger: this.logger,
            metadata: { email },
          }
        );
      }
    }

    // Step 1.6: Create SF Contact — Birthdate + Sex__c (DEGRADABLE, NEW)
    if (dateOfBirth && gender) {
      await safeOperation(
        async () =>
          this.salesforceAccountService.createContact({
            accountId: sfResult.sfAccountId,
            firstName: finalFirstName,
            lastName: finalLastName,
            email,
            phone: phone ?? "",
            gender: gender,
            dateOfBirth,
          }),
        {
          criticality: OperationCriticality.OPTIONAL,
          fallback: undefined,
          context: "SF contact creation (Birthdate/Sex__c)",
          logger: this.logger,
          metadata: { email },
        }
      );
    }

    // Step 3: Create WHMCS Client (CRITICAL)
    let whmcsAddress;
    if (withEligibility) {
      whmcsAddress = this.addressWriter.resolveWhmcsAddressFromBilingual(
        address as BilingualAddress
      );
    } else {
      const completeAddress = address as NonNullable<CompleteAccountRequest["address"]>;
      whmcsAddress = await this.addressWriter.resolveWhmcsAddress({
        postcode: completeAddress.postcode,
        townJa: completeAddress.townJa,
        streetAddress: completeAddress.streetAddress || "",
        buildingName: completeAddress.buildingName,
        roomNumber: completeAddress.roomNumber,
        residenceType: completeAddress.residenceType || "house",
      });
    }

    const whmcsResult = await this.whmcsStep.execute({
      firstName: finalFirstName,
      lastName: finalLastName,
      email,
      password,
      phone: phone ?? "",
      address: this.addressWriter.toWhmcsStepAddress(whmcsAddress),
      customerNumber: sfResult.customerNumber ?? null,
      dateOfBirth,
      gender,
    });

    // Step 4: Create Portal User (CRITICAL, rollback WHMCS on fail)
    let portalUserResult: { userId: string };
    try {
      portalUserResult = await this.portalUserStep.execute({
        email,
        passwordHash,
        sfAccountId: sfResult.sfAccountId,
        whmcsClientId: whmcsResult.whmcsClientId,
      });
    } catch (portalError) {
      await this.whmcsStep.rollback(whmcsResult.whmcsClientId, email);
      throw portalError;
    }

    // Step 5: Update SF Flags (DEGRADABLE — step handles its own errors)
    await this.sfFlagsStep.execute({
      sfAccountId: sfResult.sfAccountId,
      whmcsClientId: whmcsResult.whmcsClientId,
    });

    // Step 6: Generate Auth Result
    let auditSource: string;
    if (withEligibility) {
      auditSource = "signup_with_eligibility";
    } else if (isNewCustomer) {
      auditSource = "get_started_new_customer";
    } else {
      auditSource = "get_started_complete_account";
    }

    const authResult = await this.authResultStep.execute({
      userId: portalUserResult.userId,
      email,
      auditSource,
      auditDetails: { whmcsClientId: whmcsResult.whmcsClientId },
    });

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

    // Send welcome email (only for eligibility path, DEGRADABLE)
    if (withEligibility) {
      await this.sendWelcomeWithEligibilityEmail(email, finalFirstName, eligibilityRequestId);
    }

    this.logger.log(
      {
        email,
        userId: portalUserResult.userId,
        isNewCustomer,
        withEligibility,
        ...(eligibilityRequestId != null && { eligibilityRequestId }),
      },
      withEligibility
        ? "Signup with eligibility completed successfully"
        : "Account completed successfully"
    );

    if (withEligibility) {
      return {
        success: true,
        ...(eligibilityRequestId != null && { eligibilityRequestId }),
        authResult,
      };
    }

    return authResult;
  }

  // ---------------------------------------------------------------------------
  // Private helpers
  // ---------------------------------------------------------------------------

  private validateRequest(
    request: CompleteAccountRequest,
    session: Pick<GetStartedSession, "sfAccountId">
  ): 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.");
    }
  }

  private async ensureNoExistingAccounts(email: string): Promise<void> {
    const portalUser = await this.usersService.findByEmailInternal(email);

    if (portalUser) {
      throw new ConflictException("An account already exists. Please log in.");
    }
  }

  private async checkExistingAccountsSafe(
    email: string
  ): Promise<{ success: false; message: string } | null> {
    const portalUser = await this.usersService.findByEmailInternal(email);

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

    return null;
  }

  private resolveAddress(
    requestAddress: CompleteAccountRequest["address"] | undefined,
    sessionAddress: GetStartedSession["address"] | undefined
  ): NonNullable<CompleteAccountRequest["address"]> | GetStartedSession["address"] {
    const address = requestAddress ?? sessionAddress;

    if (!address || !address.postcode) {
      throw new BadRequestException(
        "Address information is incomplete. Please ensure postcode is provided."
      );
    }

    return address;
  }

  private resolveNames(
    firstName: string | undefined,
    lastName: string | undefined,
    session: Pick<GetStartedSession, "firstName" | "lastName">
  ): { 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 };
  }

  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");

    try {
      if (templateId) {
        await this.emailService.sendEmail({
          to: email,
          subject: "Welcome! Your account is ready",
          templateId,
          dynamicTemplateData: {
            firstName,
            portalUrl: appBase,
            dashboardUrl: `${appBase}/account`,
            eligibilityRequestId: 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>
            ${eligibilityRequestId ? `<p>Reference ID: ${eligibilityRequestId}</p>` : ""}
            <p>Log in to your dashboard: <a href="${appBase}/account">${appBase}/account</a></p>
          `,
        });
      }
    } catch (emailError) {
      this.logger.warn(
        { error: extractErrorMessage(emailError), email },
        "Failed to send welcome email (non-critical)"
      );
    }
  }
}
