import { InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService, PrismaService } from '@modules/shared';
import { type PropertyType, ListingStatus, Prisma } from '@prisma/client';
import { GetMarketSnapshotQuery } from './get-market-snapshot.query';

export interface PriceChangePct {
  d1: number;
  d7: number;
  d30: number;
}

export interface MarketSnapshotDto {
  city: string;
  propertyType?: PropertyType;
  activeCount: number;
  avgPrice: number;
  medianPrice: number;
  priceChangePct: PriceChangePct;
  avgPricePerM2: number;
  daysOnMarket: number;
  newListings24h: number;
  cachedAt: string | null;
  nextRefreshAt: string | null;
}

@QueryHandler(GetMarketSnapshotQuery)
export class GetMarketSnapshotHandler implements IQueryHandler<GetMarketSnapshotQuery> {
  constructor(
    private readonly prisma: PrismaService,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
  ) {}

  async execute(query: GetMarketSnapshotQuery): Promise<MarketSnapshotDto> {
    try {
      const cacheKey = CacheService.buildKey(
        CachePrefix.MARKET_SNAPSHOT,
        query.city,
        query.propertyType,
      );

      return await this.cache.getOrSet(
        cacheKey,
        () => this.computeSnapshot(query.city, query.propertyType),
        CacheTTL.MARKET_SNAPSHOT,
        'market_snapshot',
      );
    } catch (error) {
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to get market snapshot: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException(
        'Không thể truy vấn tổng quan thị trường. Vui lòng thử lại sau.',
      );
    }
  }

  private async computeSnapshot(
    city: string,
    propertyType?: PropertyType,
  ): Promise<MarketSnapshotDto> {
    const now = new Date();
    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
    const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
    const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

    const propertyWhere: Prisma.PropertyWhereInput = {
      city: { equals: city, mode: 'insensitive' },
      ...(propertyType ? { propertyType } : {}),
    };

    const baseListingWhere: Prisma.ListingWhereInput = {
      status: ListingStatus.ACTIVE,
      property: propertyWhere,
    };

    // Run queries in parallel for performance
    const [
      activeAgg,
      medianResult,
      newListings24h,
      avgDaysOnMarket,
      priceChange1d,
      priceChange7d,
      priceChange30d,
    ] = await Promise.all([
      // Active listings count + avg price + avg price/m2
      this.prisma.listing.aggregate({
        where: baseListingWhere,
        _count: true,
        _avg: {
          priceVND: true,
          pricePerM2: true,
        },
      }),

      // Median price via raw SQL for efficiency
      this.prisma.$queryRaw<{ median: bigint | null }[]>`
        SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY l."priceVND")::bigint AS median
        FROM "Listing" l
        JOIN "Property" p ON p.id = l."propertyId"
        WHERE l.status = 'ACTIVE'
          AND LOWER(p.city) = LOWER(${city})
          ${propertyType ? Prisma.sql`AND p."propertyType" = ${propertyType}::"PropertyType"` : Prisma.empty}
      `,

      // New listings in last 24h
      this.prisma.listing.count({
        where: {
          ...baseListingWhere,
          publishedAt: { gte: oneDayAgo },
        },
      }),

      // Average days on market
      this.prisma.$queryRaw<{ avg_days: number | null }[]>`
        SELECT AVG(EXTRACT(EPOCH FROM (NOW() - l."publishedAt")) / 86400)::float AS avg_days
        FROM "Listing" l
        JOIN "Property" p ON p.id = l."propertyId"
        WHERE l.status = 'ACTIVE'
          AND l."publishedAt" IS NOT NULL
          AND LOWER(p.city) = LOWER(${city})
          ${propertyType ? Prisma.sql`AND p."propertyType" = ${propertyType}::"PropertyType"` : Prisma.empty}
      `,

      // Price change %: compare current avg vs avg of listings from 1d/7d/30d ago
      this.computePriceChangePct(city, propertyType, oneDayAgo, now),
      this.computePriceChangePct(city, propertyType, sevenDaysAgo, oneDayAgo),
      this.computePriceChangePct(city, propertyType, thirtyDaysAgo, sevenDaysAgo),
    ]);

    const currentAvg = Number(activeAgg._avg.priceVND ?? 0);
    const median = medianResult[0]?.median ? Number(medianResult[0].median) : 0;
    const avgPricePerM2 = activeAgg._avg.pricePerM2 ?? 0;
    const daysOnMarket = Math.round(avgDaysOnMarket[0]?.avg_days ?? 0);

    return {
      city,
      propertyType,
      activeCount: activeAgg._count,
      avgPrice: currentAvg,
      medianPrice: median,
      priceChangePct: {
        d1: this.calcChangePct(currentAvg, priceChange1d),
        d7: this.calcChangePct(currentAvg, priceChange7d),
        d30: this.calcChangePct(currentAvg, priceChange30d),
      },
      avgPricePerM2: Math.round(avgPricePerM2),
      daysOnMarket,
      newListings24h,
      cachedAt: null,       // Filled by CacheMetaInterceptor
      nextRefreshAt: null,   // Filled by CacheMetaInterceptor
    };
  }

  private async computePriceChangePct(
    city: string,
    propertyType: PropertyType | undefined,
    from: Date,
    to: Date,
  ): Promise<number> {
    const result = await this.prisma.$queryRaw<{ avg_price: number | null }[]>`
      SELECT AVG(l."priceVND")::float AS avg_price
      FROM "Listing" l
      JOIN "Property" p ON p.id = l."propertyId"
      WHERE l.status = 'ACTIVE'
        AND l."publishedAt" >= ${from}
        AND l."publishedAt" < ${to}
        AND LOWER(p.city) = LOWER(${city})
        ${propertyType ? Prisma.sql`AND p."propertyType" = ${propertyType}::"PropertyType"` : Prisma.empty}
    `;
    return result[0]?.avg_price ?? 0;
  }

  private calcChangePct(current: number, previous: number): number {
    if (!previous || previous === 0) return 0;
    return Math.round(((current - previous) / previous) * 1000) / 10; // 1 decimal
  }
}
