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

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

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> {
    const resolved = await this.resolveParams(params);
    const comparables = await this.findComparables(
      resolved.lat, resolved.lng, resolved.propertyType, DEFAULT_RADIUS_METERS,
    );

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

    const { pricePerM2, confidence } = calculateWeightedPrice(
      comparables, resolved.areaM2, resolved.propertyType,
      resolved.yearBuilt, resolved.floor, resolved.totalFloors,
    );
    const estimatedPrice = BigInt(Math.round(pricePerM2 * resolved.areaM2));

    return {
      estimatedPrice: estimatedPrice.toString(),
      confidence,
      pricePerM2,
      comparables: comparables.map(toComparableDto),
      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(toComparableDto);
  }

  async estimateBatch(items: BatchValuationItem[]): Promise<BatchValuationResult[]> {
    return Promise.all(
      items.map(async (item) => {
        try {
          const valuation = await this.estimateValue({ propertyId: item.propertyId });
          return { propertyId: item.propertyId, valuation };
        } catch {
          return { propertyId: item.propertyId, valuation: null, error: 'Lỗi định giá' };
        }
      }),
    );
  }

  private async resolveParams(params: AVMParams): Promise<{
    lat: number; lng: number; areaM2: number;
    propertyType: PropertyType | undefined;
    yearBuilt: number | null; floor: number | null; totalFloors: number | null;
  }> {
    if (params.propertyId) {
      const loc = await this.getPropertyLocation(params.propertyId);
      return {
        lat: loc.latitude,
        lng: loc.longitude,
        areaM2: params.areaM2 ?? loc.areaM2,
        propertyType: params.propertyType ?? loc.propertyType,
        yearBuilt: params.yearBuilt ?? loc.yearBuilt,
        floor: params.floor ?? loc.floor,
        totalFloors: params.totalFloors ?? loc.totalFloors,
      };
    }

    if (params.latitude != null && params.longitude != null && params.areaM2 != null) {
      return {
        lat: params.latitude,
        lng: params.longitude,
        areaM2: params.areaM2,
        propertyType: params.propertyType,
        yearBuilt: params.yearBuilt ?? null,
        floor: params.floor ?? null,
        totalFloors: params.totalFloors ?? null,
      };
    }

    throw new Error('Either propertyId or (latitude, longitude, areaM2) must be provided');
  }

  private async getPropertyLocation(propertyId: string): Promise<PropertyLocation> {
    const rows = await this.prisma.$queryRaw<PropertyLocation[]>`
      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,
    );
  }
}
