import {
  Injectable,
  Inject,
  NotFoundException,
  BadRequestException,
  ConflictException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { randomUUID } from "crypto";
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 { safeOperation, OperationCriticality } from "@bff/core/utils/safe-operation.util.js";
import { parseUuidOrThrow } from "@bff/core/utils/validation.util.js";
import { UserAuthRepository } from "./user-auth.repository.js";
import { AddressReconcileQueueService } from "../queue/address-reconcile.queue.js";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";

const ERROR_INVALID_USER_ID = "Invalid user ID format";
const ERROR_USER_NOT_FOUND = "User not found";
const ERROR_USER_MAPPING_NOT_FOUND = "User mapping not found";
const DEFAULT_CURRENCY = "JPY";

interface RecentSubscription {
  id: number;
  status: string;
  registrationDate: string;
  productName: string;
}

interface RecentInvoice {
  id: number;
  status: string;
  dueDate?: string | undefined;
  total: number;
  number: string;
  issuedAt?: string | undefined;
  paidDate?: string | undefined;
  currency?: string | null | undefined;
}

function getDateTimestamp(dateStr: string | undefined | null, fallback: number): number {
  return dateStr ? new Date(dateStr).getTime() : fallback;
}

function processSubscriptionsData(subscriptions: Subscription[]): {
  activeCount: number;
  recentSubscriptions: RecentSubscription[];
} {
  const activeSubscriptions = subscriptions.filter(sub => sub.status === "Active");
  const activeCount = activeSubscriptions.length;

  const recentSubscriptions = activeSubscriptions
    .sort((a, b) => {
      const aTime = getDateTimestamp(a.registrationDate, Number.NEGATIVE_INFINITY);
      const bTime = getDateTimestamp(b.registrationDate, Number.NEGATIVE_INFINITY);
      return bTime - aTime;
    })
    .slice(0, 3)
    .map(sub => ({
      id: sub.id,
      status: sub.status,
      registrationDate: sub.registrationDate,
      productName: sub.productName,
    }));

  return { activeCount, recentSubscriptions };
}

function findNextInvoice(invoices: Invoice[]): NextInvoice | null {
  const upcomingInvoices = invoices
    .filter(inv => (inv.status === "Unpaid" || inv.status === "Overdue") && inv.dueDate)
    .sort((a, b) => {
      const aTime = getDateTimestamp(a.dueDate, Number.POSITIVE_INFINITY);
      const bTime = getDateTimestamp(b.dueDate, Number.POSITIVE_INFINITY);
      return aTime - bTime;
    });

  const invoice = upcomingInvoices[0];
  if (!invoice?.dueDate) {
    return null;
  }

  return {
    id: invoice.id,
    dueDate: invoice.dueDate,
    amount: invoice.total,
    currency: invoice.currency ?? DEFAULT_CURRENCY,
  };
}

function processInvoicesData(invoices: Invoice[]): RecentInvoice[] {
  return invoices
    .sort((a, b) => {
      const aTime = getDateTimestamp(a.issuedAt, Number.NEGATIVE_INFINITY);
      const bTime = getDateTimestamp(b.issuedAt, 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,
    }));
}

function buildInvoiceActivities(invoices: RecentInvoice[]): Activity[] {
  const activities: Activity[] = [];

  for (const invoice of invoices) {
    const baseMetadata: Record<string, string | number | boolean | null> = {
      amount: invoice.total,
      currency: invoice.currency ?? DEFAULT_CURRENCY,
    };
    if (invoice.dueDate) {
      baseMetadata["dueDate"] = invoice.dueDate;
    }
    if (invoice.number) {
      baseMetadata["invoiceNumber"] = invoice.number;
    }

    if (invoice.status === "Paid") {
      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: baseMetadata,
      });
    } else if (invoice.status === "Unpaid" || invoice.status === "Overdue") {
      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: { ...baseMetadata, status: invoice.status },
      });
    }
  }

  return activities;
}

function buildSubscriptionActivities(subscriptions: RecentSubscription[]): Activity[] {
  return subscriptions.map(subscription => {
    const metadata: Record<string, string | number | boolean | null> = {
      productName: subscription.productName,
      status: subscription.status,
    };
    if (subscription.registrationDate) {
      metadata["registrationDate"] = subscription.registrationDate;
    }

    return {
      id: `service-activated-${subscription.id}`,
      type: "service_activated",
      title: `${subscription.productName} activated`,
      description: "Service successfully provisioned",
      date: subscription.registrationDate,
      relatedId: subscription.id,
      metadata,
    };
  });
}

/**
 * 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 {
  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;
  private readonly addressReconcileQueue: AddressReconcileQueueService;
  private readonly auditService: AuditService;
  private readonly logger: Logger;

  // eslint-disable-next-line max-params
  constructor(
    userAuthRepository: UserAuthRepository,
    mappingsService: MappingsService,
    whmcsClientService: WhmcsClientService,
    whmcsInvoiceService: WhmcsInvoiceService,
    whmcsSubscriptionService: WhmcsSubscriptionService,
    salesforceService: SalesforceFacade,
    configService: ConfigService,
    addressReconcileQueue: AddressReconcileQueueService,
    auditService: AuditService,
    @Inject(Logger) logger: Logger
  ) {
    this.userAuthRepository = userAuthRepository;
    this.mappingsService = mappingsService;
    this.whmcsClientService = whmcsClientService;
    this.whmcsInvoiceService = whmcsInvoiceService;
    this.whmcsSubscriptionService = whmcsSubscriptionService;
    this.salesforceService = salesforceService;
    this.configService = configService;
    this.addressReconcileQueue = addressReconcileQueue;
    this.auditService = auditService;
    this.logger = logger;
  }

  async findById(userId: string): Promise<User | null> {
    const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);
    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, ERROR_INVALID_USER_ID);
    const user = await this.userAuthRepository.findById(validId);
    if (!user) {
      throw new NotFoundException(ERROR_USER_NOT_FOUND);
    }
    return this.getProfileForUser(user);
  }

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

  // eslint-disable-next-line no-restricted-syntax -- Mutation is intentional for this aggregator
  async updateAddress(userId: string, addressUpdate: Partial<Address>): Promise<Address> {
    const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);
    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 safeOperation(
      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 ?? {});
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Update address for user ${validId}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException],
        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)
   */
  // eslint-disable-next-line no-restricted-syntax -- Mutation is intentional for this aggregator
  async updateBilingualAddress(
    userId: string,
    bilingualAddress: BilingualAddress
  ): Promise<Address> {
    const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);

    return safeOperation(
      async () => {
        const mapping = await this.mappingsService.findByUserId(validId);
        if (!mapping?.whmcsClientId) {
          throw new NotFoundException(ERROR_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) {
          await this.syncSalesforceAddress(
            validId,
            mapping.sfAccountId,
            mapping.whmcsClientId,
            bilingualAddress
          );
        } else {
          this.logger.debug("No Salesforce mapping found, skipping Japanese address sync", {
            userId: validId,
          });
        }

        return this.fetchRefreshedAddress(validId, mapping.whmcsClientId);
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Update bilingual address for user ${validId}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException],
        fallbackMessage: "Unable to update address",
      }
    );
  }

  private async syncSalesforceAddress(
    userId: string,
    sfAccountId: string,
    whmcsClientId: number,
    bilingualAddress: BilingualAddress
  ): Promise<void> {
    const sfFields = prepareSalesforceContactAddressFields(bilingualAddress);
    const correlationId = randomUUID();
    const addressData = {
      mailingStreet: sfFields.MailingStreet,
      mailingCity: sfFields.MailingCity,
      mailingState: sfFields.MailingState,
      mailingPostalCode: sfFields.MailingPostalCode,
      mailingCountry: sfFields.MailingCountry,
      buildingName: sfFields.BuildingName__c,
      roomNumber: sfFields.RoomNumber__c,
    };

    try {
      await this.salesforceService.updateContactAddress(sfAccountId, addressData);

      this.logger.log("Successfully updated Japanese address in Salesforce", {
        userId,
        sfAccountId,
        correlationId,
      });

      await this.auditService.log({
        userId,
        action: AuditAction.ADDRESS_UPDATE,
        resource: "address",
        details: { sfAccountId, whmcsClientId, correlationId },
        success: true,
      });
    } catch (sfError) {
      const errorMessage = extractErrorMessage(sfError);

      this.logger.warn("Failed to update Salesforce address - queueing reconciliation", {
        userId,
        sfAccountId,
        error: errorMessage,
        correlationId,
      });

      await this.addressReconcileQueue.enqueueReconciliation({
        userId,
        sfAccountId,
        address: addressData,
        originalError: errorMessage,
        correlationId,
      });

      await this.auditService.log({
        userId,
        action: AuditAction.ADDRESS_RECONCILE_QUEUED,
        resource: "address",
        details: { sfAccountId, whmcsClientId, originalError: errorMessage, correlationId },
        success: true,
      });
    }
  }

  private async fetchRefreshedAddress(userId: string, whmcsClientId: number): Promise<Address> {
    const refreshedProfile = await this.getProfile(userId);
    if (refreshedProfile.address) {
      return refreshedProfile.address;
    }

    const refreshedAddress = await this.whmcsClientService.getClientAddress(whmcsClientId);
    return addressSchema.parse(refreshedAddress ?? {});
  }

  // eslint-disable-next-line no-restricted-syntax -- Mutation is intentional for this aggregator
  async updateProfile(userId: string, update: UpdateCustomerProfileRequest): Promise<User> {
    const validId = parseUuidOrThrow(userId, ERROR_INVALID_USER_ID);
    const parsed = updateCustomerProfileRequestSchema.parse(update);

    return safeOperation(
      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(ERROR_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(ERROR_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);
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Update profile for user ${validId}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException],
        fallbackMessage: "Unable to update profile",
      }
    );
  }

  async getUserSummary(userId: string): Promise<DashboardSummary> {
    return safeOperation(async () => this.buildUserSummary(userId), {
      criticality: OperationCriticality.CRITICAL,
      context: `Get user summary for ${userId}`,
      logger: this.logger,
      rethrow: [NotFoundException, BadRequestException],
      fallbackMessage: "Unable to retrieve dashboard summary",
    });
  }

  private async buildUserSummary(userId: string): Promise<DashboardSummary> {
    const user = await this.userAuthRepository.findById(userId);
    if (!user) {
      throw new NotFoundException(ERROR_USER_NOT_FOUND);
    }

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

    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,
      }),
    ]);

    const { activeCount, recentSubscriptions } = this.extractSubscriptionData(
      subscriptionsData,
      userId
    );
    const { unpaidCount, nextInvoice, recentInvoices } = this.extractInvoiceData(
      invoicesData,
      unpaidInvoicesData,
      userId
    );

    const activities = [
      ...buildInvoiceActivities(recentInvoices),
      ...buildSubscriptionActivities(recentSubscriptions),
    ]
      .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
      .slice(0, 10);

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

    const currency = await this.fetchClientCurrency(mapping.whmcsClientId, userId);

    return dashboardSummarySchema.parse({
      stats: {
        activeSubscriptions: activeCount,
        unpaidInvoices: unpaidCount,
        openCases: 0,
        currency,
      },
      nextInvoice,
      recentActivity: activities,
    });
  }

  private buildEmptySummary(): DashboardSummary {
    return {
      stats: {
        activeSubscriptions: 0,
        unpaidInvoices: 0,
        openCases: 0,
        currency: DEFAULT_CURRENCY,
      },
      nextInvoice: null,
      recentActivity: [],
    };
  }

  private extractSubscriptionData(
    subscriptionsData: PromiseSettledResult<{ subscriptions: Subscription[] }>,
    userId: string
  ): { activeCount: number; recentSubscriptions: RecentSubscription[] } {
    if (subscriptionsData.status === "fulfilled") {
      return processSubscriptionsData(subscriptionsData.value.subscriptions);
    }

    this.logger.error(
      `Failed to fetch subscriptions for user ${userId}:`,
      subscriptionsData.reason
    );
    return { activeCount: 0, recentSubscriptions: [] };
  }

  private extractInvoiceData(
    invoicesData: PromiseSettledResult<{ invoices: Invoice[]; pagination: { totalItems: number } }>,
    unpaidInvoicesData: PromiseSettledResult<{ pagination: { totalItems: number } }>,
    userId: string
  ): { unpaidCount: number; nextInvoice: NextInvoice | null; recentInvoices: RecentInvoice[] } {
    let unpaidCount = 0;

    if (unpaidInvoicesData.status === "fulfilled") {
      unpaidCount = 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") {
      this.logger.error(`Failed to fetch invoices for user ${userId}`, {
        reason: extractErrorMessage(invoicesData.reason),
      });
      return { unpaidCount, nextInvoice: null, recentInvoices: [] };
    }

    const invoices = invoicesData.value.invoices;

    if (unpaidInvoicesData.status === "rejected") {
      unpaidCount = invoices.filter(
        inv => inv.status === "Unpaid" || inv.status === "Overdue"
      ).length;
    }

    return {
      unpaidCount,
      nextInvoice: findNextInvoice(invoices),
      recentInvoices: processInvoicesData(invoices),
    };
  }

  private async fetchClientCurrency(whmcsClientId: number, userId: string): Promise<string> {
    try {
      const client = await this.whmcsClientService.getClientDetails(whmcsClientId);
      const resolvedCurrency =
        typeof client.currency_code === "string" && client.currency_code.trim().length > 0
          ? client.currency_code
          : null;
      return resolvedCurrency ?? DEFAULT_CURRENCY;
    } catch (error) {
      this.logger.warn("Could not fetch currency from WHMCS client", {
        userId,
        error: extractErrorMessage(error),
      });
      return DEFAULT_CURRENCY;
    }
  }

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

    return safeOperation(
      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,
        };
      },
      {
        criticality: OperationCriticality.CRITICAL,
        context: `Fetch client profile from WHMCS for user ${user.id}`,
        logger: this.logger,
        rethrow: [NotFoundException, BadRequestException],
        fallbackMessage: "Unable to retrieve customer profile from billing system",
      }
    );
  }
}
