import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs';
import {
  DomainException,
  ForbiddenException,
  NotFoundException,
  ValidationException,
  CacheService,
  CachePrefix,
  LoggerService,
} from '@modules/shared';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
import { PROPERTY_REPOSITORY, type IPropertyRepository } from '../../../domain/repositories/property.repository';
import { UpdateListingCommand } from './update-listing.command';

export interface UpdateListingResult {
  listingId: string;
  status: string;
  updatedFields: string[];
  resubmittedForModeration: boolean;
}

@CommandHandler(UpdateListingCommand)
export class UpdateListingHandler implements ICommandHandler<UpdateListingCommand> {
  constructor(
    @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
    @Inject(PROPERTY_REPOSITORY) private readonly propertyRepo: IPropertyRepository,
    private readonly eventBus: EventBus,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
  ) {}

  async execute(command: UpdateListingCommand): Promise<UpdateListingResult> {
    try {
      // 1. Load listing
      const listing = await this.listingRepo.findById(command.listingId);
      if (!listing) {
        throw new NotFoundException('Listing', command.listingId);
      }

      // 2. Ownership check: only the seller, assigned agent, or admin can edit
      const isOwner = listing.sellerId === command.userId;
      const isAgent = listing.agentId !== null && listing.agentId === command.userId;
      const isAdmin = command.userRole === 'ADMIN';
      if (!isOwner && !isAgent && !isAdmin) {
        throw new ForbiddenException(
          'Chỉ người bán, môi giới được giao hoặc quản trị viên mới có thể chỉnh sửa tin đăng',
        );
      }

      // 2b. Ownership-transfer guard: only admin OR current owner-agent may
      // change `agentId`. `undefined` → no change. Equal → no-op (handled by
      // domain method, but checked here so we don't audit a non-change).
      const wantsOwnershipTransfer =
        command.agentId !== undefined && command.agentId !== listing.agentId;
      if (wantsOwnershipTransfer) {
        const isCurrentOwnerAgent =
          listing.agentId !== null && listing.agentId === command.userId;
        if (!isAdmin && !isCurrentOwnerAgent) {
          throw new ForbiddenException(
            'Chỉ quản trị viên hoặc môi giới hiện tại mới có quyền chuyển ownership tin đăng',
          );
        }
      }

      // 3. Validate no fields are being sent (empty update)
      const hasListingUpdates =
        command.priceVND !== undefined || command.rentPriceMonthly !== undefined;
      const extras = command.extras;
      const hasExtrasUpdates =
        extras !== undefined &&
        (
          extras.furnishing !== undefined ||
          extras.propertyCondition !== undefined ||
          extras.balconyDirection !== undefined ||
          extras.maintenanceFeeVND !== undefined ||
          extras.parkingSlots !== undefined ||
          extras.viewType !== undefined ||
          extras.petFriendly !== undefined ||
          extras.suitableFor !== undefined ||
          extras.whyThisLocation !== undefined
        );
      const hasPropertyUpdates =
        command.title !== undefined ||
        command.description !== undefined ||
        command.amenities !== undefined ||
        hasExtrasUpdates;
      const hasMediaOrderUpdate =
        command.mediaOrder !== undefined && command.mediaOrder.length > 0;

      if (
        !hasListingUpdates &&
        !hasPropertyUpdates &&
        !hasMediaOrderUpdate &&
        !wantsOwnershipTransfer
      ) {
        throw new ValidationException('Không có trường nào được cập nhật', {});
      }

      // 4. Load property for property-level updates
      const property = await this.propertyRepo.findById(listing.propertyId);
      if (!property) {
        throw new NotFoundException('Property', listing.propertyId);
      }

      // 5. Apply updates to domain entities
      const allUpdatedFields: string[] = [];
      const previousAgentId = listing.agentId;

      // Ownership transfer (must run before re-moderation so the audit
      // event reflects the new owner). Domain method is a no-op when the
      // requested agentId equals the current one.
      if (wantsOwnershipTransfer) {
        const transferred = listing.transferOwnership(
          command.agentId ?? null,
          command.userId,
          command.userRole ?? 'USER',
        );
        if (transferred) {
          allUpdatedFields.push('agentId');
        }
      }

      // Update listing fields (price, rentPriceMonthly)
      if (hasListingUpdates) {
        const listingUpdated = listing.updateContent({
          priceVND: command.priceVND,
          rentPriceMonthly: command.rentPriceMonthly,
          areaM2: property.areaM2, // needed for pricePerM2 recalculation
        });
        allUpdatedFields.push(...listingUpdated);
      }

      // Update property fields (title, description, amenities, rich fields)
      if (hasPropertyUpdates) {
        const propertyUpdated = property.updateContent({
          title: command.title,
          description: command.description,
          amenities: command.amenities,
          ...(extras?.furnishing !== undefined && { furnishing: extras.furnishing }),
          ...(extras?.propertyCondition !== undefined && { propertyCondition: extras.propertyCondition }),
          ...(extras?.balconyDirection !== undefined && { balconyDirection: extras.balconyDirection }),
          ...(extras?.maintenanceFeeVND !== undefined && { maintenanceFeeVND: extras.maintenanceFeeVND }),
          ...(extras?.parkingSlots !== undefined && { parkingSlots: extras.parkingSlots }),
          ...(extras?.viewType !== undefined && { viewType: extras.viewType }),
          ...(extras?.petFriendly !== undefined && { petFriendly: extras.petFriendly }),
          ...(extras?.suitableFor !== undefined && { suitableFor: extras.suitableFor }),
          ...(extras?.whyThisLocation !== undefined && { whyThisLocation: extras.whyThisLocation }),
        });
        allUpdatedFields.push(...propertyUpdated);
      }

      // Update media order
      if (hasMediaOrderUpdate) {
        await this.propertyRepo.updateMediaOrder(listing.propertyId, command.mediaOrder!);
        allUpdatedFields.push('mediaOrder');
      }

      // 6. If listing was ACTIVE, transition to PENDING_REVIEW for re-moderation
      const previousStatus = listing.status;
      listing.markEditedForReModeration(property.id, allUpdatedFields);
      const resubmitted = previousStatus === 'ACTIVE' && listing.status === 'PENDING_REVIEW';

      // 7. Persist changes
      await this.listingRepo.update(listing);
      if (hasPropertyUpdates) {
        await this.propertyRepo.update(property);
      }

      // 8. Publish domain events
      const listingEvents = listing.clearDomainEvents();
      const propertyEvents = property.clearDomainEvents();
      for (const event of [...listingEvents, ...propertyEvents]) {
        this.eventBus.publish(event);
      }

      // 9. Invalidate caches
      const cacheInvalidations: Promise<void>[] = [
        this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
        this.cache.invalidateByPrefix(CachePrefix.SEARCH),
        this.cache.invalidateByPrefix(CachePrefix.MARKET_DISTRICT),
        this.cache.invalidateByPrefix(CachePrefix.MARKET_REPORT),
      ];
      // On ownership transfer, also invalidate per-agent listing caches for
      // both the previous and the new owner so their dashboards refresh.
      if (allUpdatedFields.includes('agentId')) {
        if (previousAgentId !== null) {
          cacheInvalidations.push(
            this.cache.invalidate(
              CacheService.buildKey(CachePrefix.AGENT_LISTINGS, previousAgentId),
            ),
          );
        }
        if (listing.agentId !== null) {
          cacheInvalidations.push(
            this.cache.invalidate(
              CacheService.buildKey(CachePrefix.AGENT_LISTINGS, listing.agentId),
            ),
          );
        }
      }
      await Promise.all(cacheInvalidations);

      return {
        listingId: listing.id,
        status: listing.status,
        updatedFields: allUpdatedFields,
        resubmittedForModeration: resubmitted,
      };
    } catch (error) {
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to update listing ${command.listingId}: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException('Không thể cập nhật tin đăng');
    }
  }
}
