import { Injectable } from '@nestjs/common';
import { type Prisma, type PropertyMedia as PrismaMedia, type PropertyType, type Direction, type Furnishing, type PropertyCondition } from '@prisma/client';
import { PrismaService } from '@modules/shared';
import { PropertyMediaEntity, type PropertyMediaProps } from '../../domain/entities/property-media.entity';
import { PropertyEntity, type PropertyProps } from '../../domain/entities/property.entity';
import { type IPropertyRepository } from '../../domain/repositories/property.repository';
import { Address } from '../../domain/value-objects/address.vo';
import { GeoPoint } from '../../domain/value-objects/geo-point.vo';

/** Shape returned by findById raw SQL query with ST_X/ST_Y extracted coordinates */
interface PropertyWithGeo {
  id: string;
  propertyType: PropertyType;
  title: string;
  description: string;
  address: string;
  ward: string;
  district: string;
  city: string;
  latitude: number;
  longitude: number;
  areaM2: number;
  usableAreaM2: number | null;
  bedrooms: number | null;
  bathrooms: number | null;
  floors: number | null;
  floor: number | null;
  totalFloors: number | null;
  direction: Direction | null;
  yearBuilt: number | null;
  legalStatus: string | null;
  amenities: unknown;
  nearbyPOIs: unknown;
  metroDistanceM: number | null;
  projectName: string | null;
  furnishing: Furnishing | null;
  propertyCondition: PropertyCondition | null;
  balconyDirection: Direction | null;
  maintenanceFeeVND: bigint | null;
  parkingSlots: number | null;
  viewType: string[] | null;
  petFriendly: boolean | null;
  suitableFor: string[] | null;
  whyThisLocation: string | null;
  createdAt: Date;
  updatedAt: Date;
}

@Injectable()
export class PrismaPropertyRepository implements IPropertyRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): Promise<PropertyEntity | null> {
    // Use raw SQL to extract lat/lng from PostGIS geometry(Point, 4326)
    const rows = await this.prisma.$queryRaw<PropertyWithGeo[]>`
      SELECT
        "id", "propertyType", "title", "description",
        "address", "ward", "district", "city",
        ST_Y("location"::geometry) AS latitude,
        ST_X("location"::geometry) AS longitude,
        "areaM2", "usableAreaM2", "bedrooms", "bathrooms",
        "floors", "floor", "totalFloors", "direction",
        "yearBuilt", "legalStatus", "amenities", "nearbyPOIs",
        "metroDistanceM", "projectName",
        "furnishing", "propertyCondition", "balconyDirection",
        "maintenanceFeeVND", "parkingSlots", "viewType",
        "petFriendly", "suitableFor", "whyThisLocation",
        "createdAt", "updatedAt"
      FROM "Property"
      WHERE "id" = ${id}
      LIMIT 1
    `;
    const row = rows[0];
    return row ? this.toDomainWithGeo(row) : null;
  }

  async save(entity: PropertyEntity): Promise<void> {
    await this.prisma.$executeRaw`
      INSERT INTO "Property" (
        "id", "propertyType", "title", "description", "address", "ward", "district", "city",
        "addressNormalized",
        "location", "areaM2", "usableAreaM2", "bedrooms", "bathrooms", "floors", "floor",
        "totalFloors", "direction", "yearBuilt", "legalStatus", "amenities", "nearbyPOIs",
        "metroDistanceM", "projectName",
        "furnishing", "propertyCondition", "balconyDirection",
        "maintenanceFeeVND", "parkingSlots", "viewType",
        "petFriendly", "suitableFor", "whyThisLocation",
        "createdAt", "updatedAt"
      ) VALUES (
        ${entity.id}, ${entity.propertyType}::"PropertyType", ${entity.title}, ${entity.description},
        ${entity.address.address}, ${entity.address.ward}, ${entity.address.district}, ${entity.address.city},
        ${entity.address.normalized},
        ST_SetSRID(ST_MakePoint(${entity.location.longitude}, ${entity.location.latitude}), 4326),
        ${entity.areaM2}, ${entity.usableAreaM2}, ${entity.bedrooms}, ${entity.bathrooms},
        ${entity.floors}, ${entity.floor}, ${entity.totalFloors},
        ${entity.direction}::"Direction", ${entity.yearBuilt}, ${entity.legalStatus},
        ${entity.amenities ? JSON.stringify(entity.amenities) : null}::jsonb,
        ${entity.nearbyPOIs ? JSON.stringify(entity.nearbyPOIs) : null}::jsonb,
        ${entity.metroDistanceM}, ${entity.projectName},
        ${entity.furnishing}::"Furnishing",
        ${entity.propertyCondition}::"PropertyCondition",
        ${entity.balconyDirection}::"Direction",
        ${entity.maintenanceFeeVND},
        ${entity.parkingSlots},
        ${entity.viewType}::text[],
        ${entity.petFriendly},
        ${entity.suitableFor}::text[],
        ${entity.whyThisLocation},
        ${entity.createdAt}, ${entity.updatedAt}
      )`;
  }

  async update(entity: PropertyEntity): Promise<void> {
    await this.prisma.$executeRaw`
      UPDATE "Property" SET
        "propertyType" = ${entity.propertyType}::"PropertyType",
        "title" = ${entity.title},
        "description" = ${entity.description},
        "address" = ${entity.address.address},
        "ward" = ${entity.address.ward},
        "district" = ${entity.address.district},
        "city" = ${entity.address.city},
        "addressNormalized" = ${entity.address.normalized},
        "location" = ST_SetSRID(ST_MakePoint(${entity.location.longitude}, ${entity.location.latitude}), 4326),
        "areaM2" = ${entity.areaM2},
        "usableAreaM2" = ${entity.usableAreaM2},
        "bedrooms" = ${entity.bedrooms},
        "bathrooms" = ${entity.bathrooms},
        "floors" = ${entity.floors},
        "floor" = ${entity.floor},
        "totalFloors" = ${entity.totalFloors},
        "direction" = ${entity.direction}::"Direction",
        "yearBuilt" = ${entity.yearBuilt},
        "legalStatus" = ${entity.legalStatus},
        "amenities" = ${entity.amenities ? JSON.stringify(entity.amenities) : null}::jsonb,
        "nearbyPOIs" = ${entity.nearbyPOIs ? JSON.stringify(entity.nearbyPOIs) : null}::jsonb,
        "metroDistanceM" = ${entity.metroDistanceM},
        "projectName" = ${entity.projectName},
        "furnishing" = ${entity.furnishing}::"Furnishing",
        "propertyCondition" = ${entity.propertyCondition}::"PropertyCondition",
        "balconyDirection" = ${entity.balconyDirection}::"Direction",
        "maintenanceFeeVND" = ${entity.maintenanceFeeVND},
        "parkingSlots" = ${entity.parkingSlots},
        "viewType" = ${entity.viewType}::text[],
        "petFriendly" = ${entity.petFriendly},
        "suitableFor" = ${entity.suitableFor}::text[],
        "whyThisLocation" = ${entity.whyThisLocation},
        "updatedAt" = NOW()
      WHERE "id" = ${entity.id}`;
  }

  async addMedia(media: PropertyMediaEntity): Promise<void> {
    await this.prisma.propertyMedia.create({
      data: {
        id: media.id,
        propertyId: media.propertyId,
        url: media.url,
        type: media.type,
        order: media.order,
        caption: media.caption,
        aiTags: media.aiTags as Prisma.InputJsonValue,
      },
    });
  }

  async findMediaByPropertyId(propertyId: string): Promise<PropertyMediaEntity[]> {
    const mediaList = await this.prisma.propertyMedia.findMany({
      where: { propertyId },
      orderBy: { order: 'asc' },
      take: 50,
    });
    return mediaList.map((m) => this.toMediaDomain(m));
  }

  async deleteMedia(mediaId: string): Promise<void> {
    await this.prisma.propertyMedia.delete({ where: { id: mediaId } });
  }

  async countMediaByPropertyId(propertyId: string): Promise<number> {
    return this.prisma.propertyMedia.count({ where: { propertyId } });
  }

  async updateMediaOrder(propertyId: string, mediaOrder: { mediaId: string; order: number }[]): Promise<void> {
    await this.prisma.$transaction(
      mediaOrder.map((item) =>
        this.prisma.propertyMedia.updateMany({
          where: { id: item.mediaId, propertyId },
          data: { order: item.order },
        }),
      ),
    );
  }

  private toDomainWithGeo(raw: PropertyWithGeo): PropertyEntity {
    const geoPoint = GeoPoint.create(raw.latitude, raw.longitude).unwrap();
    const address = Address.create(raw.address, raw.ward, raw.district, raw.city).unwrap();

    const props: PropertyProps = {
      propertyType: raw.propertyType,
      title: raw.title,
      description: raw.description,
      address,
      location: geoPoint,
      areaM2: raw.areaM2,
      usableAreaM2: raw.usableAreaM2,
      bedrooms: raw.bedrooms,
      bathrooms: raw.bathrooms,
      floors: raw.floors,
      floor: raw.floor,
      totalFloors: raw.totalFloors,
      direction: raw.direction,
      yearBuilt: raw.yearBuilt,
      legalStatus: raw.legalStatus,
      amenities: raw.amenities,
      nearbyPOIs: raw.nearbyPOIs,
      metroDistanceM: raw.metroDistanceM,
      projectName: raw.projectName,
      furnishing: raw.furnishing,
      propertyCondition: raw.propertyCondition,
      balconyDirection: raw.balconyDirection,
      maintenanceFeeVND: raw.maintenanceFeeVND,
      parkingSlots: raw.parkingSlots,
      viewType: raw.viewType ?? [],
      petFriendly: raw.petFriendly,
      suitableFor: raw.suitableFor ?? [],
      whyThisLocation: raw.whyThisLocation,
    };

    return new PropertyEntity(raw.id, props, raw.createdAt, raw.updatedAt);
  }

  private toMediaDomain(raw: PrismaMedia): PropertyMediaEntity {
    const props: PropertyMediaProps = {
      propertyId: raw.propertyId,
      url: raw.url,
      type: raw.type as 'image' | 'video',
      order: raw.order,
      caption: raw.caption,
      aiTags: raw.aiTags,
    };
    return new PropertyMediaEntity(raw.id, props, raw.createdAt);
  }
}
