import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { WhmcsConnectionFacade } from "../facades/whmcs.facade.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import {
  transformWhmcsClientResponse,
  whmcsGetUsersResponseSchema,
} from "@customer-portal/domain/customer/providers";
import type { WhmcsClient } from "@customer-portal/domain/customer";

/**
 * Service for discovering and verifying WHMCS accounts by email.
 * Separated from WhmcsClientService to isolate "discovery" logic from "authenticated" logic.
 */
@Injectable()
export class WhmcsAccountDiscoveryService {
  constructor(
    private readonly connectionService: WhmcsConnectionFacade,
    private readonly cacheService: WhmcsCacheService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Find a client by email address.
   * This is a "discovery" operation used during signup/login flows.
   * It uses a specialized cache to map Email -> Client ID.
   */
  async findClientByEmail(email: string): Promise<WhmcsClient | null> {
    try {
      // 1. Try to find client ID by email from cache
      const cachedClientId = await this.cacheService.getClientIdByEmail(email);
      if (cachedClientId) {
        this.logger.debug({ email, cachedClientId }, "Cache hit for email-to-id lookup");
        // If we have ID, fetch the full client data (which has its own cache)
        return this.getClientDetailsById(cachedClientId);
      }

      // 2. If no mapping, fetch from API
      // We use a try-catch here because the connection service might throw if not found
      // or if the API returns a specific error for "no results"
      const response = await this.connectionService.getClientDetailsByEmail(email);

      if (!response || !response.client) {
        // Not found is a valid state for discovery (return null)
        return null;
      }

      const client = transformWhmcsClientResponse(response);

      // 3. Cache both the data and the mapping
      await Promise.all([
        this.cacheService.setClientData(client.id, client),
        this.cacheService.setClientIdByEmail(email, client.id),
      ]);

      this.logger.log({ email, clientId: client.id }, "Discovered client by email");
      return client;
    } catch (error) {
      // Handle "Not Found" specifically — this is expected for discovery
      if (
        error instanceof NotFoundException ||
        (error instanceof Error && error.message.toLowerCase().includes("not found"))
      ) {
        return null;
      }

      // Re-throw all other errors (auth failures, network issues, timeouts, etc.)
      // to avoid silently masking problems like 403 permission errors
      this.logger.error(
        {
          email,
          error: extractErrorMessage(error),
        },
        "Failed to discover client by email"
      );
      throw error;
    }
  }

  /**
   * Find a user by email address.
   * WHMCS users are sub-accounts under clients. If a user exists, we return the associated client ID.
   * This handles cases where the email belongs to a user (sub-account) rather than the main client.
   *
   * @see https://developers.whmcs.com/api-reference/getusers/
   */
  async findUserByEmail(email: string): Promise<{ userId: number; clientId: number } | null> {
    try {
      const rawResponse = await this.connectionService.getUsersByEmail(email);

      // Validate response matches expected schema
      const response = whmcsGetUsersResponseSchema.parse(rawResponse);

      if (!response.users || response.users.length === 0) {
        return null;
      }

      // Find an exact email match (search parameter matches start of email/name, so we need exact match)
      const exactMatch = response.users.find(
        user => user.email.toLowerCase() === email.toLowerCase()
      );

      if (!exactMatch) {
        return null;
      }

      // Get the first associated client (users can belong to multiple clients)
      const clientAssociation = exactMatch.clients?.[0];
      if (!clientAssociation) {
        this.logger.warn(
          { userId: exactMatch.id, email },
          "User found but has no associated clients"
        );
        return null;
      }

      this.logger.log(
        { email, userId: exactMatch.id, clientId: clientAssociation.id },
        "Discovered user by email"
      );
      return {
        userId: Number(exactMatch.id),
        clientId: Number(clientAssociation.id),
      };
    } catch (error) {
      // Sub-account lookup is best-effort — many WHMCS setups don't expose GetUsers
      // (commonly returns HTTP 403). Log at debug to avoid polluting warn logs on every
      // signup. The primary client lookup (findClientByEmail) is the authority.
      this.logger.debug(
        { email, error: extractErrorMessage(error) },
        "User sub-account lookup unavailable — skipping"
      );
      return null;
    }
  }

  /**
   * Find either a client or user by email.
   * First checks for a client, then falls back to checking for a user.
   * Returns the client data if found via either method.
   */
  async findAccountByEmail(email: string): Promise<WhmcsClient | null> {
    // First, try to find a client directly
    const client = await this.findClientByEmail(email);
    if (client) {
      return client;
    }

    // If no client found, check for a user (sub-account)
    const user = await this.findUserByEmail(email);
    if (user) {
      return this.getClientDetailsById(user.clientId);
    }

    return null;
  }

  /**
   * Helper to get details by ID, reusing the cache logic from ClientService logic
   * We duplicate this small fetch to avoid circular dependency or tight coupling with WhmcsClientService
   */
  private async getClientDetailsById(clientId: number): Promise<WhmcsClient> {
    // Try cache first
    const cached = await this.cacheService.getClientData(clientId);
    if (cached) {
      return cached;
    }

    const response = await this.connectionService.getClientDetails(clientId);
    if (!response || !response.client) {
      throw new NotFoundException(`Client ${clientId} not found`);
    }

    const client = transformWhmcsClientResponse(response);
    await this.cacheService.setClientData(client.id, client);
    return client;
  }
}
