import { Injectable } from '@nestjs/common';
import { type PropertyType } from '@prisma/client';
import type { PrismaService } from '@modules/shared';
import {
  type IAVMService,
  type AVMParams,
  type ValuationResult,
  type Comparable,
} from '../../domain/services/avm-service';

const MODEL_VERSION = 'avm-v1.0';
const DEFAULT_RADIUS_METERS = 2000;
const MIN_COMPARABLES = 3;

interface RawComparable {
  property_id: string;
  address: string;
  district: string;
  price_vnd: bigint;
  price_per_m2: number;
  area_m2: number;
  property_type: PropertyType;
  distance_meters: number;
  published_at: Date;
}

interface PropertyLocation {
  latitude: number;
  longitude: number;
  areaM2: number;
  propertyType: PropertyType;
  yearBuilt: number | null;
  floor: number | null;
  totalFloors: number | null;
}

@Injectable()
export class PrismaAVMService implements IAVMService {
  constructor(private readonly prisma: PrismaService) {}

  async estimateValue(params: AVMParams): Promise<ValuationResult> {
    let lat: number;
    let lng: number;
    let areaM2: number;
    let propertyType: PropertyType | undefined = params.propertyType;
    let yearBuilt: number | null = params.yearBuilt ?? null;
    let floor: number | null = params.floor ?? null;
    let totalFloors: number | null = params.totalFloors ?? null;

    if (params.propertyId) {
      const loc = await this.getPropertyLocation(params.propertyId);
      lat = loc.latitude;
      lng = loc.longitude;
      areaM2 = params.areaM2 ?? loc.areaM2;
      propertyType = propertyType ?? loc.propertyType;
      yearBuilt = yearBuilt ?? loc.yearBuilt;
      floor = floor ?? loc.floor;
      totalFloors = totalFloors ?? loc.totalFloors;
    } else if (params.latitude != null && params.longitude != null && params.areaM2 != null) {
      lat = params.latitude;
      lng = params.longitude;
      areaM2 = params.areaM2;
    } else {
      throw new Error('Either propertyId or (latitude, longitude, areaM2) must be provided');
    }

    const comparables = await this.findComparables(lat, lng, propertyType, DEFAULT_RADIUS_METERS);

    if (comparables.length < MIN_COMPARABLES) {
      return {
        estimatedPrice: '0',
        confidence: 0,
        pricePerM2: 0,
        comparables: comparables.map((c) => this.toComparableDto(c)),
        modelVersion: MODEL_VERSION,
      };
    }

    const { pricePerM2, confidence } = this.calculateWeightedPrice(
      comparables,
      areaM2,
      propertyType,
      yearBuilt,
      floor,
      totalFloors,
    );

    const estimatedPrice = BigInt(Math.round(pricePerM2 * areaM2));

    return {
      estimatedPrice: estimatedPrice.toString(),
      confidence,
      pricePerM2,
      comparables: comparables.map((c) => this.toComparableDto(c)),
      modelVersion: MODEL_VERSION,
    };
  }

  async getComparables(propertyId: string, radiusMeters: number): Promise<Comparable[]> {
    const loc = await this.getPropertyLocation(propertyId);
    const raws = await this.findComparables(loc.latitude, loc.longitude, loc.propertyType, radiusMeters);
    return raws.map((c) => this.toComparableDto(c));
  }

  private async getPropertyLocation(propertyId: string): Promise<PropertyLocation> {
    const rows = await this.prisma.$queryRaw<
      Array<{
        latitude: number;
        longitude: number;
        areaM2: number;
        propertyType: PropertyType;
        yearBuilt: number | null;
        floor: number | null;
        totalFloors: number | null;
      }>
    >`
      SELECT
        ST_Y(location::geometry) AS "latitude",
        ST_X(location::geometry) AS "longitude",
        "areaM2",
        "propertyType",
        "yearBuilt",
        "floor",
        "totalFloors"
      FROM "Property"
      WHERE id = ${propertyId}
      LIMIT 1
    `;

    const row = rows[0];
    if (!row) {
      throw new Error(`Property not found: ${propertyId}`);
    }
    return row;
  }

  private async findComparables(
    lat: number,
    lng: number,
    propertyType: PropertyType | undefined,
    radiusMeters: number,
  ): Promise<RawComparable[]> {
    const typeFilter = propertyType ? `AND p."propertyType" = '${propertyType}'` : '';

    return this.prisma.$queryRawUnsafe<RawComparable[]>(
      `
      SELECT
        p.id AS property_id,
        p.address,
        p.district,
        l."priceVND" AS price_vnd,
        l."pricePerM2" AS price_per_m2,
        p."areaM2" AS area_m2,
        p."propertyType" AS property_type,
        ST_Distance(
          p.location::geography,
          ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
        ) AS distance_meters,
        l."publishedAt" AS published_at
      FROM "Property" p
      JOIN "Listing" l ON l."propertyId" = p.id
      WHERE l.status = 'ACTIVE'
        AND l."publishedAt" IS NOT NULL
        AND ST_DWithin(
          p.location::geography,
          ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
          $3
        )
        ${typeFilter}
      ORDER BY distance_meters ASC
      LIMIT 20
      `,
      lng,
      lat,
      radiusMeters,
    );
  }

  private calculateWeightedPrice(
    comparables: RawComparable[],
    _areaM2: number,
    _propertyType: PropertyType | undefined,
    _yearBuilt: number | null,
    _floor: number | null,
    _totalFloors: number | null,
  ): { pricePerM2: number; confidence: number } {
    // Distance-weighted average: closer properties have more weight
    let totalWeight = 0;
    let weightedSum = 0;

    for (const comp of comparables) {
      const distance = Math.max(comp.distance_meters, 1);
      const weight = 1 / distance;
      weightedSum += comp.price_per_m2 * weight;
      totalWeight += weight;
    }

    const pricePerM2 = totalWeight > 0 ? weightedSum / totalWeight : 0;

    // Confidence based on number of comparables and distance spread
    const maxComparables = 15;
    const countFactor = Math.min(comparables.length / maxComparables, 1);
    const avgDistance =
      comparables.reduce((sum, c) => sum + c.distance_meters, 0) / comparables.length;
    const distanceFactor = Math.max(0, 1 - avgDistance / DEFAULT_RADIUS_METERS);
    const confidence = Math.round((countFactor * 0.6 + distanceFactor * 0.4) * 100) / 100;

    return { pricePerM2: Math.round(pricePerM2), confidence };
  }

  private toComparableDto(raw: RawComparable): Comparable {
    return {
      propertyId: raw.property_id,
      address: raw.address,
      district: raw.district,
      priceVND: raw.price_vnd.toString(),
      pricePerM2: raw.price_per_m2,
      areaM2: raw.area_m2,
      propertyType: raw.property_type,
      distanceMeters: Math.round(raw.distance_meters),
      soldAt: raw.published_at.toISOString(),
    };
  }
}
