"""
Session HTTP vers Metal Archives via FlareSolverr persistent sessions.

Cloudflare bloque les requêtes `requests` directes (fingerprint TLS/JA3).
Toutes les requêtes passent par FlareSolverr qui utilise un Chrome headless réel.
Une session Chrome est créée une fois et réutilisée pour toute la durée du crawl
(refresh automatique toutes les 4h ou sur 403/429).
"""
import json
import logging
import re
import time
from html import unescape as html_unescape
from typing import Any, Dict, Optional
from urllib.parse import urlencode

from .config import MA_BASE, AJAX_PAGE_SIZE, LIST_MIN_DELAY, LIST_MAX_DELAY
from .flaresolverr import FlareSolverr, FlareSolverrError
from .polite import sleep_range

log = logging.getLogger(__name__)

_WARMUP_URL = f"{MA_BASE}/lists/FR"
_SESSION_TTL = 3600 * 4  # refresh session toutes les 4h


class MASession:
    def __init__(self, fs: FlareSolverr):
        self.fs = fs
        self._session_id: Optional[str] = None
        self._session_age = 0.0

    # ------------------------------------------------------------------
    # Session management
    # ------------------------------------------------------------------

    def ensure_session(self, force_refresh: bool = False):
        age = time.time() - self._session_age
        if self._session_id is None or force_refresh or age > _SESSION_TTL:
            if self._session_id:
                self.fs.destroy_session(self._session_id)
                self._session_id = None
            log.info("[ma_http] creating FlareSolverr persistent session...")
            self._session_id = self.fs.create_session()
            log.info(f"[ma_http] warming up CF cookies on {_WARMUP_URL}")
            status, _ = self.fs.get(_WARMUP_URL, self._session_id)
            log.info(f"[ma_http] warmup done (status={status}), session ready")
            self._session_age = time.time()

    def get_json(self, url: str, params: Dict = None, retries: int = 2) -> Any:
        """GET JSON depuis un endpoint AJAX MA."""
        self.ensure_session()
        full_url = _build_url(url, params)
        for attempt in range(retries + 1):
            status, body = self.fs.get(full_url, self._session_id)
            if status in (403, 429, 503) and attempt < retries:
                log.warning(f"[ma_http] {status} on {url}, refreshing session...")
                self.ensure_session(force_refresh=True)
                sleep_range(LIST_MIN_DELAY * 2, LIST_MAX_DELAY * 2)
                continue
            if status != 200:
                raise RuntimeError(f"HTTP {status} on {full_url}")
            try:
                return _extract_json(body)
            except (json.JSONDecodeError, ValueError) as e:
                if attempt < retries:
                    log.warning(f"[ma_http] JSON invalide/vide depuis {url} (tentative {attempt+1}), refresh session…")
                    self.ensure_session(force_refresh=True)
                    sleep_range(LIST_MIN_DELAY * 2, LIST_MAX_DELAY * 2)
                    continue
                raise RuntimeError(f"JSON parse error après {retries} tentatives sur {url}: {e}")
        raise RuntimeError(f"Failed after {retries} retries: {url}")

    def get_html(self, url: str, retries: int = 2) -> str:
        """GET HTML rendu (pages band, etc.)."""
        self.ensure_session()
        for attempt in range(retries + 1):
            status, body = self.fs.get(url, self._session_id)
            if status in (403, 429, 503) and attempt < retries:
                log.warning(f"[ma_http] {status} on {url}, refreshing session...")
                self.ensure_session(force_refresh=True)
                sleep_range(LIST_MIN_DELAY * 2, LIST_MAX_DELAY * 2)
                continue
            if status != 200:
                if attempt < retries:
                    sleep_range(3, 6)
                    continue
                raise RuntimeError(f"HTTP {status} on {url}")
            return body
        raise RuntimeError(f"Failed after {retries} retries: {url}")

    # ------------------------------------------------------------------
    # Metal Archives AJAX helpers
    # ------------------------------------------------------------------

    def fetch_country_bands(self, country_code: str):
        """
        Pagine l'endpoint AJAX country list MA et yield chaque band.
        Endpoint : /browse/ajax-country/c/{cc}/json/1
        Réponse  : {"iTotalRecords": N, "aaData": [["<a href='url'>name</a>", genre, location, status], ...]}
        """
        url = f"{MA_BASE}/browse/ajax-country/c/{country_code}/json/1"
        start = 0
        total = None
        echo = 1

        while True:
            params = {
                "sEcho": echo,
                "iColumns": 4,
                "iDisplayStart": start,
                "iDisplayLength": AJAX_PAGE_SIZE,
            }
            log.info(f"[ma_http] country={country_code} start={start}")
            data = self.get_json(url, params=params)
            echo += 1

            if total is None:
                total = int(data.get("iTotalRecords") or data.get("iTotalDisplayRecords") or 0)
                log.info(f"[ma_http] {country_code}: {total} total bands")

            rows = data.get("aaData") or []
            if not rows:
                break

            for row in rows:
                parsed = _parse_list_row(row, country_code)
                if parsed:
                    yield parsed

            start += len(rows)
            if start >= total:
                break

            sleep_range(LIST_MIN_DELAY, LIST_MAX_DELAY)

    def fetch_archive_bands(self, order_by: str, since_date: Optional[str] = None):
        """
        Pagine l'endpoint AJAX des archives (latest additions ou latest modified).
        order_by : 'created' | 'modified'
        Endpoint : /archives/ajax-band-list/by/{order_by}/json/1
        Réponse  : {"iTotalRecords": N, "aaData": [["<a>name</a>", country, genre, date], ...]}

        Si since_date (ISO date string), s'arrête dès qu'on voit des entrées plus anciennes.
        """
        url = f"{MA_BASE}/archives/ajax-band-list/by/{order_by}/json/1"
        start = 0
        echo = 1

        while True:
            params = {
                "sEcho": echo,
                "iColumns": 4,
                "iDisplayStart": start,
                "iDisplayLength": AJAX_PAGE_SIZE,
            }
            log.info(f"[ma_http] archives/{order_by} start={start}")
            data = self.get_json(url, params=params)
            echo += 1

            rows = data.get("aaData") or []
            if not rows:
                break

            stop = False
            for row in rows:
                parsed = _parse_archive_row(row, order_by)
                if not parsed:
                    continue
                if since_date and parsed.get("date_str") and parsed["date_str"] <= since_date:
                    stop = True
                    break
                yield parsed

            if stop:
                break

            total = int(data.get("iTotalRecords") or 0)
            start += len(rows)
            if start >= total:
                break

            sleep_range(LIST_MIN_DELAY, LIST_MAX_DELAY)


# ------------------------------------------------------------------
# Helpers internes
# ------------------------------------------------------------------

def _build_url(base: str, params: Dict = None) -> str:
    if not params:
        return base
    return f"{base}?{urlencode(params)}"


def _extract_json(body: str) -> Any:
    """
    FlareSolverr wraps les réponses JSON dans <pre> avec HTML-encoding des <>.
    On extrait le contenu, on unescape les entités HTML, puis on parse le JSON.
    """
    m = re.search(r"<pre[^>]*>([\s\S]*?)</pre>", body)
    text = html_unescape(m.group(1)) if m else body
    return json.loads(text)


# ------------------------------------------------------------------
# Row parsers
# ------------------------------------------------------------------

def _extract_link(html_str: str):
    """Extrait href et texte d'un fragment HTML '<a href="...">text</a>'."""
    m = re.search(r'href=["\']([^"\']+)["\']', html_str or "")
    href = m.group(1) if m else None
    text = re.sub(r"<[^>]+>", "", html_str or "").strip()
    return href, text


def _extract_ma_id(url: str) -> Optional[int]:
    m = re.search(r"/bands/[^/]+/(\d+)", url or "")
    return int(m.group(1)) if m else None


def _clean(s) -> str:
    return re.sub(r"<[^>]+>", "", str(s or "")).strip()


def _parse_list_row(row, country_code: str) -> Optional[Dict]:
    """Ligne du listing pays : [name_html, genre, location, status]"""
    if len(row) < 3:
        return None
    href, name = _extract_link(row[0])
    ma_id = _extract_ma_id(href)
    if not ma_id or not name:
        return None
    return {
        "ma_id": ma_id,
        "name": name,
        "url": href,
        "country": country_code,
        "genre": _clean(row[1]),
        "location_text": _clean(row[2]),
        "status": _clean(row[3]) if len(row) > 3 else None,
    }


def _parse_archive_row(row, order_by: str) -> Optional[Dict]:
    """
    Ligne des archives MA (6 colonnes) :
      [date_label, band_link, country_link, genre, time_label, user_link]
    ex: ["June  1", "<a href='/bands/Foo/123'>Foo</a>", "<a href='/lists/DE'>Germany</a>",
         "Black Metal", "Jun 1st, 02:04", "<a href='/users/bar'>bar</a>"]

    Note : les dates ("June 1", "Jun 1st, 02:04") n'incluent pas l'année → date_str=None,
    la logique d'arrêt incrémental sur date est désactivée, on re-fetch toutes les ~800
    entrées à chaque run (coût négligeable).
    """
    if len(row) < 4:
        return None
    href, name = _extract_link(row[1])
    ma_id = _extract_ma_id(href)
    if not ma_id or not name:
        return None

    country_href, _ = _extract_link(row[2])
    m = re.search(r"/lists/([A-Z]{1,3})", country_href or "")
    cc = m.group(1) if m else _clean(row[2])

    return {
        "ma_id": ma_id,
        "name": name,
        "url": href,
        "country": cc,
        "genre": _clean(row[3]),
        "date_str": None,  # pas d'année dans le format MA → pas de filtre date
        "date_type": order_by,
    }
