"""
Fonctions d'entrée/sortie pour PriceWatch.

Gère la lecture de la configuration YAML et l'écriture des résultats JSON.
"""

import json
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, Field, field_validator

from pricewatch.app.core.logging import get_logger
from pricewatch.app.core.schema import ProductSnapshot

logger = get_logger("core.io")


class ScrapingOptions(BaseModel):
    """Options de scraping depuis le fichier YAML."""

    use_playwright: bool = Field(
        default=True, description="Utiliser Playwright en fallback"
    )
    force_playwright: bool = Field(
        default=False, description="Forcer Playwright même si HTTP réussi"
    )
    headful: bool = Field(default=False, description="Mode headful (voir le navigateur)")
    save_html: bool = Field(
        default=True, description="Sauvegarder HTML pour debug"
    )
    save_screenshot: bool = Field(
        default=True, description="Sauvegarder screenshot pour debug"
    )
    timeout_ms: int = Field(
        default=60000, description="Timeout par page en millisecondes", ge=1000
    )


class ScrapingConfig(BaseModel):
    """Configuration complète du scraping depuis YAML."""

    urls: list[str] = Field(description="Liste des URLs à scraper")
    options: ScrapingOptions = Field(
        default_factory=ScrapingOptions, description="Options de scraping"
    )

    @field_validator("urls")
    @classmethod
    def validate_urls(cls, v: list[str]) -> list[str]:
        """Valide et nettoie les URLs."""
        if not v:
            raise ValueError("Au moins une URL doit être fournie")

        cleaned = [url.strip() for url in v if url and url.strip()]
        if not cleaned:
            raise ValueError("Aucune URL valide trouvée")

        return cleaned


def read_yaml_config(yaml_path: str | Path) -> ScrapingConfig:
    """
    Lit et valide le fichier YAML de configuration.

    Args:
        yaml_path: Chemin vers le fichier YAML

    Returns:
        Configuration validée

    Raises:
        FileNotFoundError: Si le fichier n'existe pas
        ValueError: Si le YAML est invalide

    Justification technique:
    - Utilisation de Pydantic pour valider la structure YAML
    - Cela évite des bugs si le fichier est mal formé
    - Les erreurs sont explicites pour l'utilisateur
    """
    yaml_path = Path(yaml_path)

    if not yaml_path.exists():
        logger.error(f"Fichier YAML introuvable: {yaml_path}")
        raise FileNotFoundError(f"Fichier YAML introuvable: {yaml_path}")

    logger.info(f"Lecture configuration: {yaml_path}")

    try:
        with open(yaml_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)

        if not data:
            raise ValueError("Fichier YAML vide")

        config = ScrapingConfig.model_validate(data)
        logger.info(
            f"Configuration chargée: {len(config.urls)} URL(s), "
            f"playwright={config.options.use_playwright}, "
            f"force_playwright={config.options.force_playwright}"
        )
        return config

    except yaml.YAMLError as e:
        logger.error(f"Erreur parsing YAML: {e}")
        raise ValueError(f"YAML invalide: {e}") from e
    except Exception as e:
        logger.error(f"Erreur validation config: {e}")
        raise


def write_json_results(
    snapshots: list[ProductSnapshot], json_path: str | Path, indent: int = 2
) -> None:
    """
    Écrit les résultats du scraping dans un fichier JSON.

    Args:
        snapshots: Liste des ProductSnapshot à sauvegarder
        json_path: Chemin du fichier JSON de sortie
        indent: Indentation pour lisibilité (None = compact)

    Justification technique:
    - Serialization via Pydantic pour garantir la structure
    - Pretty-print par défaut (indent=2) pour faciliter le debug manuel
    - Création automatique des dossiers parents si nécessaire
    """
    json_path = Path(json_path)

    # Créer le dossier parent si nécessaire
    json_path.parent.mkdir(parents=True, exist_ok=True)

    logger.info(f"Écriture de {len(snapshots)} snapshot(s) dans: {json_path}")

    try:
        # Serialization via Pydantic
        data = [snapshot.model_dump(mode="json") for snapshot in snapshots]

        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=indent, ensure_ascii=False)

        logger.info(f"Résultats sauvegardés: {json_path} ({json_path.stat().st_size} bytes)")

    except Exception as e:
        logger.error(f"Erreur écriture JSON: {e}")
        raise


def read_json_results(json_path: str | Path) -> list[ProductSnapshot]:
    """
    Lit et valide un fichier JSON de résultats.

    Args:
        json_path: Chemin vers le fichier JSON

    Returns:
        Liste de ProductSnapshot validés

    Raises:
        FileNotFoundError: Si le fichier n'existe pas
        ValueError: Si le JSON est invalide
    """
    json_path = Path(json_path)

    if not json_path.exists():
        logger.error(f"Fichier JSON introuvable: {json_path}")
        raise FileNotFoundError(f"Fichier JSON introuvable: {json_path}")

    logger.info(f"Lecture résultats: {json_path}")

    try:
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        if not isinstance(data, list):
            raise ValueError("Le JSON doit contenir une liste")

        snapshots = [ProductSnapshot.model_validate(item) for item in data]
        logger.info(f"{len(snapshots)} snapshot(s) chargé(s)")
        return snapshots

    except json.JSONDecodeError as e:
        logger.error(f"Erreur parsing JSON: {e}")
        raise ValueError(f"JSON invalide: {e}") from e
    except Exception as e:
        logger.error(f"Erreur validation snapshots: {e}")
        raise


def save_debug_html(html: str, filename: str, output_dir: str | Path = "scraped") -> Path:
    """
    Sauvegarde le HTML récupéré pour debug.

    Args:
        html: Contenu HTML
        filename: Nom du fichier (sans extension)
        output_dir: Dossier de sortie

    Returns:
        Chemin du fichier sauvegardé
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    filepath = output_dir / f"{filename}.html"

    with open(filepath, "w", encoding="utf-8") as f:
        f.write(html)

    logger.debug(f"HTML sauvegardé: {filepath} ({len(html)} chars)")
    return filepath


def save_debug_screenshot(
    screenshot_bytes: bytes, filename: str, output_dir: str | Path = "scraped"
) -> Path:
    """
    Sauvegarde un screenshot pour debug.

    Args:
        screenshot_bytes: Données binaires du screenshot
        filename: Nom du fichier (sans extension)
        output_dir: Dossier de sortie

    Returns:
        Chemin du fichier sauvegardé
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    filepath = output_dir / f"{filename}.png"

    with open(filepath, "wb") as f:
        f.write(screenshot_bytes)

    logger.debug(f"Screenshot sauvegardé: {filepath} ({len(screenshot_bytes)} bytes)")
    return filepath
