import { Injectable } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PropertyType } from '@prisma/client';
import { PrismaService, LoggerService } from '@modules/shared';
import { UpdateMarketIndexCommand } from '../../application/commands/update-market-index/update-market-index.command';

interface MarketStats {
  district: string;
  city: string;
  propertyType: PropertyType;
  medianPrice: bigint;
  avgPriceM2: number;
  totalListings: number;
  avgDaysOnMarket: number;
  inventoryLevel: number;
}

@Injectable()
export class MarketIndexCronService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly commandBus: CommandBus,
    private readonly logger: LoggerService,
  ) {}

  @Cron(CronExpression.EVERY_DAY_AT_2AM, { name: 'market-index-calculation' })
  async calculateMarketIndices(): Promise<void> {
    this.logger.log('Starting market index calculation...', 'MarketIndexCronService');

    const period = this.getCurrentPeriod();

    try {
      const stats = await this.aggregateMarketStats();

      let updatedCount = 0;
      for (const stat of stats) {
        try {
          await this.commandBus.execute(
            new UpdateMarketIndexCommand(
              stat.district,
              stat.city,
              stat.propertyType,
              period,
              stat.medianPrice,
              stat.avgPriceM2,
              stat.totalListings,
              stat.avgDaysOnMarket,
              stat.inventoryLevel,
            ),
          );
          updatedCount++;
        } catch (err) {
          this.logger.error(
            `Failed to update market index for ${stat.district}/${stat.city}/${stat.propertyType}: ${(err as Error).message}`,
            undefined,
            'MarketIndexCronService',
          );
        }
      }

      this.logger.log(
        `Market index calculation completed: ${updatedCount}/${stats.length} indices updated for period ${period}`,
        'MarketIndexCronService',
      );
    } catch (err) {
      this.logger.error(`Market index calculation failed: ${(err as Error).message}`, undefined, 'MarketIndexCronService');
    }
  }

  private async aggregateMarketStats(): Promise<MarketStats[]> {
    const propertyTypes = Object.values(PropertyType);

    const stats = await this.prisma.$queryRaw<
      Array<{
        district: string;
        city: string;
        property_type: PropertyType;
        median_price: bigint;
        avg_price_m2: number;
        total_listings: bigint;
        avg_days_on_market: number;
        inventory_level: bigint;
      }>
    >`
      SELECT
        p.district,
        p.city,
        p."propertyType" AS property_type,
        PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY l."priceVND") AS median_price,
        AVG(l."pricePerM2")::float AS avg_price_m2,
        COUNT(l.id) AS total_listings,
        AVG(
          EXTRACT(EPOCH FROM (NOW() - l."publishedAt")) / 86400
        )::float AS avg_days_on_market,
        COUNT(l.id) FILTER (WHERE l.status = 'ACTIVE') AS inventory_level
      FROM "Listing" l
      JOIN "Property" p ON l."propertyId" = p.id
      WHERE l.status IN ('ACTIVE', 'SOLD', 'RENTED')
        AND l."publishedAt" IS NOT NULL
        AND l."publishedAt" >= NOW() - INTERVAL '90 days'
        AND p."propertyType" = ANY(${propertyTypes}::"PropertyType"[])
      GROUP BY p.district, p.city, p."propertyType"
      HAVING COUNT(l.id) >= 3
      ORDER BY p.city, p.district, p."propertyType"
    `;

    return stats.map((s) => ({
      district: s.district,
      city: s.city,
      propertyType: s.property_type,
      medianPrice: BigInt(Math.round(Number(s.median_price))),
      avgPriceM2: s.avg_price_m2,
      totalListings: Number(s.total_listings),
      avgDaysOnMarket: Math.round(s.avg_days_on_market * 10) / 10,
      inventoryLevel: Number(s.inventory_level),
    }));
  }

  private getCurrentPeriod(): string {
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0');
    return `${year}-${month}`;
  }
}
