import { EventsHandler, type IEventHandler } from '@nestjs/cqrs';
import { ListingApprovedEvent } from '@modules/admin/domain/events/listing-approved.event';
import { InquiryReadEvent } from '@modules/inquiries/domain/events/inquiry-read.event';
import { ListingPriceChangedEvent } from '@modules/listings/domain/events/listing-price-changed.event';
import { LoggerService, PrismaService } from '@modules/shared';
import { NotificationsGateway } from '../../presentation/gateways/notifications.gateway';

const CONTEXT = 'ResidentialEventsListener';

/**
 * Shape of the `filters` JSON column on `SavedSearch`. Matches fields
 * consumed by the saved-search alert matcher. Anything else is ignored.
 */
interface SavedSearchFilters {
  transactionType?: string;
  propertyType?: string;
  projectId?: string;
  district?: string;
  city?: string;
  priceMin?: number;
  priceMax?: number;
  areaMin?: number;
  areaMax?: number;
  bedrooms?: number;
}

/**
 * Fans residential domain events out as Socket.IO events on the
 * `/notifications` namespace so subscribed users get live updates
 * without waiting for the email/push pipeline.
 *
 * Three WS events are emitted:
 *   • `residential:price-drop` — listing price lowered and matches an
 *     alert-enabled saved search.
 *   • `residential:new-listing-in-project` — approved listing lives in
 *     a project that the user tracks via `filters.projectId`.
 *   • `residential:inquiry-reply` — the listing owner/agent marked the
 *     user's inquiry as read, signalling that a reply is incoming.
 *
 * Redis pub/sub fan-out is handled by {@link RedisIoAdapter}, so the
 * broadcast reaches the user's socket regardless of which API pod
 * holds the connection.
 */
@EventsHandler(ListingPriceChangedEvent)
export class ResidentialPriceDropListener
  implements IEventHandler<ListingPriceChangedEvent>
{
  constructor(
    private readonly prisma: PrismaService,
    private readonly gateway: NotificationsGateway,
    private readonly logger: LoggerService,
  ) {}

  async handle(event: ListingPriceChangedEvent): Promise<void> {
    if (event.newPrice >= event.oldPrice) {
      return;
    }

    try {
      const listing = await this.prisma.listing.findUnique({
        where: { id: event.aggregateId },
        include: { property: true },
      });
      if (!listing || !listing.property) return;

      const savedSearches = await this.prisma.savedSearch.findMany({
        where: { alertEnabled: true },
        select: { id: true, userId: true, name: true, filters: true },
      });

      let matchCount = 0;
      for (const search of savedSearches) {
        if (search.userId === listing.sellerId) continue;

        const filters = normalizeFilters(search.filters);
        if (!matchesFilters(listing, listing.property, filters)) continue;

        this.gateway.emitResidentialEvent(search.userId, 'residential:price-drop', {
          listingId: listing.id,
          savedSearchId: search.id,
          savedSearchName: search.name,
          title: listing.property.title,
          oldPrice: event.oldPrice.toString(),
          newPrice: event.newPrice.toString(),
          district: listing.property.district,
          city: listing.property.city,
          occurredAt: event.occurredAt.toISOString(),
        });
        matchCount++;
      }

      if (matchCount > 0) {
        this.logger.log(
          `Emitted residential:price-drop to ${matchCount} users for listing ${listing.id}`,
          CONTEXT,
        );
      }
    } catch (err) {
      this.logger.warn(
        `Price-drop WS emission failed for listing ${event.aggregateId}: ${
          err instanceof Error ? err.message : String(err)
        }`,
        CONTEXT,
      );
    }
  }
}

@EventsHandler(ListingApprovedEvent)
export class ResidentialNewListingInProjectListener
  implements IEventHandler<ListingApprovedEvent>
{
  constructor(
    private readonly prisma: PrismaService,
    private readonly gateway: NotificationsGateway,
    private readonly logger: LoggerService,
  ) {}

  async handle(event: ListingApprovedEvent): Promise<void> {
    try {
      const listing = await this.prisma.listing.findUnique({
        where: { id: event.aggregateId },
        include: { property: true },
      });
      if (!listing || !listing.property?.projectDevelopmentId) return;

      const projectId = listing.property.projectDevelopmentId;

      const savedSearches = await this.prisma.savedSearch.findMany({
        where: { alertEnabled: true },
        select: { id: true, userId: true, name: true, filters: true },
      });

      let matchCount = 0;
      for (const search of savedSearches) {
        if (search.userId === listing.sellerId) continue;

        const filters = normalizeFilters(search.filters);
        if (filters.projectId !== projectId) continue;

        this.gateway.emitResidentialEvent(
          search.userId,
          'residential:new-listing-in-project',
          {
            listingId: listing.id,
            projectId,
            savedSearchId: search.id,
            savedSearchName: search.name,
            title: listing.property.title,
            price: listing.priceVND.toString(),
            district: listing.property.district,
            city: listing.property.city,
            occurredAt: event.occurredAt.toISOString(),
          },
        );
        matchCount++;
      }

      if (matchCount > 0) {
        this.logger.log(
          `Emitted residential:new-listing-in-project to ${matchCount} users for project ${projectId}`,
          CONTEXT,
        );
      }
    } catch (err) {
      this.logger.warn(
        `New-listing-in-project WS emission failed for listing ${event.aggregateId}: ${
          err instanceof Error ? err.message : String(err)
        }`,
        CONTEXT,
      );
    }
  }
}

@EventsHandler(InquiryReadEvent)
export class ResidentialInquiryReplyListener
  implements IEventHandler<InquiryReadEvent>
{
  constructor(
    private readonly gateway: NotificationsGateway,
    private readonly logger: LoggerService,
  ) {}

  async handle(event: InquiryReadEvent): Promise<void> {
    try {
      this.gateway.emitResidentialEvent(event.userId, 'residential:inquiry-reply', {
        inquiryId: event.aggregateId,
        listingId: event.listingId,
        occurredAt: event.occurredAt.toISOString(),
      });
    } catch (err) {
      this.logger.warn(
        `Inquiry-reply WS emission failed for inquiry ${event.aggregateId}: ${
          err instanceof Error ? err.message : String(err)
        }`,
        CONTEXT,
      );
    }
  }
}

/* ────────────────────────────────────────────
 * Private helpers
 * ──────────────────────────────────────────── */

function normalizeFilters(raw: unknown): SavedSearchFilters {
  if (!raw || typeof raw !== 'object') return {};
  return raw as SavedSearchFilters;
}

function matchesFilters(
  listing: { transactionType: string; priceVND: bigint; sellerId: string },
  property: {
    propertyType: string;
    areaM2: number;
    bedrooms: number | null;
    district: string;
    city: string;
  },
  filters: SavedSearchFilters,
): boolean {
  if (filters.transactionType && filters.transactionType !== listing.transactionType) return false;
  if (filters.propertyType && filters.propertyType !== property.propertyType) return false;
  if (filters.district && filters.district !== property.district) return false;
  if (filters.city && filters.city !== property.city) return false;

  const price = Number(listing.priceVND);
  if (filters.priceMin !== undefined && price < Number(filters.priceMin)) return false;
  if (filters.priceMax !== undefined && price > Number(filters.priceMax)) return false;
  if (filters.areaMin !== undefined && property.areaM2 < Number(filters.areaMin)) return false;
  if (filters.areaMax !== undefined && property.areaM2 > Number(filters.areaMax)) return false;
  if (
    filters.bedrooms !== undefined &&
    property.bedrooms !== null &&
    property.bedrooms < Number(filters.bedrooms)
  ) {
    return false;
  }

  return true;
}
