import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, CacheService, CachePrefix, CacheTTL, LoggerService } from '@modules/shared';
import {
  MARKET_INDEX_REPOSITORY,
  type IMarketIndexRepository,
  type MarketHistoryPoint,
} from '../../../domain/repositories/market-index.repository';
import { GetMarketHistoryQuery } from './get-market-history.query';

export interface MarketHistoryDto {
  city: string;
  points: MarketHistoryPoint[];
}

@QueryHandler(GetMarketHistoryQuery)
export class GetMarketHistoryHandler implements IQueryHandler<GetMarketHistoryQuery> {
  constructor(
    @Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
    private readonly cache: CacheService,
    private readonly logger: LoggerService,
  ) {}

  async execute(query: GetMarketHistoryQuery): Promise<MarketHistoryDto> {
    try {
      const cacheKey = CacheService.buildKey(
        CachePrefix.MARKET_HISTORY,
        query.city,
        query.period,
        query.granularity,
        query.propertyType ?? 'all',
      );

      return await this.cache.getOrSet(
        cacheKey,
        async () => {
          const periods = this.generatePeriods(query.period, query.granularity);
          const points = await this.marketIndexRepo.getMarketHistory(query.city, periods);
          return { city: query.city, points };
        },
        CacheTTL.MARKET_HISTORY,
        'market_history',
      );
    } catch (error) {
      if (error instanceof DomainException) throw error;
      this.logger.error(
        `Failed to get market history: ${error instanceof Error ? error.message : error}`,
        error instanceof Error ? error.stack : undefined,
        this.constructor.name,
      );
      throw new InternalServerErrorException(
        'Không thể truy vấn lịch sử thị trường. Vui lòng thử lại sau.',
      );
    }
  }

  /**
   * Generate period strings based on the requested look-back and granularity.
   * E.g. "12m" with "monthly" → ["2025-05", "2025-06", ..., "2026-04"]
   */
  private generatePeriods(period: string, granularity: 'monthly' | 'weekly'): string[] {
    const match = period.match(/^(\d+)m$/);
    const months = match?.[1] ? parseInt(match[1], 10) : 12;

    const now = new Date();
    const periods: string[] = [];

    if (granularity === 'monthly') {
      for (let i = months - 1; i >= 0; i--) {
        const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
        const yyyy = d.getFullYear();
        const mm = String(d.getMonth() + 1).padStart(2, '0');
        periods.push(`${yyyy}-${mm}`);
      }
    } else {
      // weekly: generate ISO week strings for the past N months
      const startDate = new Date(now.getFullYear(), now.getMonth() - months, now.getDate());
      const cursor = new Date(startDate);
      while (cursor <= now) {
        const yyyy = cursor.getFullYear();
        const week = this.getISOWeek(cursor);
        periods.push(`${yyyy}-W${String(week).padStart(2, '0')}`);
        cursor.setDate(cursor.getDate() + 7);
      }
    }

    return periods;
  }

  private getISOWeek(date: Date): number {
    const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
    const dayNum = d.getUTCDay() || 7;
    d.setUTCDate(d.getUTCDate() + 4 - dayNum);
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
    return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
  }
}
