import { Prisma, type SourceName } from "@prisma/client"

import type { ComparisonDetail, ComparisonResponse, FeaturedResponse, ListingCard } from "~/types/listing"
import { prisma } from "~/server/utils/prisma"

const listingSources: SourceName[] = [
  "MARITANO",
  "BRUNO_FRITSCH",
  "SALFA",
  "SALAZAR_ISRAEL",
  "PORTILLO_SUR",
  "SERGIO_ESCOBAR"
]

const normalize = (value?: string | null) =>
  value
    ?.normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .toLowerCase()
    .trim() ?? ""

const sourceLabel: Record<SourceName, ListingCard["source"]> = {
  BRUNO_FRITSCH: "Bruno Fritsch",
  CLICAR: "Clicar",
  CHILEAUTOS: "Chileautos",
  YAPO: "Yapo",
  MARITANO: "Maritano",
  SALFA: "Salfa",
  SALAZAR_ISRAEL: "Salazar Israel",
  PORTILLO_SUR: "Portillo Sur",
  SERGIO_ESCOBAR: "Sergio Escobar"
}

const transmissionFallback: Record<string, string> = {
  AUTOMATIC: "Automatica",
  MANUAL: "Manual",
  CVT: "CVT",
  DCT: "DCT",
  UNKNOWN: "Por validar"
}

const fuelFallback: Record<string, string> = {
  GASOLINE: "Gasolina",
  DIESEL: "Diesel",
  HYBRID: "Hibrido",
  ELECTRIC: "Electrico",
  LPG: "GLP",
  UNKNOWN: "Por validar"
}

interface StoredNormalizedSummary {
  make: string
  model: string
  version?: string
  transmission: string
  transmissionLabel: string
  fuel: string
  fuelLabel: string
  normalizedLabel: string
  comparisonKey: string
}

const listingSelect = {
  id: true,
  source: true,
  externalId: true,
  sourceUrl: true,
  title: true,
  priceClp: true,
  modelYear: true,
  mileageKm: true,
  region: true,
  commune: true,
  sellerText: true,
  normalizedSummary: true,
  images: {
    orderBy: {
      position: "asc" as const
    },
    take: 1,
    select: {
      imageUrl: true
    }
  },
  priceSnapshots: {
    orderBy: {
      observedAt: "desc" as const
    },
    take: 2,
    select: {
      priceClp: true
    }
  }
} satisfies Prisma.SourceListingSelect

type StoredListingRecord = Prisma.SourceListingGetPayload<{
  select: typeof listingSelect
}>

const parseStoredSummary = (value: Prisma.JsonValue | null): StoredNormalizedSummary | undefined => {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return undefined
  }

  const raw = value as Record<string, unknown>
  const make = typeof raw.make === "string" ? raw.make : ""
  const model = typeof raw.model === "string" ? raw.model : ""

  if (!make || !model) {
    return undefined
  }

  return {
    make,
    model,
    version: typeof raw.version === "string" ? raw.version : undefined,
    transmission: typeof raw.transmission === "string" ? raw.transmission : "UNKNOWN",
    transmissionLabel:
      typeof raw.transmissionLabel === "string" ? raw.transmissionLabel : transmissionFallback.UNKNOWN,
    fuel: typeof raw.fuel === "string" ? raw.fuel : "UNKNOWN",
    fuelLabel: typeof raw.fuelLabel === "string" ? raw.fuelLabel : fuelFallback.UNKNOWN,
    normalizedLabel: typeof raw.normalizedLabel === "string" ? raw.normalizedLabel : `${make} ${model}`,
    comparisonKey: typeof raw.comparisonKey === "string" ? raw.comparisonKey : `${make}|${model}`
  }
}

const getPriceDelta = (priceSnapshots: StoredListingRecord["priceSnapshots"]) => {
  if (priceSnapshots.length < 2) {
    return undefined
  }

  return priceSnapshots[0].priceClp - priceSnapshots[1].priceClp
}

const fallbackMakeModel = (title: string) => {
  const [make = "Por validar", model = "Por validar"] = title.split(/\s+/)

  return {
    make,
    model
  }
}

const mapDbListingToCard = (item: StoredListingRecord): ListingCard => {
  const summary = parseStoredSummary(item.normalizedSummary)
  const fallback = fallbackMakeModel(item.title)

  return {
    id: item.id,
    source: sourceLabel[item.source],
    externalId: item.externalId,
    sourceUrl: item.sourceUrl,
    title: item.title,
    normalizedLabel: summary?.normalizedLabel ?? item.title,
    comparisonKey: summary?.comparisonKey ?? `${fallback.make}|${fallback.model}`,
    make: summary?.make ?? fallback.make,
    model: summary?.model ?? fallback.model,
    version: summary?.version,
    year: item.modelYear,
    priceClp: item.priceClp,
    mileageKm: item.mileageKm ?? undefined,
    transmission: summary?.transmissionLabel ?? transmissionFallback.UNKNOWN,
    fuel: summary?.fuelLabel ?? fuelFallback.UNKNOWN,
    region: item.region ?? "Por validar",
    commune: item.commune ?? undefined,
    seller: item.sellerText ?? sourceLabel[item.source],
    sellerType: "automotora",
    comparisonBucket: "destacada",
    priceDeltaLast30d: getPriceDelta(item.priceSnapshots),
    imageUrl: item.images[0]?.imageUrl
  }
}

const median = (values: number[]) => {
  if (!values.length) {
    return 0
  }

  const sorted = [...values].sort((left, right) => left - right)
  const middle = Math.floor(sorted.length / 2)

  return sorted.length % 2 === 0
    ? Math.round((sorted[middle - 1] + sorted[middle]) / 2)
    : sorted[middle]
}

const sortStrings = (values: Iterable<string>) =>
  [...new Set(values)]
    .filter(Boolean)
    .sort((left, right) => left.localeCompare(right, "es"))

export interface DbSearchFilters {
  q?: string
  source?: ListingCard["source"]
  make?: string
  transmission?: string
  fuel?: string
  minYear?: number
  maxYear?: number
  maxPrice?: number
  page?: number
  pageSize?: number
}

const matchesKeyword = (item: ListingCard, keyword?: string) => {
  if (!keyword) {
    return true
  }

  const haystack = [
    item.title,
    item.make,
    item.model,
    item.version,
    item.seller,
    item.source,
    item.normalizedLabel
  ]
    .map(normalize)
    .join(" ")

  return haystack.includes(normalize(keyword))
}

const matchesExactValue = (value: string | undefined, expected: string | undefined) => {
  if (!expected) {
    return true
  }

  return normalize(value) === normalize(expected)
}

export async function loadSearchListings(filters: DbSearchFilters = {}) {
  const safePage = Math.max(1, Math.trunc(filters.page || 1))
  const safePageSize = Math.max(1, Math.min(50, Math.trunc(filters.pageSize || 12)))

  const records = await prisma.sourceListing.findMany({
    where: {
      source: {
        in: listingSources
      },
      isAvailable: true
    },
    select: listingSelect,
    orderBy: [{ priceClp: "asc" }, { modelYear: "desc" }, { externalId: "asc" }]
  })

  const allItems = records.map(mapDbListingToCard)
  const filteredItems = allItems
    .filter((item) => matchesKeyword(item, filters.q))
    .filter((item) => matchesExactValue(item.source, filters.source))
    .filter((item) => matchesExactValue(item.make, filters.make))
    .filter((item) => matchesExactValue(item.transmission, filters.transmission))
    .filter((item) => matchesExactValue(item.fuel, filters.fuel))
    .filter((item) => (filters.minYear ? item.year >= filters.minYear : true))
    .filter((item) => (filters.maxYear ? item.year <= filters.maxYear : true))
    .filter((item) => (filters.maxPrice ? item.priceClp <= filters.maxPrice : true))

  const total = filteredItems.length
  const totalPages = Math.max(1, Math.ceil(total / safePageSize))
  const currentPage = Math.min(safePage, totalPages)
  const pagedItems = filteredItems.slice((currentPage - 1) * safePageSize, currentPage * safePageSize)

  return {
    total,
    page: currentPage,
    pageSize: safePageSize,
    totalPages,
    medianPrice: median(filteredItems.map((item) => item.priceClp)),
    cheapest: filteredItems[0]?.priceClp ?? 0,
    items: pagedItems,
    facets: {
      sources: sortStrings(allItems.map((item) => item.source)) as ListingCard["source"][],
      makes: sortStrings(allItems.map((item) => item.make)),
      transmissions: sortStrings(allItems.map((item) => item.transmission)),
      fuels: sortStrings(allItems.map((item) => item.fuel)),
      years: [...new Set(allItems.map((item) => item.year))].sort((left, right) => right - left),
      maxPrice: Math.max(...allItems.map((item) => item.priceClp), 0)
    }
  }
}

const buildComparisonDetail = (item: ListingCard, targetPrice: number, isTarget: boolean): ComparisonDetail => ({
  ...item,
  isTarget,
  priceGapVsTarget: item.priceClp - targetPrice
})

const getComparisonScore = (candidate: ListingCard, target: ListingCard) => {
  const transmissionPenalty = candidate.transmission === target.transmission ? 0 : 1_000_000
  const fuelPenalty = candidate.fuel === target.fuel ? 0 : 500_000

  return Math.abs(candidate.year - target.year) * 10_000_000 + transmissionPenalty + fuelPenalty + Math.abs(candidate.priceClp - target.priceClp)
}

const pickBestPerSource = (items: ListingCard[], target: ListingCard) => {
  const grouped = new Map<ListingCard["source"], ListingCard[]>()

  for (const item of items) {
    const current = grouped.get(item.source) ?? []
    current.push(item)
    grouped.set(item.source, current)
  }

  const selectedIds = new Set<string>()
  const primaryMatches = [...grouped.entries()].map(([source, candidates]) => {
    if (source === target.source) {
      selectedIds.add(target.id)
      return target
    }

    const winner = [...candidates].sort((left, right) => getComparisonScore(left, target) - getComparisonScore(right, target))[0]
    selectedIds.add(winner.id)
    return winner
  })

  const extraMatches = items.filter((item) => !selectedIds.has(item.id))

  return {
    primaryMatches: primaryMatches.sort((left, right) => {
      if (left.id === target.id) return -1
      if (right.id === target.id) return 1
      return left.priceClp - right.priceClp
    }),
    extraMatches
  }
}

export async function loadFeaturedListings(page = 1, pageSize = 12): Promise<FeaturedResponse> {
  const safePage = Math.max(1, Math.trunc(page) || 1)
  const safePageSize = Math.max(1, Math.min(50, Math.trunc(pageSize) || 12))
  const total = await prisma.sourceListing.count({
    where: {
      source: {
        in: listingSources
      },
      isAvailable: true
    }
  })
  const totalPages = Math.max(1, Math.ceil(total / safePageSize))
  const currentPage = Math.min(safePage, totalPages)
  const items = await prisma.sourceListing.findMany({
    where: {
      source: {
        in: listingSources
      },
      isAvailable: true
    },
    select: listingSelect,
    orderBy: [{ priceClp: "asc" }, { modelYear: "desc" }, { externalId: "asc" }],
    skip: (currentPage - 1) * safePageSize,
    take: safePageSize
  })

  return {
    total,
    page: currentPage,
    pageSize: safePageSize,
    totalPages,
    items: items.map(mapDbListingToCard)
  }
}

export async function loadListingStats() {
  const [sources, aggregate] = await Promise.all([
    prisma.sourceListing.groupBy({
      by: ["source"],
      where: {
        source: {
          in: listingSources
        },
        isAvailable: true
      }
    }),
    prisma.sourceListing.aggregate({
      where: {
        source: {
          in: listingSources
        },
        isAvailable: true
      },
      _count: {
        _all: true
      },
      _avg: {
        priceClp: true
      },
      _min: {
        priceClp: true
      }
    })
  ])

  return {
    sources: sources.length,
    listings: aggregate._count._all,
    averagePrice: Math.round(aggregate._avg.priceClp ?? 0),
    cheapest: aggregate._min.priceClp ?? 0
  }
}

export async function loadComparisonByListingId(listingId: string): Promise<ComparisonResponse | null> {
  const targetRecord = await prisma.sourceListing.findFirst({
    where: {
      id: listingId,
      source: {
        in: listingSources
      },
      isAvailable: true
    },
    select: listingSelect
  })

  if (!targetRecord) {
    return null
  }

  const target = mapDbListingToCard(targetRecord)

  const modelRecords = await prisma.sourceListing.findMany({
    where: {
      source: {
        in: listingSources
      },
      isAvailable: true,
      AND: [
        {
          normalizedSummary: {
            path: ["make"],
            equals: target.make
          }
        },
        {
          normalizedSummary: {
            path: ["model"],
            equals: target.model
          }
        }
      ]
    },
    select: listingSelect,
    orderBy: [{ priceClp: "asc" }, { modelYear: "desc" }, { externalId: "asc" }]
  })

  const modelMatches = modelRecords.map(mapDbListingToCard)
  const { primaryMatches, extraMatches } = pickBestPerSource(modelMatches, target)
  const allMatches = [...primaryMatches, ...extraMatches]
  const prices = allMatches.map((item) => item.priceClp)
  const cheapestPrice = prices.length ? Math.min(...prices) : target.priceClp
  const highestPrice = prices.length ? Math.max(...prices) : target.priceClp

  return {
    target,
    exactMatches: primaryMatches.map((item) => buildComparisonDetail(item, target.priceClp, item.id === target.id)),
    relatedMatches: extraMatches.map((item) => buildComparisonDetail(item, target.priceClp, false)).slice(0, 12),
    exactCount: primaryMatches.length,
    relatedCount: extraMatches.length,
    cheapestPrice,
    highestPrice,
    spreadPrice: highestPrice - cheapestPrice
  }
}
