import { load } from "cheerio"

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

const SERGIO_ESCOBAR_BASE_URL = "https://sergioescobarusados.cl"
const SERGIO_ESCOBAR_USED_CARS_URL =
  process.env.SERGIO_ESCOBAR_USED_CARS_URL ??
  "https://sergioescobarusados.cl/web/autos-usados?VehiculoEsSearch%5Bsegmento_filter%5D=&VehiculoEsSearch%5Bmarca_filter%5D=&VehiculoEsSearch%5Bmodelo_filter%5D=&VehiculoEsSearch%5Byear_range%5D=2014%2C2026&VehiculoEsSearch%5Bkms_range%5D=56%2C940000&VehiculoEsSearch%5Bprecio_range%5D=5000000%2C39000000&search-button="
const SERGIO_ESCOBAR_REQUEST_TIMEOUT_MS = 45_000
const SERGIO_ESCOBAR_RETRIES = 3
const SERGIO_ESCOBAR_MAX_PAGES = Number.parseInt(process.env.SERGIO_ESCOBAR_MAX_PAGES ?? "160", 10)
const SERGIO_ESCOBAR_PAGE_DELAY_MS = Number.parseInt(process.env.SERGIO_ESCOBAR_PAGE_DELAY_MS ?? "350", 10)

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"
}

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

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

const parsePositiveInt = (value: string | undefined, fallback: number) => {
  const parsed = Number.parseInt(value ?? "", 10)
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
}

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

const normalizeSlug = (value?: string) =>
  normalizeText(value)
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .toUpperCase()

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, SERGIO_ESCOBAR_BASE_URL).toString()
}

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

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

const inferRegion = (commune?: string) => {
  const normalized = normalizeSlug(commune)

  if (!normalized) {
    return undefined
  }

  if (
    normalized.includes("CONCEPCION") ||
    normalized.includes("LOS ANGELES") ||
    normalized.includes("CHIGUAYANTE") ||
    normalized.includes("SAN PEDRO DE LA PAZ")
  ) {
    return "Biobio"
  }

  if (normalized.includes("HUECHURABA") || normalized.includes("SANTIAGO")) {
    return "Metropolitana"
  }

  if (normalized.includes("OSORNO") || normalized.includes("PUERTO MONTT")) {
    return "Los Lagos"
  }

  if (normalized.includes("CHILLAN")) {
    return "Nuble"
  }

  if (normalized.includes("TEMUCO")) {
    return "Araucania"
  }

  return undefined
}

const decodeHtmlUrl = (value: string) => value.replace(/&amp;/g, "&")

const getTotalPages = ($: ReturnType<typeof load>) => {
  const pageNumbers = $("ul.pagination a[href*='page=']")
    .map((_, element) => {
      const href = $(element).attr("href")

      if (!href) {
        return undefined
      }

      const page = parseNumber(new URL(decodeHtmlUrl(href), SERGIO_ESCOBAR_BASE_URL).searchParams.get("page"))
      return page
    })
    .get()
    .filter((value): value is number => Boolean(value))

  return Math.max(1, ...pageNumbers)
}

const parseCardInfo = (value: string[]) => {
  const yearText = value.find((item) => /^A[nñ]o\s*:/i.test(item))
  const kmsText = value.find((item) => /^Km[s]?\s*:/i.test(item))
  const transmissionLabel = normalizeSlug(value[2])
  const transmissionText = transmissionLabel.includes("MANUAL")
    ? "MECANICA"
    : transmissionLabel.includes("AUTOMAT")
      ? "AUTOMATICA"
      : normalizeText(value[2])
  const fuelText = normalizeText(value[3])

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

export class SergioEscobarScraper implements SourceScraper {
  source = "SERGIO_ESCOBAR" as const

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

  async scrapeAll(options: SergioEscobarScrapeOptions = {}): Promise<RawListing[]> {
    const firstPage = await this.fetchPage(1)
    const maxPages = Math.max(
      1,
      Math.min(options.maxPages ?? parsePositiveInt(process.env.SERGIO_ESCOBAR_MAX_PAGES, SERGIO_ESCOBAR_MAX_PAGES), firstPage.totalPages)
    )
    const items: RawListing[] = []
    const seenExternalIds = new Set<string>()
    const pages: SergioEscobarPageResult[] = [firstPage]

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

    for (let page = 2; page <= maxPages; page += 1) {
      const result = await this.fetchPage(page)
      pages.push(result)
      options.onProgress?.(`Pagina ${page}/${maxPages}: ${result.items.length} avisos`)

      await sleep(parsePositiveInt(process.env.SERGIO_ESCOBAR_PAGE_DELAY_MS, SERGIO_ESCOBAR_PAGE_DELAY_MS))
    }

    for (const item of pages.flatMap((page) => page.items)) {
      if (seenExternalIds.has(item.externalId)) {
        continue
      }

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

    if (!items.length) {
      throw new Error("Sergio Escobar no devolvio avisos utilizables.")
    }

    return items
  }

  private async fetchPage(page: number): Promise<SergioEscobarPageResult> {
    const url = new URL(SERGIO_ESCOBAR_USED_CARS_URL)
    url.searchParams.set("page", String(page))
    url.searchParams.set("per-page", "12")

    let lastError: Error | undefined

    for (let attempt = 1; attempt <= SERGIO_ESCOBAR_RETRIES; attempt += 1) {
      try {
        const response = await fetch(url, {
          headers: defaultHeaders,
          signal: AbortSignal.timeout(SERGIO_ESCOBAR_REQUEST_TIMEOUT_MS)
        })

        if (!response.ok) {
          throw new Error(
            `Listado Sergio Escobar respondio ${response.status} ${response.statusText} en page=${page} (intento ${attempt}/${SERGIO_ESCOBAR_RETRIES})`
          )
        }

        const html = await response.text()
        const $ = load(html)
        const items = this.parseListingCards($)
        const totalAvailable = parseNumber($("#totalsearch").first().attr("value"))
        const totalPages = getTotalPages($)

        return {
          totalAvailable,
          totalPages,
          items
        }
      } catch (error) {
        lastError = error instanceof Error ? error : new Error(`Fallo desconocido en Sergio Escobar page=${page}`)
      }

      if (attempt < SERGIO_ESCOBAR_RETRIES) {
        await sleep(450 * attempt)
      }
    }

    throw lastError ?? new Error(`No pude descargar Sergio Escobar page=${page}`)
  }

  private parseListingCards($: ReturnType<typeof load>) {
    const cards = $("#w0 .card").filter((_, element) =>
      Boolean($(element).find("a.a-img-top-card[href*='/web/vehiculos/view?id=']").first().attr("href"))
    )

    return unique(
      cards
        .map((_, element) => {
          const card = $(element)
          const detailPath = card.find("a.a-img-top-card[href*='/web/vehiculos/view?id=']").first().attr("href")
          const sourceUrl = toAbsoluteUrl(detailPath)
          const detailTitle = normalizeText(card.find("a.a-img-top-card").first().attr("title"))
          const externalId = sourceUrl ? new URL(sourceUrl).searchParams.get("id") : undefined
          const modelName = normalizeText(card.find("h4.card-title span").first().text())
          const versionName = normalizeText(card.find("p.card-text").first().text())
          const infoItems = card
            .find("ul.card-content-info li")
            .map((__, item) => normalizeText($(item).text()))
            .get()
          const { year: yearFromInfo, mileageKm, transmissionText, fuelText } = parseCardInfo(infoItems)
          const yearFromTitle = parseNumber(detailTitle.match(/\b(19|20)\d{2}\b/)?.[0])
          const year = yearFromInfo ?? yearFromTitle
          const commune = normalizeText(card.find("p.adress span").first().text())
          const priceClp = parseNumber(card.find("p.card-price").first().text())
          const imageUrl = toAbsoluteUrl(card.find("img.card-img-top").first().attr("src"))
          const title = [modelName, versionName].filter(Boolean).join(" ")

          if (!sourceUrl || !externalId || !title || !year || !priceClp) {
            return undefined
          }

          return rawListingSchema.parse({
            source: this.source,
            externalId,
            url: sourceUrl,
            title,
            priceClp,
            year,
            mileageKm,
            imageUrl,
            region: inferRegion(commune),
            commune: commune || undefined,
            sellerName: "Sergio Escobar",
            transmissionText,
            fuelText,
            rawPayload: {
              listUrl: SERGIO_ESCOBAR_USED_CARS_URL,
              detailPath,
              infoItems,
              modelName,
              versionName
            }
          })
        })
        .get()
        .filter((item): item is RawListing => Boolean(item))
    )
  }
}
