import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
import { AVM_SERVICE, type IAVMService } from '@modules/analytics';
import { type ListingDetailData } from '../../../domain/repositories/listing-read.dto';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
import { GetListingQuery } from './get-listing.query';

/** @deprecated Use ListingDetailData from listing-read.dto instead */
export type ListingDetailDto = ListingDetailData;

@QueryHandler(GetListingQuery)
export class GetListingHandler implements IQueryHandler<GetListingQuery> {
  constructor(
    @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
    @Inject(AVM_SERVICE) private readonly avmService: IAVMService,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
  ) {}

  /**
   * Returns listing detail or null when not found.
   * The controller is responsible for mapping null to a 404 HttpException.
   *
   * Enrichment added on top of the base repository query:
   *  - `valuationEstimate`   — cached 24 h per listing id; null on AVM failure
   *  - `inquiryCount`        — gated: visible only to owner or ADMIN; public gets null
   *  - `agentQualityScore`   — denormalised from the agent record in the repo query
   *  - `similarCount`        — counted in the repo query
   */
  async execute(query: GetListingQuery): Promise<ListingDetailData | null> {
    try {
      const cacheKey = CacheService.buildKey(CachePrefix.LISTING, query.listingId);

      // Load base listing (cached 5 min)
      const base = await this.cache.getOrSet<ListingDetailData | null>(
        cacheKey,
        async () => {
          const result = await this.listingRepo.findByIdWithProperty(query.listingId);
          if (!result) {
            throw new ListingNotFoundSignal();
          }
          return result;
        },
        CacheTTL.LISTING_DETAIL,
        'listing',
      );

      // ------------------------------------------------------------------
      // AVM valuation — cached separately for 24 h keyed by listing id
      // ------------------------------------------------------------------
      const valuationCacheKey = CacheService.buildKey(CachePrefix.VALUATION, query.listingId);
      let valuationEstimate: ListingDetailData['valuationEstimate'] = null;
      try {
        valuationEstimate = await this.cache.getOrSet(
          valuationCacheKey,
          async () => {
            const result = await this.avmService.estimateValue({ propertyId: base!.property.id });
            const estimate: ListingDetailData['valuationEstimate'] = {
              value: result.estimatedPrice,
              confidence: result.confidence,
              modelVersion: result.modelVersion,
              estimatedAt: new Date().toISOString(),
            };
            return estimate;
          },
          CacheTTL.VALUATION_LISTING,
          'valuation',
        );
      } catch (avmError) {
        // AVM failure is non-fatal — return null, log for observability
        this.logger.warn(
          `AVM estimation failed for listing ${query.listingId}: ${avmError instanceof Error ? avmError.message : avmError}`,
          this.constructor.name,
        );
      }

      // ------------------------------------------------------------------
      // Access-gate inquiryCount: only owner or ADMIN may see it
      // ------------------------------------------------------------------
      const { caller } = query;
      const isOwner = caller != null && base!.seller.id === caller.userId;
      const isAdmin = caller?.role === 'ADMIN';
      const inquiryCount: number | null =
        isOwner || isAdmin ? (base!.inquiryCount as number) : null;

      return {
        ...base!,
        valuationEstimate,
        inquiryCount,
      };
    } catch (error) {
      // Not-found: return null without caching so subsequent requests can find a newly-created listing
      if (error instanceof ListingNotFoundSignal) return null;
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to get listing ${query.listingId}: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException('Không thể lấy thông tin tin đăng');
    }
  }
}

/** Internal signal to avoid caching not-found results. Not a domain exception. */
class ListingNotFoundSignal extends Error {
  constructor() {
    super('listing_not_found');
    this.name = 'ListingNotFoundSignal';
  }
}

