import {
  Injectable,
  Inject,
  NotFoundException,
  BadRequestException,
  ConflictException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import type { User as PrismaUser } from "@prisma/client";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import {
  addressSchema,
  combineToUser,
  type Address,
  type User,
} from "@customer-portal/domain/customer";
import {
  type BilingualAddress,
  prepareWhmcsAddressFields,
  prepareSalesforceContactAddressFields,
} from "@customer-portal/domain/address";
import {
  getCustomFieldValue,
  mapPrismaUserToUserAuth,
} from "@customer-portal/domain/customer/providers";
import {
  updateCustomerProfileRequestSchema,
  type UpdateCustomerProfileRequest,
} from "@customer-portal/domain/auth";
import type { Subscription } from "@customer-portal/domain/subscriptions";
import type { Invoice } from "@customer-portal/domain/billing";
import type { Activity, DashboardSummary, NextInvoice } from "@customer-portal/domain/dashboard";
import { dashboardSummarySchema } from "@customer-portal/domain/dashboard";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { WhmcsClientService } from "@bff/integrations/whmcs/services/whmcs-client.service.js";
import { WhmcsInvoiceService } from "@bff/integrations/whmcs/services/whmcs-invoice.service.js";
import { WhmcsSubscriptionService } from "@bff/integrations/whmcs/services/whmcs-subscription.service.js";
import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforce.facade.js";
import { withErrorHandling } from "@bff/core/utils/error-handler.util.js";
import { parseUuidOrThrow } from "@bff/core/utils/validation.util.js";
import { UserAuthRepository } from "./user-auth.repository.js";

/**
 * User Profile Aggregator
 *
 * Combines user data from multiple sources (Portal DB, WHMCS, Salesforce)
 * to build comprehensive user profiles. Handles profile reads and
 * coordinated updates across systems.
 */
@Injectable()
export class UserProfileAggregator {
  constructor(
    private readonly userAuthRepository: UserAuthRepository,
    private readonly mappingsService: MappingsService,
    private readonly whmcsClientService: WhmcsClientService,
    private readonly whmcsInvoiceService: WhmcsInvoiceService,
    private readonly whmcsSubscriptionService: WhmcsSubscriptionService,
    private readonly salesforceService: SalesforceFacade,
    private readonly configService: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async findById(userId: string): Promise<User | null> {
    const validId = parseUuidOrThrow(userId, "Invalid user ID format");
    const user = await this.userAuthRepository.findById(validId);
    if (!user) {
      return null;
    }
    return this.getProfileForUser(user);
  }

  async getProfile(userId: string): Promise<User> {
    const validId = parseUuidOrThrow(userId, "Invalid user ID format");
    const user = await this.userAuthRepository.findById(validId);
    if (!user) {
      throw new NotFoundException("User not found");
    }
    return this.getProfileForUser(user);
  }

  async getAddress(userId: string): Promise<Address | null> {
    const profile = await this.getProfile(userId);
    return profile.address ?? null;
  }

  async updateAddress(userId: string, addressUpdate: Partial<Address>): Promise<Address> {
    const validId = parseUuidOrThrow(userId, "Invalid user ID format");
    const parsed = addressSchema.partial().parse(addressUpdate ?? {});
    const hasUpdates = Object.values(parsed).some(value => value !== undefined);

    if (!hasUpdates) {
      throw new BadRequestException("No address fields provided for update");
    }

    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(validId);

    return withErrorHandling(
      async () => {
        await this.whmcsClientService.updateClientAddress(whmcsClientId, parsed);
        await this.whmcsClientService.invalidateUserCache(validId);

        this.logger.log("Successfully updated customer address in WHMCS", {
          userId: validId,
          whmcsClientId,
        });

        const refreshedProfile = await this.getProfile(validId);
        if (refreshedProfile.address) {
          return refreshedProfile.address;
        }

        const refreshedAddress = await this.whmcsClientService.getClientAddress(whmcsClientId);
        return addressSchema.parse(refreshedAddress ?? {});
      },
      this.logger,
      {
        context: `Update address for user ${validId}`,
        fallbackMessage: "Unable to update address",
      }
    );
  }

  /**
   * Update address with bilingual data (Japanese + English)
   * Dual-write: English to WHMCS, Japanese to Salesforce
   *
   * @param userId - User ID
   * @param bilingualAddress - Address data with both Japanese and English fields
   * @returns Updated address (from WHMCS, source of truth)
   */
  async updateBilingualAddress(
    userId: string,
    bilingualAddress: BilingualAddress
  ): Promise<Address> {
    const validId = parseUuidOrThrow(userId, "Invalid user ID format");

    return withErrorHandling(
      async () => {
        const mapping = await this.mappingsService.findByUserId(validId);
        if (!mapping?.whmcsClientId) {
          throw new NotFoundException("User mapping not found");
        }

        // 1. Update WHMCS with English address (source of truth)
        const whmcsFields = prepareWhmcsAddressFields(bilingualAddress);
        await this.whmcsClientService.updateClientAddress(mapping.whmcsClientId, whmcsFields);
        await this.whmcsClientService.invalidateUserCache(validId);

        this.logger.log("Successfully updated customer address in WHMCS", {
          userId: validId,
          whmcsClientId: mapping.whmcsClientId,
        });

        // 2. Update Salesforce with Japanese address (secondary, non-blocking)
        if (mapping.sfAccountId) {
          const sfFields = prepareSalesforceContactAddressFields(bilingualAddress);
          try {
            await this.salesforceService.updateContactAddress(mapping.sfAccountId, {
              mailingStreet: sfFields.MailingStreet,
              mailingCity: sfFields.MailingCity,
              mailingState: sfFields.MailingState,
              mailingPostalCode: sfFields.MailingPostalCode,
              mailingCountry: sfFields.MailingCountry,
              buildingName: sfFields.BuildingName__c,
              roomNumber: sfFields.RoomNumber__c,
            });

            this.logger.log("Successfully updated Japanese address in Salesforce", {
              userId: validId,
              sfAccountId: mapping.sfAccountId,
            });
          } catch (sfError) {
            // Log but don't fail - WHMCS is source of truth
            this.logger.warn("Failed to update Salesforce address (non-blocking)", {
              userId: validId,
              sfAccountId: mapping.sfAccountId,
              error: extractErrorMessage(sfError),
            });
          }
        } else {
          this.logger.debug("No Salesforce mapping found, skipping Japanese address sync", {
            userId: validId,
          });
        }

        // Return refreshed address from WHMCS
        const refreshedProfile = await this.getProfile(validId);
        if (refreshedProfile.address) {
          return refreshedProfile.address;
        }

        const refreshedAddress = await this.whmcsClientService.getClientAddress(
          mapping.whmcsClientId
        );
        return addressSchema.parse(refreshedAddress ?? {});
      },
      this.logger,
      {
        context: `Update bilingual address for user ${validId}`,
        fallbackMessage: "Unable to update address",
      }
    );
  }

  async updateProfile(userId: string, update: UpdateCustomerProfileRequest): Promise<User> {
    const validId = parseUuidOrThrow(userId, "Invalid user ID format");
    const parsed = updateCustomerProfileRequestSchema.parse(update);

    return withErrorHandling(
      async () => {
        // Explicitly disallow name changes from portal
        if (parsed.firstname !== undefined || parsed.lastname !== undefined) {
          throw new BadRequestException("Name cannot be changed from the portal.");
        }

        const mapping = await this.mappingsService.findByUserId(validId);
        if (!mapping) {
          throw new NotFoundException("User mapping not found");
        }

        // Email changes must update both Portal DB and WHMCS, and must be unique in Portal.
        if (parsed.email) {
          const currentUser = await this.userAuthRepository.findById(validId);
          if (!currentUser) {
            throw new NotFoundException("User not found");
          }
          const newEmail = parsed.email;

          const existing = await this.userAuthRepository.findByEmail(newEmail);
          if (existing && existing.id !== validId) {
            throw new ConflictException("That email address is already in use.");
          }

          // Update WHMCS first (source of truth for billing profile), then update Portal DB.
          await this.whmcsClientService.updateClient(mapping.whmcsClientId, { email: newEmail });
          await this.userAuthRepository.updateEmail(validId, newEmail);
        }

        // Allow phone/company/language updates through to WHMCS
        // Exclude email/firstname/lastname from WHMCS update (handled separately above or disallowed)
        const { email, firstname, lastname, ...whmcsUpdate } = parsed;
        void email; // Email is handled above in a separate flow
        void firstname; // Name changes are explicitly disallowed
        void lastname;
        if (Object.keys(whmcsUpdate).length > 0) {
          await this.whmcsClientService.updateClient(mapping.whmcsClientId, whmcsUpdate);
        }

        this.logger.log({ userId: validId }, "Successfully updated customer profile in WHMCS");

        return this.getProfile(validId);
      },
      this.logger,
      {
        context: `Update profile for user ${validId}`,
        fallbackMessage: "Unable to update profile",
      }
    );
  }

  async getUserSummary(userId: string): Promise<DashboardSummary> {
    return withErrorHandling(
      async () => {
        const user = await this.userAuthRepository.findById(userId);
        if (!user) {
          throw new NotFoundException("User not found");
        }

        const mapping = await this.mappingsService.findByUserId(userId);
        if (!mapping?.whmcsClientId) {
          this.logger.warn(`No WHMCS mapping found for user ${userId}`);

          const currency = "JPY";

          const summary: DashboardSummary = {
            stats: {
              activeSubscriptions: 0,
              unpaidInvoices: 0,
              openCases: 0,
              currency,
            },
            nextInvoice: null,
            recentActivity: [],
          };
          return summary;
        }

        const [subscriptionsData, invoicesData, unpaidInvoicesData] = await Promise.allSettled([
          this.whmcsSubscriptionService.getSubscriptions(mapping.whmcsClientId, userId),
          this.whmcsInvoiceService.getInvoices(mapping.whmcsClientId, userId, { limit: 10 }),
          this.whmcsInvoiceService.getInvoices(mapping.whmcsClientId, userId, {
            status: "Unpaid",
            limit: 1,
          }),
        ]);

        let activeSubscriptions = 0;
        let recentSubscriptions: Array<{
          id: number;
          status: string;
          registrationDate: string;
          productName: string;
        }> = [];
        if (subscriptionsData.status === "fulfilled") {
          const subscriptions: Subscription[] = subscriptionsData.value.subscriptions;
          activeSubscriptions = subscriptions.filter(sub => sub.status === "Active").length;
          recentSubscriptions = subscriptions
            .filter(sub => sub.status === "Active")
            .sort((a, b) => {
              const aTime = a.registrationDate
                ? new Date(a.registrationDate).getTime()
                : Number.NEGATIVE_INFINITY;
              const bTime = b.registrationDate
                ? new Date(b.registrationDate).getTime()
                : Number.NEGATIVE_INFINITY;
              return bTime - aTime;
            })
            .slice(0, 3)
            .map(sub => ({
              id: sub.id,
              status: sub.status,
              registrationDate: sub.registrationDate,
              productName: sub.productName,
            }));
        } else {
          this.logger.error(
            `Failed to fetch subscriptions for user ${userId}:`,
            subscriptionsData.reason
          );
        }

        let unpaidInvoices = 0;
        let nextInvoice: NextInvoice | null = null;
        let recentInvoices: Array<{
          id: number;
          status: string;
          dueDate?: string | undefined;
          total: number;
          number: string;
          issuedAt?: string | undefined;
          paidDate?: string | undefined;
          currency?: string | null | undefined;
        }> = [];

        // Process unpaid invoices count
        if (unpaidInvoicesData.status === "fulfilled") {
          unpaidInvoices = unpaidInvoicesData.value.pagination.totalItems;
        } else {
          this.logger.error(`Failed to fetch unpaid invoices count for user ${userId}`, {
            reason: extractErrorMessage(unpaidInvoicesData.reason),
          });
        }

        if (invoicesData.status === "fulfilled") {
          const invoices: Invoice[] = invoicesData.value.invoices;

          // Fallback if unpaid invoices call failed, though inaccurate for total count > 10
          if (unpaidInvoicesData.status === "rejected") {
            unpaidInvoices = invoices.filter(
              inv => inv.status === "Unpaid" || inv.status === "Overdue"
            ).length;
          }

          const upcomingInvoices = invoices
            .filter(inv => (inv.status === "Unpaid" || inv.status === "Overdue") && inv.dueDate)
            .sort((a, b) => {
              const aTime = a.dueDate ? new Date(a.dueDate).getTime() : Number.POSITIVE_INFINITY;
              const bTime = b.dueDate ? new Date(b.dueDate).getTime() : Number.POSITIVE_INFINITY;
              return aTime - bTime;
            });

          if (upcomingInvoices.length > 0) {
            const invoice = upcomingInvoices[0];
            if (invoice) {
              nextInvoice = {
                id: invoice.id,
                dueDate: invoice.dueDate!,
                amount: invoice.total,
                currency: invoice.currency ?? "JPY",
              };
            }
          }

          recentInvoices = invoices
            .sort((a, b) => {
              const aTime = a.issuedAt ? new Date(a.issuedAt).getTime() : Number.NEGATIVE_INFINITY;
              const bTime = b.issuedAt ? new Date(b.issuedAt).getTime() : Number.NEGATIVE_INFINITY;
              return bTime - aTime;
            })
            .slice(0, 5)
            .map(inv => ({
              id: inv.id,
              status: inv.status,
              dueDate: inv.dueDate,
              total: inv.total,
              number: inv.number,
              issuedAt: inv.issuedAt,
              currency: inv.currency ?? null,
            }));
        } else {
          this.logger.error(`Failed to fetch invoices for user ${userId}`, {
            reason: extractErrorMessage(invoicesData.reason),
          });
        }

        const activities: Activity[] = [];

        for (const invoice of recentInvoices) {
          if (invoice.status === "Paid") {
            const metadata: Record<string, unknown> = {
              amount: invoice.total,
              currency: invoice.currency ?? "JPY",
            };
            if (invoice.dueDate) metadata["dueDate"] = invoice.dueDate;
            if (invoice.number) metadata["invoiceNumber"] = invoice.number;
            activities.push({
              id: `invoice-paid-${invoice.id}`,
              type: "invoice_paid",
              title: `Invoice #${invoice.number} paid`,
              description: `Payment of ¥${invoice.total.toLocaleString()} processed`,
              date: invoice.paidDate || invoice.issuedAt || new Date().toISOString(),
              relatedId: invoice.id,
              metadata,
            });
          } else if (invoice.status === "Unpaid" || invoice.status === "Overdue") {
            const metadata: Record<string, unknown> = {
              amount: invoice.total,
              currency: invoice.currency ?? "JPY",
              status: invoice.status,
            };
            if (invoice.dueDate) metadata["dueDate"] = invoice.dueDate;
            if (invoice.number) metadata["invoiceNumber"] = invoice.number;
            activities.push({
              id: `invoice-created-${invoice.id}`,
              type: "invoice_created",
              title: `Invoice #${invoice.number} created`,
              description: `Amount: ¥${invoice.total.toLocaleString()}`,
              date: invoice.issuedAt || new Date().toISOString(),
              relatedId: invoice.id,
              metadata,
            });
          }
        }

        for (const subscription of recentSubscriptions) {
          const metadata: Record<string, unknown> = {
            productName: subscription.productName,
            status: subscription.status,
          };
          if (subscription.registrationDate) {
            metadata["registrationDate"] = subscription.registrationDate;
          }
          activities.push({
            id: `service-activated-${subscription.id}`,
            type: "service_activated",
            title: `${subscription.productName} activated`,
            description: "Service successfully provisioned",
            date: subscription.registrationDate,
            relatedId: subscription.id,
            metadata,
          });
        }

        activities.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
        const recentActivity = activities.slice(0, 10);

        this.logger.log(`Generated dashboard summary for user ${userId}`, {
          activeSubscriptions,
          unpaidInvoices,
          activitiesCount: recentActivity.length,
          hasNextInvoice: !!nextInvoice,
        });

        let currency = "JPY";
        try {
          const client = await this.whmcsClientService.getClientDetails(mapping.whmcsClientId);
          const resolvedCurrency =
            typeof client.currency_code === "string" && client.currency_code.trim().length > 0
              ? client.currency_code
              : null;
          if (resolvedCurrency) {
            currency = resolvedCurrency;
          }
        } catch (error) {
          this.logger.warn("Could not fetch currency from WHMCS client", {
            userId,
            error: extractErrorMessage(error),
          });
        }

        const summary: DashboardSummary = {
          stats: {
            activeSubscriptions,
            unpaidInvoices,
            openCases: 0,
            currency,
          },
          nextInvoice,
          recentActivity,
        };
        return dashboardSummarySchema.parse(summary);
      },
      this.logger,
      {
        context: `Get user summary for ${userId}`,
        fallbackMessage: "Unable to retrieve dashboard summary",
      }
    );
  }

  private async getProfileForUser(user: PrismaUser): Promise<User> {
    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(user.id);

    return withErrorHandling(
      async () => {
        const whmcsClient = await this.whmcsClientService.getClientDetails(whmcsClientId);
        const userAuth = mapPrismaUserToUserAuth(user);
        const base = combineToUser(userAuth, whmcsClient);

        // Portal-visible identifiers (read-only). These are stored in WHMCS custom fields.
        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 rawSfNumber = customerNumberFieldId
          ? getCustomFieldValue(whmcsClient.customfields, customerNumberFieldId)
          : undefined;
        const rawDob = dobFieldId
          ? getCustomFieldValue(whmcsClient.customfields, dobFieldId)
          : undefined;
        const rawGender = genderFieldId
          ? getCustomFieldValue(whmcsClient.customfields, genderFieldId)
          : undefined;

        const sfNumber = rawSfNumber?.trim() ? rawSfNumber.trim() : null;
        const dateOfBirth = rawDob?.trim() ? rawDob.trim() : null;
        const gender = rawGender?.trim() ? rawGender.trim() : null;

        return {
          ...base,
          sfNumber,
          dateOfBirth,
          gender,
        };
      },
      this.logger,
      {
        context: `Fetch client profile from WHMCS for user ${user.id}`,
        fallbackMessage: "Unable to retrieve customer profile from billing system",
      }
    );
  }
}
