import { load } from "cheerio"

import { rawListingSchema, type RawListing, type SourceScraper } from "../core/contracts"

const SALAZAR_ISRAEL_BASE_URL = "https://www.salazarisrael.cl"
const SALAZAR_ISRAEL_LIST_URL = `${SALAZAR_ISRAEL_BASE_URL}/vehiculos/usado`

const defaultHeaders = {
  "user-agent":
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
}

const normalizeText = (value?: string) => value?.replace(/\s+/g, " ").trim() ?? ""

const parseNumber = (value?: string | number | null) => {
  if (typeof value === "number") {
    return Number.isFinite(value) ? Math.trunc(value) : undefined
  }

  const digits = String(value ?? "").replace(/[^\d]/g, "")
  return digits ? Number(digits) : undefined
}

const toAbsoluteUrl = (value?: string) => {
  if (!value) {
    return undefined
  }

  return new URL(value, SALAZAR_ISRAEL_BASE_URL).toString()
}

const unique = <T>(values: T[]) => [...new Set(values)]

const mapWithConcurrency = async <T, R>(
  values: T[],
  concurrency: number,
  mapper: (value: T, index: number) => Promise<R>
) => {
  const results = new Array<R>(values.length)
  let nextIndex = 0

  const worker = async () => {
    while (nextIndex < values.length) {
      const currentIndex = nextIndex
      nextIndex += 1
      results[currentIndex] = await mapper(values[currentIndex], currentIndex)
    }
  }

  await Promise.all(
    Array.from({ length: Math.max(1, Math.min(concurrency, values.length || 1)) }, async () => {
      await worker()
    })
  )

  return results
}

const extractNextImageUrl = (value?: string) => {
  if (!value) {
    return undefined
  }

  const url = new URL(value, SALAZAR_ISRAEL_BASE_URL)
  const nestedUrl = url.searchParams.get("url")

  if (nestedUrl) {
    return nestedUrl
  }

  return url.toString()
}

const getExternalId = (url: string) => url.match(/-(\d+)$/)?.[1] ?? new URL(url).pathname

const parseSpecs = (value: string) => {
  const [yearText, mileageText, transmissionText, fuelText] = value.split("|").map((part) => normalizeText(part))
  const year = parseNumber(yearText)

  if (!year) {
    throw new Error(`No pude parsear el año desde "${value}"`)
  }

  return {
    year,
    mileageKm: parseNumber(mileageText),
    transmissionText,
    fuelText
  }
}

export interface SalazarIsraelScrapeOptions {
  maxPages?: number
  pageConcurrency?: number
  onProgress?: (message: string) => void
}

interface SalazarIsraelPageResult {
  totalAvailable?: number
  totalPages?: number
  items: RawListing[]
}

export class SalazarIsraelScraper implements SourceScraper {
  source = "SALAZAR_ISRAEL" as const

  async scrape(): Promise<RawListing[]> {
    return this.scrapeAll()
  }

  async scrapeAll(options: SalazarIsraelScrapeOptions = {}): Promise<RawListing[]> {
    const pageConcurrency = options.pageConcurrency ?? 4
    const firstPage = await this.fetchPage(1)
    const totalPages = Math.max(1, Math.min(options.maxPages ?? firstPage.totalPages ?? 1, firstPage.totalPages ?? 1))
    const pageNumbers = Array.from({ length: Math.max(totalPages - 1, 0) }, (_, index) => index + 2)

    options.onProgress?.(
      `Pagina 1/${totalPages}: ${firstPage.items.length} avisos, total reportado ${firstPage.totalAvailable ?? "?"}`
    )

    const remainingPages = await mapWithConcurrency(pageNumbers, pageConcurrency, async (page) => {
      const result = await this.fetchPage(page)
      options.onProgress?.(`Pagina ${page}/${totalPages}: ${result.items.length} avisos`)
      return result
    })

    const seen = new Set<string>()
    const items: RawListing[] = []

    for (const item of [firstPage, ...remainingPages].flatMap((page) => page.items)) {
      if (seen.has(item.externalId)) {
        continue
      }

      seen.add(item.externalId)
      items.push(item)
    }

    return items
  }

  private async fetchPage(page: number): Promise<SalazarIsraelPageResult> {
    const url = new URL(SALAZAR_ISRAEL_LIST_URL)

    if (page > 1) {
      url.searchParams.set("page", String(page))
    }

    const response = await fetch(url, {
      headers: defaultHeaders
    })

    if (!response.ok) {
      throw new Error(`Listado Salazar Israel respondió ${response.status} ${response.statusText} en page=${page}`)
    }

    const html = await response.text()
    const $ = load(html)
    const items = this.parseListingCards($)
    const totalAvailable = parseNumber($.root().text().match(/([\d.]+)\s+veh[ií]culos/i)?.[1])
    const totalPages = Math.max(
      1,
      ...$("nav[aria-label='pagination'] a[href*='/vehiculos/usado']")
        .map((_, element) => parseNumber($(element).text()))
        .get()
        .filter((value): value is number => Boolean(value))
    )

    return {
      totalAvailable,
      totalPages,
      items
    }
  }

  private parseListingCards($: ReturnType<typeof load>) {
    const cards = $("article").filter((_, element) =>
      Boolean($(element).find("a[href^='/marcas/'][href*='/usado/']").first().attr("href"))
    )

    return unique(
      cards
        .map((_, element) => {
          const card = $(element)
          const detailPath = card.find("a[href^='/marcas/'][href*='/usado/']").first().attr("href")
          const sourceUrl = toAbsoluteUrl(detailPath)
          const brand = normalizeText(card.find("span[class*='text-xl'][class*='font-bold']").first().text())
          const model = normalizeText(card.find("span[class*='text-xl'][class*='font-medium']").first().text())
          const version = normalizeText(
            card.find("span[class*='text-base'][class*='font-normal'][class*='text-stone-700']").first().text()
          )
          const specsText = normalizeText(
            card
              .find("span")
              .map((__, span) => normalizeText($(span).text()))
              .get()
              .find((text) => /^\d{4}\s*\|/i.test(text))
          )
          const priceClp = parseNumber(card.find("span[class*='text-[2rem]']").first().text())
          const imageUrl = extractNextImageUrl(card.find("img").first().attr("src"))

          if (!sourceUrl || !brand || !model || !version || !specsText || !priceClp) {
            throw new Error("No pude extraer uno de los campos base del listado Salazar Israel")
          }

          const { year, mileageKm, transmissionText, fuelText } = parseSpecs(specsText)
          const title = [brand, model, version].filter(Boolean).join(" ")

          return rawListingSchema.parse({
            source: this.source,
            externalId: getExternalId(sourceUrl),
            url: sourceUrl,
            title,
            priceClp,
            year,
            mileageKm,
            imageUrl,
            sellerName: "Salazar Israel",
            transmissionText,
            fuelText,
            rawPayload: {
              listUrl: SALAZAR_ISRAEL_LIST_URL,
              detailPath,
              specsText,
              brand,
              model,
              version
            }
          })
        })
        .get()
    )
  }
}
