import { load } from "cheerio"

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

const PORTILLO_SUR_BASE_URL = "https://www.portillosur.cl"
const PORTILLO_SUR_AJAX_URL = `${PORTILLO_SUR_BASE_URL}/cms/wp-admin/admin-ajax.php`
const PORTILLO_SUR_LIST_URL = `${PORTILLO_SUR_BASE_URL}/usados/concepcion/`
const PORTILLO_SUR_PER_PAGE_CANDIDATES = [100, 80, 60]
const PORTILLO_SUR_LIST_RETRIES = 6
const PORTILLO_SUR_DETAIL_RETRIES = 4
const PORTILLO_SUR_LIST_TIMEOUT_MS = 90000
const PORTILLO_SUR_DETAIL_TIMEOUT_MS = 75000
const PORTILLO_SUR_MIN_RETRY_DELAY_MS = 900
const PORTILLO_SUR_MAX_RETRY_DELAY_MS = 12000
const RETRYABLE_HTTP_STATUS = new Set([408, 425, 429, 500, 502, 503, 504, 522, 524])

const defaultHeaders = {
  accept: "application/json, text/plain, */*",
  "accept-language": "es-CL,es;q=0.9,en;q=0.8",
  referer: PORTILLO_SUR_LIST_URL,
  "user-agent":
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
}

const cityLabelBySlug = {
  concepcion: "Concepcion",
  osorno: "Osorno",
  temuco: "Temuco"
} as const

const regionByCitySlug = {
  concepcion: "Biobio",
  osorno: "Los Lagos",
  temuco: "Araucania"
} as const

type PortilloCitySlug = keyof typeof cityLabelBySlug

interface PortilloSurAjaxListing {
  id: number
  title?: string
  subtitle?: string
  image?: string
  price?: string | number
  list_price?: string | number
  secondary_button?: {
    title?: string
    url?: string
  }
  brand?: string
  model?: string
}

interface PortilloSurAjaxResponse {
  data?: PortilloSurAjaxListing[]
  pagination?: {
    current_page?: number | string | false
    next_page?: number | string | false
    prev_page?: number | string | false
    max_pages?: number | string | false
    per_page?: number | string | false
    total_items?: number | string | false
  }
}

interface PortilloSurPageResult {
  totalAvailable?: number
  totalPages: number
  items: PortilloSurAjaxListing[]
}

export interface PortilloSurScrapeOptions {
  maxPages?: number
  pageConcurrency?: number
  detailConcurrency?: number
  perPageCandidates?: number[]
  onProgress?: (message: string) => void
}

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

const parseNumber = (value?: string | number | boolean | 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, PORTILLO_SUR_BASE_URL).toString()
}

const formatSlug = (value?: string) =>
  value
    ?.split("-")
    .filter(Boolean)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ")
    .trim() ?? ""

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

const parseRetryAfterMs = (retryAfterHeader: string | null) => {
  if (!retryAfterHeader) {
    return undefined
  }

  const numericSeconds = Number(retryAfterHeader)

  if (Number.isFinite(numericSeconds)) {
    const waitMs = Math.trunc(numericSeconds * 1000)
    return waitMs > 0 ? Math.min(waitMs, PORTILLO_SUR_MAX_RETRY_DELAY_MS) : undefined
  }

  const parsedDateMs = Date.parse(retryAfterHeader)

  if (!Number.isFinite(parsedDateMs)) {
    return undefined
  }

  const deltaMs = parsedDateMs - Date.now()
  return deltaMs > 0 ? Math.min(deltaMs, PORTILLO_SUR_MAX_RETRY_DELAY_MS) : undefined
}

const isRetryableHttpStatus = (statusCode: number) => RETRYABLE_HTTP_STATUS.has(statusCode)

const computeRetryDelayMs = (attempt: number, retryAfterMs?: number) => {
  if (retryAfterMs && retryAfterMs > 0) {
    return retryAfterMs
  }

  const exponentialDelay = PORTILLO_SUR_MIN_RETRY_DELAY_MS * 2 ** Math.max(attempt - 1, 0)
  const cappedDelay = Math.min(exponentialDelay, PORTILLO_SUR_MAX_RETRY_DELAY_MS)
  const jitterMs = Math.trunc(Math.random() * 450)
  return cappedDelay + jitterMs
}

const extractLocationFromSlug = (value?: string) => {
  const slug = normalizeText(value).replace(/^semi-nuevos-/, "")

  for (const citySlug of Object.keys(cityLabelBySlug) as PortilloCitySlug[]) {
    if (slug === citySlug || slug.startsWith(`${citySlug}-`)) {
      const branchSlug = slug.slice(citySlug.length).replace(/^-/, "")
      const branchName = [cityLabelBySlug[citySlug], formatSlug(branchSlug)].filter(Boolean).join(" ").trim()

      return {
        branchName: branchName || cityLabelBySlug[citySlug],
        commune: cityLabelBySlug[citySlug],
        region: regionByCitySlug[citySlug]
      }
    }
  }

  return {
    branchName: formatSlug(slug) || undefined,
    commune: undefined,
    region: undefined
  }
}

const extractPathSegment = (sourceUrl: string, index: number) => {
  const segments = new URL(sourceUrl).pathname.split("/").filter(Boolean)
  return segments[index]
}

const buildTitle = (listing: PortilloSurAjaxListing) => {
  const brand = normalizeText(listing.brand)
  const title = normalizeText(listing.title)

  if (!brand) {
    return title
  }

  if (title.toUpperCase().startsWith(`${brand.toUpperCase()} `)) {
    return title
  }

  return [brand, title].filter(Boolean).join(" ")
}

export class PortilloSurScraper implements SourceScraper {
  source = "PORTILLO_SUR" as const

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

  async scrapeAll(options: PortilloSurScrapeOptions = {}): Promise<RawListing[]> {
    const candidatePerPageValues = (options.perPageCandidates?.length
      ? options.perPageCandidates
      : PORTILLO_SUR_PER_PAGE_CANDIDATES
    ).filter((value) => Number.isFinite(value) && value >= 20)

    let lastError: Error | undefined

    for (let index = 0; index < candidatePerPageValues.length; index += 1) {
      const perPage = Math.trunc(candidatePerPageValues[index]!)

      try {
        return await this.scrapeAllWithPerPage(perPage, options)
      } catch (error) {
        lastError = error instanceof Error ? error : new Error("Fallo desconocido en scrapeo de Portillo Sur")
        const nextPerPage = candidatePerPageValues[index + 1]

        if (nextPerPage) {
          options.onProgress?.(
            `Portillo Sur fallo con per_page=${perPage}. Reintentando con per_page=${nextPerPage}. Motivo: ${lastError.message}`
          )
        }
      }
    }

    throw lastError ?? new Error("No pude completar el scrapeo de Portillo Sur")
  }

  private uniqueById(items: PortilloSurAjaxListing[]) {
    const seen = new Set<number>()
    const uniqueItems: PortilloSurAjaxListing[] = []

    for (const item of items) {
      if (!item?.id || seen.has(item.id)) {
        continue
      }

      seen.add(item.id)
      uniqueItems.push(item)
    }

    return uniqueItems
  }

  private async scrapeAllWithPerPage(perPage: number, options: PortilloSurScrapeOptions): Promise<RawListing[]> {
    const pageConcurrency = options.pageConcurrency ?? 1
    const detailConcurrency = options.detailConcurrency ?? 4
    const firstPage = await this.fetchPage(1, perPage)
    const totalPages = Math.max(1, Math.min(options.maxPages ?? firstPage.totalPages, firstPage.totalPages))
    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 ?? "?"} (per_page=${perPage})`
    )

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

    const listings = this.uniqueById([firstPage, ...remainingPages].flatMap((page) => page.items))

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

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

  private async fetchPage(page: number, perPage: number): Promise<PortilloSurPageResult> {
    const url = new URL(PORTILLO_SUR_AJAX_URL)
    url.searchParams.set("action", "get-cars-data")
    url.searchParams.set("page", String(page))
    url.searchParams.set("per_page", String(perPage))

    let lastError: Error | undefined

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

        if (response.ok) {
          const payload = (await response.json()) as PortilloSurAjaxResponse
          const items = Array.isArray(payload.data) ? payload.data : []
          const totalPages = Math.max(1, parseNumber(payload.pagination?.max_pages) ?? 1)
          const totalAvailable = parseNumber(payload.pagination?.total_items)

          return {
            totalAvailable,
            totalPages,
            items
          }
        }

        const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"))
        const canRetry = isRetryableHttpStatus(response.status)
        lastError = new Error(
          `Listado Portillo Sur respondio ${response.status} ${response.statusText} en page=${page}, per_page=${perPage} (intento ${attempt}/${PORTILLO_SUR_LIST_RETRIES})`
        )

        if (!canRetry || attempt >= PORTILLO_SUR_LIST_RETRIES) {
          break
        }

        await sleep(computeRetryDelayMs(attempt, retryAfterMs))
        continue
      } catch (error) {
        lastError = new Error(
          `Listado Portillo Sur fallo en page=${page}, per_page=${perPage} (intento ${attempt}/${PORTILLO_SUR_LIST_RETRIES}): ${toErrorMessage(error)}`
        )
      }

      if (attempt < PORTILLO_SUR_LIST_RETRIES) {
        await sleep(computeRetryDelayMs(attempt))
      }
    }

    throw lastError ?? new Error(`No pude descargar el listado Portillo Sur en page=${page}, per_page=${perPage}`)
  }

  private async fetchDetail(listing: PortilloSurAjaxListing): Promise<RawListing> {
    const sourceUrl = toAbsoluteUrl(listing.secondary_button?.url)

    if (!sourceUrl) {
      throw new Error(`No pude extraer la URL de detalle para el aviso ${listing.id}`)
    }

    let lastError: Error | undefined

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

      try {
        response = await fetch(sourceUrl, {
          headers: defaultHeaders,
          signal: AbortSignal.timeout(PORTILLO_SUR_DETAIL_TIMEOUT_MS)
        })
      } catch (error) {
        lastError = new Error(
          `Detalle Portillo Sur fallo para ${sourceUrl} (intento ${attempt}/${PORTILLO_SUR_DETAIL_RETRIES}): ${toErrorMessage(error)}`
        )

        if (attempt < PORTILLO_SUR_DETAIL_RETRIES) {
          await sleep(computeRetryDelayMs(attempt))
        }

        continue
      }

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

      lastError = new Error(
        `Detalle Portillo Sur respondio ${response.status} ${response.statusText} para ${sourceUrl} (intento ${attempt}/${PORTILLO_SUR_DETAIL_RETRIES})`
      )

      if (!isRetryableHttpStatus(response.status) || attempt >= PORTILLO_SUR_DETAIL_RETRIES) {
        break
      }

      await sleep(computeRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))))
    }

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

  private parseDetailPage(html: string, listing: PortilloSurAjaxListing, sourceUrl: string): RawListing {
    const $ = load(html)
    const characteristics = new Map<string, string>()

    $(".car-details__characteristics--item").each((_, element) => {
      const title = normalizeText($(element).find(".car-details__characteristics--item-title").text())
      const value = normalizeText($(element).find(".car-details__characteristics--item-text").text())

      if (title && value) {
        characteristics.set(title, value)
      }
    })

    const year = parseNumber(characteristics.get("Año") ?? characteristics.get("Ano"))
    const mileageKm = parseNumber(characteristics.get("Kilometraje"))
    const transmissionText = characteristics.get("Transmisión") ?? characteristics.get("Transmision")
    const fuelText = characteristics.get("Combustible")
    const priceClp = parseNumber(listing.price) ?? parseNumber(listing.list_price)
    const imageUrl = toAbsoluteUrl(listing.image)
    const branchLabel = normalizeText($("span.sucursal-info__title").first().text())
    const location = extractLocationFromSlug(extractPathSegment(sourceUrl, 1))
    const title = buildTitle(listing)

    if (!year || !priceClp || !title) {
      throw new Error(`No pude extraer precio, ano o titulo desde ${sourceUrl}`)
    }

    return rawListingSchema.parse({
      source: this.source,
      externalId: String(listing.id),
      url: sourceUrl,
      title,
      priceClp,
      year,
      mileageKm,
      imageUrl,
      region: location.region,
      commune: location.commune,
      sellerName: "Portillo Sur",
      transmissionText: transmissionText || undefined,
      fuelText: fuelText || undefined,
      rawPayload: {
        listUrl: PORTILLO_SUR_LIST_URL,
        detailHeading: normalizeText($("h1").first().text()) || undefined,
        subtitle: normalizeText(listing.subtitle) || undefined,
        branchLabel: branchLabel || undefined,
        branchName: location.branchName,
        characteristics: Object.fromEntries(characteristics),
        listing
      }
    })
  }
}
