import { Injectable, UnauthorizedException, BadRequestException, Inject } from "@nestjs/common";
import { UsersService } from "@bff/modules/users/application/users.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/whmcs-account-discovery.service.js";
import { WhmcsSsoService } from "@bff/integrations/whmcs/services/whmcs-sso.service.js";
import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforce.facade.js";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { sanitizeWhmcsRedirectPath } from "@bff/core/utils/sso.util.js";
import { type SsoLinkResponse } from "@customer-portal/domain/auth";

import type { User as PrismaUser } from "@prisma/client";
import type { Request } from "express";
import { TokenBlacklistService } from "../infra/token/token-blacklist.service.js";
import { AuthTokenService } from "../infra/token/token.service.js";
/**
 * Auth Orchestrator
 *
 * Application layer orchestrator that coordinates authentication operations
 * requiring multiple services: login completion, logout, SSO, account status.
 *
 * Password and health operations are handled by their own services,
 * injected directly by the controller.
 */
@Injectable()
export class AuthOrchestrator {
  constructor(
    private readonly usersService: UsersService,
    private readonly mappingsService: MappingsService,
    private readonly whmcsSsoService: WhmcsSsoService,
    private readonly discoveryService: WhmcsAccountDiscoveryService,
    private readonly salesforceService: SalesforceFacade,
    private readonly auditService: AuditService,
    private readonly tokenBlacklistService: TokenBlacklistService,
    private readonly tokenService: AuthTokenService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Complete login after credential validation (and OTP verification if required)
   * Generates tokens and updates user state
   */
  async completeLogin(user: { id: string; email: string; role: string }, request?: Request) {
    // Update last login time and reset failed attempts
    const profile = await this.usersService.update(user.id, {
      lastLoginAt: new Date(),
      failedLoginAttempts: 0,
      lockedUntil: null,
    });

    await this.auditService.logAuthEvent(
      AuditAction.LOGIN_SUCCESS,
      user.id,
      { email: user.email },
      request,
      true
    );

    const userAgent = request?.headers?.["user-agent"];
    const tokens = await this.tokenService.generateTokenPair(
      {
        id: profile.id,
        email: profile.email,
        role: user.role as "USER" | "ADMIN",
      },
      userAgent ? { userAgent } : {}
    );

    await this.updateAccountLastSignIn(user.id);

    return {
      user: {
        ...profile,
        role: user.role as "USER" | "ADMIN",
      },
      tokens,
    };
  }

  async logout(userId?: string, token?: string, request?: Request): Promise<void> {
    if (token) {
      await this.tokenBlacklistService.blacklistToken(token);
    }

    if (userId) {
      try {
        await this.tokenService.revokeAllUserTokens(userId);
      } catch (error) {
        this.logger.warn("Failed to revoke refresh tokens during logout", {
          userId,
          error: extractErrorMessage(error),
        });
      }
    } else {
      this.logger.debug("Skipping refresh token revocation during logout – userId unavailable");
    }

    await this.auditService.logAuthEvent(AuditAction.LOGOUT, userId, {}, request, true);
  }

  async getAccountStatus(email: string) {
    const normalized = email?.toLowerCase().trim();
    if (!normalized || !normalized.includes("@")) {
      throw new BadRequestException("Valid email is required");
    }

    let portalUser: PrismaUser | null = null;
    let mapped = false;
    let whmcsExists = false;
    let needsPasswordSet = false;

    try {
      portalUser = await this.usersService.findByEmailInternal(normalized);
      if (portalUser) {
        mapped = await this.mappingsService.hasMapping(portalUser.id);
        needsPasswordSet = !portalUser.passwordHash;
      }
    } catch (e) {
      this.logger.warn("Account status: portal lookup failed", { error: extractErrorMessage(e) });
    }

    if (mapped) {
      whmcsExists = true;
    } else {
      const client = await this.discoveryService.findClientByEmail(normalized);
      whmcsExists = !!client;
    }

    let state: "none" | "portal_only" | "whmcs_only" | "both_mapped" = "none";
    if (portalUser && mapped) state = "both_mapped";
    else if (portalUser) state = "portal_only";
    else if (whmcsExists) state = "whmcs_only";

    const recommendedAction = (() => {
      switch (state) {
        case "both_mapped":
          return "sign_in" as const;
        case "portal_only":
          return needsPasswordSet ? ("set_password" as const) : ("sign_in" as const);
        case "whmcs_only":
          return "link_account" as const;
        case "none":
        default:
          return "sign_up" as const;
      }
    })();

    return {
      state,
      portalUserExists: !!portalUser,
      whmcsClientExists: whmcsExists,
      mapped,
      needsPasswordSet,
      recommendedAction,
    };
  }

  /**
   * Create SSO link to WHMCS for general access
   */
  async createSsoLink(userId: string, destination?: string): Promise<SsoLinkResponse> {
    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId);
    const ssoDestination = "sso:custom_redirect";
    const ssoRedirectPath = sanitizeWhmcsRedirectPath(destination);

    return this.whmcsSsoService.createSsoToken(whmcsClientId, ssoDestination, ssoRedirectPath);
  }

  async refreshTokens(
    refreshToken: string | undefined,
    deviceInfo?: { deviceId?: string | undefined; userAgent?: string | undefined }
  ) {
    if (!refreshToken) {
      throw new UnauthorizedException("Invalid refresh token");
    }

    const { tokens, user } = await this.tokenService.refreshTokens(refreshToken, deviceInfo);
    return {
      user,
      tokens,
    };
  }

  private async updateAccountLastSignIn(userId: string): Promise<void> {
    try {
      const mapping = await this.mappingsService.findByUserId(userId);
      if (!mapping?.sfAccountId) {
        return;
      }

      await this.salesforceService.updateAccountPortalFields(mapping.sfAccountId, {
        lastSignedInAt: new Date(),
      });
    } catch (error) {
      this.logger.debug("Failed to update Salesforce last sign-in", {
        userId,
        error: extractErrorMessage(error),
      });
    }
  }
}
