import { Injectable } from '@nestjs/common';
import { type PropertyType } from '@prisma/client';
import { PrismaService } from '@modules/shared';
import {
  type DuplicateCandidate,
  type DuplicateCheckParams,
  type IDuplicateDetector,
} from '../../domain/services/duplicate-detector';

interface NearbyRow {
  listing_id: string;
  property_id: string;
  title: string;
  address: string;
  address_normalized: string | null;
  district: string;
  property_type: PropertyType;
  distance_meters: number;
  agent_id: string | null;
}

/**
 * Duplicate detector backed by PostGIS `ST_DWithin` + an equality lookup on
 * `Property.addressNormalized`.
 *
 * Hard duplicate (TEC-2932): same `agentId`, same `addressNormalized`,
 * within {@link DEFAULT_RADIUS_METERS}. The handler maps this to HTTP 409.
 *
 * Soft duplicate: nearby listing within radius (regardless of agent /
 * address match). Surfaced as a warning in the create-listing response.
 */
const DEFAULT_RADIUS_METERS = 50;

@Injectable()
export class PrismaDuplicateDetector implements IDuplicateDetector {
  constructor(private readonly prisma: PrismaService) {}

  async findDuplicates(params: DuplicateCheckParams): Promise<DuplicateCandidate[]> {
    const radiusMeters = params.radiusMeters ?? DEFAULT_RADIUS_METERS;
    // Use a sentinel value so the SQL parameter binding stays uniform when
    // the caller is updating an existing property (no excludePropertyId).
    const excludeId = params.excludePropertyId ?? '';

    // Step 1: Find nearby properties using PostGIS ST_DWithin (uses GiST index).
    // We additionally fetch `addressNormalized` and the candidate's agentId so
    // the application layer can compute the hard-duplicate signal without a
    // second round-trip. Listings that are no longer live (sold, expired,
    // rejected, cancelled) are excluded — they should not block new listings.
    const nearbyRows = await this.prisma.$queryRaw<NearbyRow[]>`
      SELECT
        l."id"                AS listing_id,
        p."id"                AS property_id,
        p."title",
        p."address",
        p."addressNormalized" AS address_normalized,
        p."district",
        p."propertyType"      AS property_type,
        l."agentId"           AS agent_id,
        ST_Distance(
          p."location"::geography,
          ST_SetSRID(ST_MakePoint(${params.longitude}, ${params.latitude}), 4326)::geography
        ) AS distance_meters
      FROM "Property" p
      INNER JOIN "Listing" l ON l."propertyId" = p."id"
      WHERE p."id" != ${excludeId}
        AND p."propertyType" = ${params.propertyType}::"PropertyType"
        AND l."status" NOT IN ('SOLD', 'RENTED', 'EXPIRED', 'REJECTED', 'CANCELLED')
        AND ST_DWithin(
          p."location"::geography,
          ST_SetSRID(ST_MakePoint(${params.longitude}, ${params.latitude}), 4326)::geography,
          ${radiusMeters}
        )
      ORDER BY distance_meters ASC
      LIMIT 20
    `;

    return nearbyRows.map((row) => ({
      listingId: row.listing_id,
      propertyId: row.property_id,
      title: row.title,
      address: row.address,
      district: row.district,
      distanceMeters: Number(row.distance_meters),
      addressMatch:
        params.addressNormalized != null &&
        row.address_normalized != null &&
        row.address_normalized === params.addressNormalized,
      sameAgent:
        params.agentId != null &&
        row.agent_id != null &&
        row.agent_id === params.agentId,
      propertyType: row.property_type,
    }));
  }
}
