from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Callable, Optional
from urllib.parse import quote

import requests

from .download import download_audio, select_audio_url
from .fetch import fetch_html_playwright, fetch_html_requests, make_session
from .parse import extract_audio_host, iter_play_items, safe_filename


@dataclass(frozen=True)
class AudioCandidate:
    play_id: str
    label: str
    url: str
    out_path: str


@dataclass(frozen=True)
class ScrapeResult:
    downloaded_count: int
    candidates: tuple[AudioCandidate, ...]


def scrape(
    word: str,
    *,
    outdir: str = "forvo_mp3",
    limit: int = 1000,
    dry_run: bool = False,
    no_head: bool = False,
    prefix: str | None = None,
    use_playwright: bool = True,
    headed: bool = False,
    emit: Optional[Callable[[str], None]] = None,
) -> ScrapeResult:
    """Scrape pronunciation audio (MP3) from a Forvo page and optionally download.

    Fetches the page, parses play items, resolves MP3 URLs, and downloads files
    into ``outdir``. Skips duplicates and failed downloads; continues until
    ``limit`` items are downloaded or the page is exhausted.

    Args:
        word: Word to search on Forvo.
        outdir: Directory to save MP3 files. Created if missing.
        limit: Maximum number of audio files to download (0 = no limit).
        dry_run: If True, do not download; only collect candidates and count.
        no_head: If True, skip HEAD checks when resolving audio URLs.
        prefix: Filename prefix; if None, derived from the input word.
        use_playwright: If True, fetch HTML with Playwright instead of requests.
        headed: If True, run Playwright browser in headed (visible) mode.
        emit: Optional callback called with progress strings (URLs, paths, skips).

    Returns:
        ScrapeResult with downloaded_count and candidates (all considered items).

    Raises:
        ValueError: If limit is negative.
    """
    if limit < 0:
        raise ValueError("limit must be >= 0")

    normalized_word = word.strip()
    encoded_word = quote(normalized_word)
    url = f"https://forvo.com/search/{encoded_word}/no/"
    name_prefix = safe_filename(prefix) if prefix else safe_filename(normalized_word)
    session = make_session()
    if use_playwright:
        html = fetch_html_playwright(url, headed=headed)
    else:
        html = fetch_html_requests(url, session)

    audio_host = extract_audio_host(html)
    base_url = f"https://{audio_host}/audios/mp3"

    downloaded = 0
    seen_urls: set[str] = set()
    candidates: list[AudioCandidate] = []

    for item in iter_play_items(html):
        if item.label.strip().casefold() != normalized_word.casefold():
            continue

        if downloaded >= limit:
            break

        chosen_url = select_audio_url(
            item,
            base_url,
            session,
            no_head=no_head,
            seen_urls=seen_urls,
        )
        if not chosen_url:
            if emit:
                emit(f"[skip] play_id={item.play_id} label={item.label} (no working mp3 URL)")
            continue

        seen_urls.add(chosen_url)
        filename = f"{name_prefix}_{safe_filename(item.label)}_{item.play_id}_{downloaded+1:03d}.mp3"
        out_path = os.path.join(outdir, filename)
        candidates.append(
            AudioCandidate(
                play_id=item.play_id,
                label=item.label,
                url=chosen_url,
                out_path=out_path,
            )
        )

        if emit:
            emit(chosen_url)

        if dry_run:
            downloaded += 1
            continue

        try:
            download_audio(chosen_url, out_path, session)
            if emit:
                emit(f"  -> {out_path}")
            downloaded += 1
        except requests.HTTPError as exc:
            if emit:
                emit(f"[skip] download failed for {chosen_url}: {exc}")
        except requests.RequestException as exc:
            if emit:
                emit(f"[skip] network error for {chosen_url}: {exc}")

    return ScrapeResult(downloaded_count=downloaded, candidates=tuple(candidates))
