import { load } from "cheerio"

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

const BRUNO_FRITSCH_BASE_URL = "https://www.brunofritsch.cl"
const BRUNO_FRITSCH_LIST_URL = `${BRUNO_FRITSCH_BASE_URL}/autos-usados`
const BRUNO_FRITSCH_LIST_RETRIES = 4
const BRUNO_FRITSCH_DETAIL_RETRIES = 2
const BRUNO_FRITSCH_REQUEST_TIMEOUT_MS = 60000

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
  }

  const url = new URL(value, BRUNO_FRITSCH_BASE_URL)

  if (url.hostname === new URL(BRUNO_FRITSCH_BASE_URL).hostname) {
    url.protocol = "https:"
  }

  return url.toString()
}

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

const inferRegion = (city?: string) => {
  const normalizedCity = city?.normalize("NFD").replace(/\p{Diacritic}/gu, "").toUpperCase()

  if (!normalizedCity) {
    return undefined
  }

  if (normalizedCity.includes("SANTIAGO")) {
    return "Metropolitana"
  }

  if (normalizedCity.includes("CONCEPCION")) {
    return "Biobio"
  }

  return undefined
}

const parseBranchAddress = (value?: string) => {
  const address = normalizeText(value)

  if (!address) {
    return {
      branchAddress: undefined,
      commune: undefined,
      city: undefined,
      region: undefined
    }
  }

  const match = address.match(/,\s*([^,-]+?)\s*-\s*([^,-]+)\s*$/)
  const commune = normalizeText(match?.[1])
  const city = normalizeText(match?.[2])

  return {
    branchAddress: address,
    commune: commune || undefined,
    city: city || undefined,
    region: inferRegion(city)
  }
}

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 sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

const toErrorMessage = (error: unknown) => {
  if (error instanceof DOMException && error.name === "TimeoutError") {
    return "Timeout del sitio remoto"
  }

  return error instanceof Error ? error.message : "fallo desconocido"
}

interface BrunoFritschStructuredProduct {
  name?: string
  description?: string
  sku?: string
  productId?: string
  brand?: string
  image?: string
  offers?: {
    price?: number | string
    priceCurrency?: string
    availability?: string
  }
}

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

export class BrunoFritschScraper implements SourceScraper {
  source = "BRUNO_FRITSCH" as const

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

  async scrapeAll(options: BrunoFritschScrapeOptions = {}): Promise<RawListing[]> {
    const detailConcurrency = options.detailConcurrency ?? 4
    const maxPages = options.maxPages ?? 200
    const detailUrls = await this.collectDetailUrls(maxPages, options.onProgress)

    const items = await mapWithConcurrency(detailUrls, detailConcurrency, async (sourceUrl, index) => {
      try {
        const item = await this.fetchDetail(sourceUrl)
        options.onProgress?.(`Detalle ${index + 1}/${detailUrls.length}: ${item.externalId}`)
        return item
      } catch (error) {
        const message = error instanceof Error ? error.message : "fallo desconocido"
        options.onProgress?.(`Detalle ${index + 1}/${detailUrls.length}: omitido (${message})`)
        return undefined
      }
    })

    return items.filter((item): item is RawListing => Boolean(item))
  }

  private async collectDetailUrls(maxPages: number, onProgress?: (message: string) => void) {
    const urls: string[] = []
    const seen = new Set<string>()

    for (let page = 1; page <= maxPages; page += 1) {
      const pageUrls = await this.fetchListingPage(page)

      if (pageUrls.length === 0) {
        onProgress?.(`Pagina ${page}: sin resultados, fin de la paginacion`)
        break
      }

      let newUrls = 0

      for (const url of pageUrls) {
        if (seen.has(url)) {
          continue
        }

        seen.add(url)
        urls.push(url)
        newUrls += 1
      }

      onProgress?.(`Pagina ${page}: ${newUrls} nuevos, ${urls.length} avisos acumulados`)

      if (newUrls === 0) {
        break
      }
    }

    return urls
  }

  private async fetchListingPage(page: number): Promise<string[]> {
    const url = new URL(BRUNO_FRITSCH_LIST_URL)

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

    let lastError: Error | undefined

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

        if (!response.ok) {
          lastError = new Error(
            `Listado Bruno Fritsch respondió ${response.status} ${response.statusText} en page=${page} (intento ${attempt}/${BRUNO_FRITSCH_LIST_RETRIES})`
          )
        } else {
          const html = await response.text()
          const $ = load(html)

          return unique(
            $("a#product-card-link[href^='/autos-usados/']")
              .map((_, element) => toAbsoluteUrl($(element).attr("href")))
              .get()
              .filter((value): value is string => Boolean(value))
          )
        }
      } catch (error) {
        lastError = new Error(`Listado Bruno Fritsch fallo en page=${page}: ${toErrorMessage(error)}`)
      }

      if (attempt < BRUNO_FRITSCH_LIST_RETRIES) {
        await sleep(500 * attempt)
      }
    }

    throw lastError ?? new Error(`No pude descargar el listado Bruno Fritsch en page=${page}`)
  }

  private async fetchDetail(sourceUrl: string): Promise<RawListing> {
    let lastError: Error | undefined

    for (let attempt = 1; attempt <= BRUNO_FRITSCH_DETAIL_RETRIES; attempt += 1) {
      let response: Response

      try {
        response = await fetch(sourceUrl, {
          headers: defaultHeaders,
          signal: AbortSignal.timeout(BRUNO_FRITSCH_REQUEST_TIMEOUT_MS)
        })
      } catch (error) {
        lastError = new Error(`Detalle Bruno Fritsch fallo para ${sourceUrl}: ${toErrorMessage(error)}`)

        if (attempt < BRUNO_FRITSCH_DETAIL_RETRIES) {
          await sleep(350 * attempt)
        }

        continue
      }

      if (response.ok) {
        const html = await response.text()
        return this.parseDetailPage(html, sourceUrl)
      }

      lastError = new Error(
        `Detalle Bruno Fritsch respondió ${response.status} ${response.statusText} para ${sourceUrl} (intento ${attempt}/${BRUNO_FRITSCH_DETAIL_RETRIES})`
      )

      if (attempt < BRUNO_FRITSCH_DETAIL_RETRIES) {
        await sleep(350 * attempt)
      }
    }

    throw lastError ?? new Error(`No pude descargar el detalle Bruno Fritsch para ${sourceUrl}`)
  }

  private parseDetailPage(html: string, sourceUrl: string): RawListing {
    const $ = load(html)
    const h1 = normalizeText($("h1").first().text())
    const h2 = normalizeText($("h2").first().text())
    const year =
      parseNumber(
        $("span")
          .map((_, element) => normalizeText($(element).text()))
          .get()
          .find((value) => /^\d{4}$/.test(value))
      ) ?? parseNumber(h1.match(/\b(19|20)\d{2}\b/)?.[0])

    const mileageKm = parseNumber(
      $("span")
        .map((_, element) => normalizeText($(element).text()))
        .get()
        .find((value) => /\bkm\b/i.test(value))
    )

    const fuelText = normalizeText(
      $("span")
        .map((_, element) => normalizeText($(element).text()))
        .get()
        .find((value) => /^(Gasolina|Diesel|Hibrido|Híbrido|Electrico|Eléctrico)$/i.test(value))
    )

    const transmissionText = normalizeText(
      $("span")
        .map((_, element) => normalizeText($(element).text()))
        .get()
        .find((value) => /^(Automatica|Automática|Mecanica|Mecánica|Manual|CVT|DCT)$/i.test(value))
    )

    const branchAddress = normalizeText(
      $("span")
        .map((_, element) => normalizeText($(element).text()))
        .get()
        .find((value) => /,\s*[^,-]+\s*-\s*[^,-]+$/.test(value))
    )

    const structuredProduct = this.parseStructuredProduct($)
    const fullTitleFromStructured = normalizeText(
      [normalizeText(structuredProduct?.brand), normalizeText(structuredProduct?.description || structuredProduct?.name)]
        .filter(Boolean)
        .join(" ")
    )
    const fallbackTitle = normalizeText(
      [h1.replace(/\b(19|20)\d{2}\b/g, ""), h2]
        .map((value) => normalizeText(value))
        .filter(Boolean)
        .join(" ")
    )
    const priceClp = parseNumber(structuredProduct?.offers?.price)
    const imageUrl = toAbsoluteUrl(
      structuredProduct?.image || $("meta[property='og:image']").attr("content") || $("img").first().attr("src")
    )
    const externalId =
      normalizeText(structuredProduct?.sku || structuredProduct?.productId) || sourceUrl.split("/").pop() || sourceUrl
    const branch = parseBranchAddress(branchAddress)

    if (!priceClp || !year) {
      throw new Error(`No pude extraer precio o año desde ${sourceUrl}`)
    }

    return rawListingSchema.parse({
      source: this.source,
      externalId,
      url: sourceUrl,
      title: fullTitleFromStructured || fallbackTitle || h1,
      priceClp,
      year,
      mileageKm,
      imageUrl,
      region: branch.region,
      commune: branch.commune,
      sellerName: "Bruno Fritsch",
      transmissionText: transmissionText || undefined,
      fuelText: fuelText || undefined,
      rawPayload: {
        branchAddress: branch.branchAddress,
        branchCity: branch.city,
        heading: h1 || undefined,
        subheading: h2 || undefined,
        structuredProduct
      }
    })
  }

  private parseStructuredProduct($: ReturnType<typeof load>): BrunoFritschStructuredProduct | undefined {
    const structuredData = $("script[data-name='occ-structured-data'][type='application/ld+json']").first().html()

    if (!structuredData) {
      return undefined
    }

    try {
      const parsed = JSON.parse(structuredData) as BrunoFritschStructuredProduct | BrunoFritschStructuredProduct[]
      return Array.isArray(parsed) ? parsed[0] : parsed
    } catch {
      return undefined
    }
  }
}
