import { load } from "cheerio"

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

const MARITANO_BASE_URL = "https://mye.cl"
const MARITANO_LIST_URL = `${MARITANO_BASE_URL}/vehiculos-usados/`
const MARITANO_LOAD_MORE_URL = `${MARITANO_BASE_URL}/vehiculos-usados/load-more`

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

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

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

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

const getExternalId = (url: string) => {
  const parsed = new URL(url)
  return parsed.searchParams.get("auto") ?? parsed.pathname
}

const readDebugField = (text: string, field: string) => {
  const match = text.match(new RegExp(`\\[${field}\\] => (.+)`, "m"))
  return match?.[1]?.trim()
}

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

  const match = value.match(/^(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2}):(\d{2})$/)

  if (!match) {
    return undefined
  }

  const [, day, month, year, hours, minutes, seconds] = match
  return new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}-03:00`).toISOString()
}

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

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

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
}

interface MaritanoPageAuto {
  html: string
  id: string
}

interface MaritanoLoadMoreResponse {
  success: boolean
  autos: MaritanoPageAuto[]
  hasMore: boolean
  totalAutos: number
  currentPage: string
}

export interface MaritanoScrapeOptions {
  includeDetails?: boolean
  detailConcurrency?: number
  limit?: number
  onProgress?: (message: string) => void
}

export interface MaritanoSample {
  scrapedAt: string
  totalAvailable?: number
  items: RawListing[]
}

interface MaritanoDetailData {
  publishedAt?: string
  color?: string
  branch?: string
  vehicleType?: string
  galleryUrls?: string[]
  rawDebugFields?: Record<string, string | undefined>
}

export class MaritanoScraper implements SourceScraper {
  source = "MARITANO" as const

  async scrape(): Promise<RawListing[]> {
    const sample = await this.scrapeAll()
    return sample.items
  }

  async scrapeSample(limit = 6): Promise<MaritanoSample> {
    const firstPage = await this.fetchPage(1)
    const items = firstPage.autos.slice(0, limit).map((auto) => this.parseListingCard(auto.html))

    return {
      scrapedAt: new Date().toISOString(),
      totalAvailable: firstPage.totalAutos,
      items
    }
  }

  async scrapeAll(options: MaritanoScrapeOptions = {}): Promise<MaritanoSample> {
    const includeDetails = options.includeDetails ?? true
    const detailConcurrency = options.detailConcurrency ?? 4
    const limit = options.limit
    const items: RawListing[] = []
    let page = 1
    let totalAvailable: number | undefined
    let hasMore = true

    while (hasMore && (limit === undefined || items.length < limit)) {
      const response = await this.fetchPage(page)

      if (!response.success) {
        throw new Error(`La página ${page} de Maritano no respondió success=true`)
      }

      totalAvailable = response.totalAutos
      const pageItems = response.autos.map((auto) => this.parseListingCard(auto.html))
      const remaining = limit === undefined ? pageItems.length : Math.max(limit - items.length, 0)

      items.push(...pageItems.slice(0, remaining))
      options.onProgress?.(`Pagina ${page}: ${items.length}/${totalAvailable ?? "?"} avisos acumulados`)

      hasMore = response.hasMore && (limit === undefined || items.length < limit)
      page += 1
    }

    const enrichedItems = includeDetails
      ? await mapWithConcurrency(items, detailConcurrency, async (item, index) => {
          const detail = await this.fetchDetail(item.url)
          options.onProgress?.(`Detalle ${index + 1}/${items.length}: ${item.externalId}`)
          return this.mergeDetailIntoListing(item, detail)
        })
      : items

    return {
      scrapedAt: new Date().toISOString(),
      totalAvailable,
      items: enrichedItems
    }
  }

  private async fetchPage(page: number): Promise<MaritanoLoadMoreResponse> {
    const url = new URL(MARITANO_LOAD_MORE_URL)
    url.searchParams.set("page", String(page))

    const response = await fetch(url, {
      headers: {
        ...defaultHeaders,
        "x-requested-with": "XMLHttpRequest"
      }
    })

    if (!response.ok) {
      throw new Error(`Maritano load-more respondió ${response.status} ${response.statusText}`)
    }

    return (await response.json()) as MaritanoLoadMoreResponse
  }

  private parseListingCard(html: string): RawListing {
    const $ = load(html)
    const detailPath = $("a[href*='/vehiculos-usados/detalle?auto=']").first().attr("href")
    const sourceUrl = toAbsoluteUrl(detailPath)
    const imageUrl = toAbsoluteUrl($("img").first().attr("src"))
    const title = normalizeText($("p.is-uppercase").first().text())
    const priceClp = parseNumber($("p.bold.is-size-5").first().text())
    const specsText = normalizeText($("p.has-text-weight-bold").first().text())
    const bonusText = normalizeText($(".bono-area").first().text())

    if (!sourceUrl || !priceClp || !title || !specsText) {
      throw new Error("No pude extraer uno de los campos base del listado Maritano")
    }

    const { year, mileageKm, transmissionText, fuelText } = parseSpecs(specsText)

    return rawListingSchema.parse({
      source: this.source,
      externalId: getExternalId(sourceUrl),
      url: sourceUrl,
      title,
      priceClp,
      year,
      mileageKm,
      imageUrl,
      sellerName: "Maritano Usados",
      transmissionText,
      fuelText,
      rawPayload: {
        listUrl: MARITANO_LIST_URL,
        detailPath,
        imageUrl,
        specsText,
        bonusText: bonusText || undefined
      }
    })
  }

  private async fetchDetail(sourceUrl: string): Promise<MaritanoDetailData> {
    const response = await fetch(sourceUrl, {
      headers: defaultHeaders
    })

    if (!response.ok) {
      throw new Error(`Detalle Maritano respondió ${response.status} ${response.statusText} para ${sourceUrl}`)
    }

    const html = await response.text()
    const $ = load(html)
    const debugText = $("pre[style*='display:none']").first().text()
    const galleryUrls = unique(
      $(".slider-auto a[href], .imagen-auto a[href]")
        .map((_, element) => toAbsoluteUrl($(element).attr("href")))
        .get()
        .filter((value): value is string => Boolean(value))
    )

    return {
      publishedAt: parseDate(readDebugField(debugText, "FechaCreacion")),
      color: readDebugField(debugText, "NombreColor"),
      branch: readDebugField(debugText, "NombreSucursal"),
      vehicleType: readDebugField(debugText, "NombreTipoVeh"),
      galleryUrls,
      rawDebugFields: {
        brand: readDebugField(debugText, "NombreMarca"),
        model: readDebugField(debugText, "NombreModelo"),
        group: readDebugField(debugText, "NombreGrupo"),
        listPrice: readDebugField(debugText, "PrecioLista"),
        offerPrice: readDebugField(debugText, "PrecioOferta"),
        branch: readDebugField(debugText, "NombreSucursal"),
        type: readDebugField(debugText, "NombreTipoVeh"),
        color: readDebugField(debugText, "NombreColor")
      }
    }
  }

  private mergeDetailIntoListing(item: RawListing, detail: MaritanoDetailData): RawListing {
    return rawListingSchema.parse({
      ...item,
      rawPayload: {
        ...(typeof item.rawPayload === "object" && item.rawPayload !== null ? item.rawPayload : {}),
        detail
      }
    })
  }
}
