import { type Prisma } from '@prisma/client';
import { type PrismaService } from '@modules/shared';
import { type ListingDetailData, type ListingSearchItem, type ListingSellerItem, type ListingSimilarItem } from '../../domain/repositories/listing-read.dto';
import { type ListingSearchParams, type PaginatedResult } from '../../domain/repositories/listing.repository';

/** Derive a human-readable tier from a numeric quality score (0–100). */
function qualityTier(score: number): 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM' {
  if (score >= 85) return 'PLATINUM';
  if (score >= 70) return 'GOLD';
  if (score >= 50) return 'SILVER';
  return 'BRONZE';
}

export async function findByIdWithProperty(
  prisma: PrismaService,
  id: string,
): Promise<ListingDetailData | null> {
  const listing = await prisma.listing.findUnique({
    where: { id },
    include: {
      property: {
        include: {
          media: { orderBy: { order: 'asc' }, take: 10 },
        },
      },
      seller: { select: { id: true, fullName: true, phone: true } },
      agent: { select: { id: true, userId: true, agency: true, qualityScore: true } },
    },
  });

  if (!listing) return null;

  // Extract lat/lng from PostGIS geometry via raw SQL
  const geoRows = await prisma.$queryRaw<{ latitude: number; longitude: number }[]>`
    SELECT
      ST_Y("location"::geometry) AS latitude,
      ST_X("location"::geometry) AS longitude
    FROM "Property"
    WHERE "id" = ${listing.property.id}
    LIMIT 1
  `;
  // location is NOT NULL in the database — geo extraction always succeeds for existing properties
  const geo = geoRows[0]!;

  // Count ACTIVE similar listings (same propertyType + district + price ±10% + area ±20%)
  const sourcePriceNum = Number(listing.priceVND);
  const similarCount = await prisma.listing.count({
    where: {
      id: { not: id },
      status: 'ACTIVE',
      priceVND: {
        gte: BigInt(Math.floor(sourcePriceNum * 0.9)),
        lte: BigInt(Math.ceil(sourcePriceNum * 1.1)),
      },
      property: {
        propertyType: listing.property.propertyType,
        district: listing.property.district,
        areaM2: {
          gte: listing.property.areaM2 * 0.8,
          lte: listing.property.areaM2 * 1.2,
        },
      },
    },
  });

  const now = new Date();
  return {
    id: listing.id,
    status: listing.status,
    transactionType: listing.transactionType,
    priceVND: listing.priceVND.toString(),
    pricePerM2: listing.pricePerM2,
    rentPriceMonthly: listing.rentPriceMonthly?.toString() ?? null,
    commissionPct: listing.commissionPct,
    viewCount: listing.viewCount,
    saveCount: listing.saveCount,
    // inquiryCount is access-gated in the query handler; return raw count here for handler to redact
    inquiryCount: listing.inquiryCount,
    isFeatured: listing.featuredUntil != null && listing.featuredUntil > now,
    featuredUntil: listing.featuredUntil?.toISOString() ?? null,
    publishedAt: listing.publishedAt?.toISOString() ?? null,
    createdAt: listing.createdAt.toISOString(),
    // Enrichment fields — handler populates valuationEstimate; set defaults here
    valuationEstimate: null,
    agentQualityScore: listing.agent != null
      ? { score: listing.agent.qualityScore, tier: qualityTier(listing.agent.qualityScore) }
      : null,
    similarCount,
    property: {
      id: listing.property.id,
      propertyType: listing.property.propertyType,
      title: listing.property.title,
      description: listing.property.description,
      address: listing.property.address,
      ward: listing.property.ward,
      district: listing.property.district,
      city: listing.property.city,
      latitude: geo.latitude,
      longitude: geo.longitude,
      areaM2: listing.property.areaM2,
      usableAreaM2: listing.property.usableAreaM2,
      bedrooms: listing.property.bedrooms,
      bathrooms: listing.property.bathrooms,
      floors: listing.property.floors,
      floor: listing.property.floor,
      totalFloors: listing.property.totalFloors,
      direction: listing.property.direction,
      yearBuilt: listing.property.yearBuilt,
      legalStatus: listing.property.legalStatus,
      amenities: listing.property.amenities,
      nearbyPOIs: listing.property.nearbyPOIs,
      metroDistanceM: listing.property.metroDistanceM,
      projectName: listing.property.projectName,
      furnishing: listing.property.furnishing,
      propertyCondition: listing.property.propertyCondition,
      balconyDirection: listing.property.balconyDirection,
      maintenanceFeeVND: listing.property.maintenanceFeeVND?.toString() ?? null,
      parkingSlots: listing.property.parkingSlots,
      viewType: listing.property.viewType ?? [],
      petFriendly: listing.property.petFriendly,
      suitableFor: listing.property.suitableFor ?? [],
      whyThisLocation: listing.property.whyThisLocation,
      media: listing.property.media.map((m) => ({
        id: m.id,
        url: m.url,
        type: m.type,
        order: m.order,
        caption: m.caption,
      })),
    },
    seller: listing.seller,
    agent: listing.agent ? { id: listing.agent.id, userId: listing.agent.userId, agency: listing.agent.agency } : null,
  };
}

export async function searchListings(
  prisma: PrismaService,
  params: ListingSearchParams,
): Promise<PaginatedResult<ListingSearchItem>> {
  const page = params.page ?? 1;
  const limit = Math.min(params.limit ?? 20, 100);
  const skip = (page - 1) * limit;

  const where: Prisma.ListingWhereInput = {};

  if (params.status) where.status = params.status;
  if (params.transactionType) where.transactionType = params.transactionType;
  if (params.minPrice || params.maxPrice) {
    where.priceVND = {};
    if (params.minPrice) where.priceVND.gte = params.minPrice;
    if (params.maxPrice) where.priceVND.lte = params.maxPrice;
  }

  if (params.propertyType || params.city || params.district || params.minArea || params.maxArea || params.bedrooms) {
    where.property = {};
    if (params.propertyType) where.property.propertyType = params.propertyType;
    if (params.city) where.property.city = params.city;
    if (params.district) where.property.district = params.district;
    if (params.minArea || params.maxArea) {
      where.property.areaM2 = {};
      if (params.minArea) where.property.areaM2.gte = params.minArea;
      if (params.maxArea) where.property.areaM2.lte = params.maxArea;
    }
    if (params.bedrooms) where.property.bedrooms = { gte: params.bedrooms };
  }

  // newSince filter — delta pull for FE "Vừa đăng" ticker
  if (params.newSince) {
    where.publishedAt = { gt: params.newSince };
  }

  // Build orderBy based on sortBy + order params
  type OrderByClause = Prisma.ListingOrderByWithRelationInput;
  const sortBy = params.sortBy ?? 'publishedAt';
  // Default direction depends on sortBy: priceAsc/priceDesc encode their own direction;
  // publishedAt/createdAt default to desc; explicit `order` overrides where applicable.
  const order: 'asc' | 'desc' = params.order === 'asc' ? 'asc' : params.order === 'desc' ? 'desc' : 'desc';
  let sortClauses: OrderByClause[];
  switch (sortBy) {
    case 'priceAsc':
      // sortBy already pins direction; allow override only if explicitly set
      sortClauses = [{ priceVND: params.order ?? 'asc' }];
      break;
    case 'priceDesc':
      sortClauses = [{ priceVND: params.order ?? 'desc' }];
      break;
    case 'createdAt':
      sortClauses = [{ createdAt: order }];
      break;
    case 'publishedAt':
    default:
      sortClauses = [
        { featuredUntil: { sort: 'desc', nulls: 'last' } },
        { publishedAt: { sort: order, nulls: 'last' } },
      ];
      break;
  }

  const [data, total] = await Promise.all([
    prisma.listing.findMany({
      where,
      skip,
      take: limit,
      orderBy: sortClauses,
      include: {
        property: {
          include: {
            media: { orderBy: { order: 'asc' }, take: 5 },
          },
        },
        seller: { select: { id: true, fullName: true } },
      },
    }),
    prisma.listing.count({ where }),
  ]);

  // Batch-extract lat/lng for all properties in the result set
  const propertyIds = data.map((l) => l.property.id);
  const geoMap = new Map<string, { latitude: number; longitude: number }>();
  if (propertyIds.length > 0) {
    const geoRows = await prisma.$queryRaw<{ id: string; latitude: number; longitude: number }[]>`
      SELECT
        "id",
        ST_Y("location"::geometry) AS latitude,
        ST_X("location"::geometry) AS longitude
      FROM "Property"
      WHERE "id" = ANY(${propertyIds})
    `;
    for (const row of geoRows) {
      geoMap.set(row.id, { latitude: row.latitude, longitude: row.longitude });
    }
  }

  const now = new Date();
  return {
    data: data.map((listing) => {
      // location is NOT NULL — every property in the result set has geo data
      const geo = geoMap.get(listing.property.id)!;
      return {
        id: listing.id,
        status: listing.status,
        transactionType: listing.transactionType,
        priceVND: listing.priceVND.toString(),
        pricePerM2: listing.pricePerM2,
        isFeatured: listing.featuredUntil != null && listing.featuredUntil > now,
        featuredUntil: listing.featuredUntil?.toISOString() ?? null,
        viewCount: listing.viewCount,
        publishedAt: listing.publishedAt?.toISOString() ?? null,
        property: {
          id: listing.property.id,
          propertyType: listing.property.propertyType,
          title: listing.property.title,
          address: listing.property.address,
          district: listing.property.district,
          city: listing.property.city,
          latitude: geo.latitude,
          longitude: geo.longitude,
          areaM2: listing.property.areaM2,
          bedrooms: listing.property.bedrooms,
          bathrooms: listing.property.bathrooms,
          thumbnail: listing.property.media[0]?.url ?? null,
          media: listing.property.media.map((m) => ({
            id: m.id,
            url: m.url,
            type: m.type,
            order: m.order,
            caption: m.caption,
          })),
        },
        seller: listing.seller,
      };
    }),
    total,
    page,
    limit,
    totalPages: Math.ceil(total / limit),
  };
}

export async function findBySellerIdQuery(
  prisma: PrismaService,
  sellerId: string,
  page: number,
  limit: number,
): Promise<PaginatedResult<ListingSellerItem>> {
  const skip = (page - 1) * limit;
  const where: Prisma.ListingWhereInput = { sellerId };

  const [data, total] = await Promise.all([
    prisma.listing.findMany({
      where,
      skip,
      take: limit,
      orderBy: { createdAt: 'desc' },
      include: {
        property: {
          include: { media: { orderBy: { order: 'asc' }, take: 1 } },
        },
      },
    }),
    prisma.listing.count({ where }),
  ]);

  const sellerNow = new Date();
  return {
    data: data.map((listing) => ({
      id: listing.id,
      status: listing.status,
      transactionType: listing.transactionType,
      priceVND: listing.priceVND.toString(),
      isFeatured: listing.featuredUntil != null && listing.featuredUntil > sellerNow,
      featuredUntil: listing.featuredUntil?.toISOString() ?? null,
      property: {
        id: listing.property.id,
        title: listing.property.title,
        district: listing.property.district,
        city: listing.property.city,
        areaM2: listing.property.areaM2,
        thumbnail: listing.property.media[0]?.url ?? null,
        media: listing.property.media.map((m) => ({
          id: m.id,
          url: m.url,
          type: m.type,
          order: m.order,
          caption: m.caption,
        })),
      },
    })),
    total,
    page,
    limit,
    totalPages: Math.ceil(total / limit),
  };
}

/**
 * Find similar listings for the "comparables" widget on listing detail page.
 *
 * Match criteria:
 *  - Same propertyType
 *  - Same district
 *  - Price within ±10% of the source listing's price
 *  - Area within ±20% of the source listing's area
 *  - Status = ACTIVE
 *  - Exclude the source listing itself
 *
 * Results are sorted by price delta (ascending) — closest comparable first.
 */
export async function findSimilarListingsQuery(
  prisma: PrismaService,
  id: string,
  limit: number,
): Promise<ListingSimilarItem[]> {
  const source = await prisma.listing.findUnique({
    where: { id },
    select: {
      priceVND: true,
      property: {
        select: {
          propertyType: true,
          district: true,
          areaM2: true,
        },
      },
    },
  });

  if (!source) return [];

  const sourcePriceNum = Number(source.priceVND);
  const minPrice = BigInt(Math.floor(sourcePriceNum * 0.9));
  const maxPrice = BigInt(Math.ceil(sourcePriceNum * 1.1));
  const minArea = source.property.areaM2 * 0.8;
  const maxArea = source.property.areaM2 * 1.2;

  const candidates = await prisma.listing.findMany({
    where: {
      id: { not: id },
      status: 'ACTIVE',
      priceVND: { gte: minPrice, lte: maxPrice },
      property: {
        propertyType: source.property.propertyType,
        district: source.property.district,
        areaM2: { gte: minArea, lte: maxArea },
      },
    },
    orderBy: { priceVND: 'asc' },
    take: limit * 3,
    include: {
      property: {
        include: { media: { orderBy: { order: 'asc' }, take: 1 } },
      },
    },
  });

  return candidates
    .map((l) => ({ listing: l, delta: Math.abs(Number(l.priceVND) - sourcePriceNum) }))
    .sort((a, b) => a.delta - b.delta)
    .slice(0, limit)
    .map(({ listing }) => ({
      id: listing.id,
      title: listing.property.title,
      priceVND: listing.priceVND.toString(),
      areaM2: listing.property.areaM2,
      district: listing.property.district,
      thumbnailUrl: listing.property.media[0]?.url ?? null,
      publishedAt: listing.publishedAt?.toISOString() ?? null,
    }));
}
