"""
Endpoints API pour la gestion des IPs
"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import List, Optional
from datetime import datetime, timedelta
import xml.etree.ElementTree as ET
import yaml
from pathlib import Path
import re
import time
import urllib.request

from backend.app.core.database import get_db
from backend.app.models.ip import IP, IPHistory
from backend.app.core.config import config_manager
from pydantic import BaseModel

router = APIRouter(prefix="/api/ips", tags=["IPs"])

ICONS_DIR = Path("./data/icons")
ALLOWED_ICON_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".svg"}
OUI_URL = "https://standards-oui.ieee.org/oui/oui.txt"
OUI_PATH = Path("./data/oui/oui.txt")


def _sanitize_filename(filename: str) -> str:
    name = Path(filename).name
    name = re.sub(r"[^A-Za-z0-9._-]+", "_", name)
    if not name or name in {".", ".."}:
        return f"icon_{int(time.time())}.png"
    if "." not in name:
        return f"{name}.png"
    return name


@router.get("/oui/status")
async def oui_status():
    """
    Statut du fichier OUI local
    """
    if not OUI_PATH.exists():
        return {"exists": False, "updated_at": None}
    updated_at = datetime.fromtimestamp(OUI_PATH.stat().st_mtime)
    return {"exists": True, "updated_at": updated_at.isoformat()}


@router.post("/oui/update")
async def update_oui(db: Session = Depends(get_db)):
    """
    Télécharge le fichier OUI et met à jour les fabricants inconnus
    """
    OUI_PATH.parent.mkdir(parents=True, exist_ok=True)
    try:
        request = urllib.request.Request(
            OUI_URL,
            headers={
                "User-Agent": "IPWatch/1.0 (+https://ipwatch.local)"
            }
        )
        with urllib.request.urlopen(request) as response:
            OUI_PATH.write_bytes(response.read())
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur téléchargement OUI: {str(e)}")

    # Mettre à jour les vendors inconnus dans la DB
    from backend.app.services.network import OuiLookup

    updated = 0
    ips = db.query(IP).filter(IP.mac.isnot(None)).all()
    for ip in ips:
        if ip.vendor and ip.vendor not in {"Unknown", ""}:
            continue
        vendor = OuiLookup.lookup(ip.mac)
        if vendor:
            ip.vendor = vendor
            updated += 1
    db.commit()

    return {"message": "Liste OUI mise à jour", "updated_vendors": updated}


@router.get("/icons")
async def list_icons():
    """
    Liste les icônes disponibles dans le dossier partagé
    """
    ICONS_DIR.mkdir(parents=True, exist_ok=True)
    files = []
    for path in ICONS_DIR.iterdir():
        if path.is_file() and path.suffix.lower() in ALLOWED_ICON_EXTENSIONS:
            files.append(path.name)
    return {"icons": sorted(files)}


@router.post("/icons/upload")
async def upload_icon(file: UploadFile = File(...)):
    """
    Upload d'une icône dans le dossier partagé
    """
    ICONS_DIR.mkdir(parents=True, exist_ok=True)
    filename = _sanitize_filename(file.filename or "")
    ext = Path(filename).suffix.lower()
    if ext not in ALLOWED_ICON_EXTENSIONS:
        raise HTTPException(status_code=400, detail="Format d'image non supporté")

    target = ICONS_DIR / filename

    try:
        content = await file.read()
        target.write_bytes(content)
        return {
            "filename": target.name,
            "url": f"/icons/{target.name}"
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur upload: {str(e)}")





# Schémas Pydantic pour validation
class IPUpdate(BaseModel):
    """Schéma pour mise à jour d'IP"""
    name: Optional[str] = None
    known: Optional[bool] = None
    tracked: Optional[bool] = None
    vm: Optional[bool] = None
    hardware_bench: Optional[bool] = None
    network_device: Optional[bool] = None
    location: Optional[str] = None
    host: Optional[str] = None
    link: Optional[str] = None
    last_status: Optional[str] = None
    mac: Optional[str] = None
    vendor: Optional[str] = None
    hostname: Optional[str] = None
    mac_changed: Optional[bool] = None
    open_ports: Optional[List[int]] = None
    first_seen: Optional[datetime] = None
    last_seen: Optional[datetime] = None
    icon_filename: Optional[str] = None
    icon_url: Optional[str] = None
    ip_parent: Optional[str] = None
    ip_enfant: Optional[List[str]] = None
    dhcp_synced: Optional[bool] = None


class IPResponse(BaseModel):
    """Schéma de réponse IP"""
    ip: str
    name: Optional[str]
    known: bool
    tracked: Optional[bool] = False
    vm: Optional[bool] = False
    hardware_bench: Optional[bool] = False
    network_device: Optional[bool] = False
    location: Optional[str]
    host: Optional[str]
    first_seen: Optional[datetime]
    last_seen: Optional[datetime]
    last_status: Optional[str]
    mac: Optional[str]
    vendor: Optional[str]
    hostname: Optional[str]
    link: Optional[str]
    mac_changed: Optional[bool] = False
    open_ports: List[int]
    icon_filename: Optional[str]
    icon_url: Optional[str]
    ip_parent: Optional[str]
    ip_enfant: List[str] = []
    dhcp_synced: Optional[bool] = False

    class Config:
        from_attributes = True


class IPHistoryResponse(BaseModel):
    """Schéma de réponse historique"""
    id: int
    ip: str
    timestamp: datetime
    status: str
    open_ports: List[int]

    class Config:
        from_attributes = True


@router.get("/", response_model=List[IPResponse])
async def get_all_ips(
    status: Optional[str] = None,
    known: Optional[bool] = None,
    db: Session = Depends(get_db)
):
    """
    Récupère toutes les IPs avec filtres optionnels

    Args:
        status: Filtrer par statut (online/offline)
        known: Filtrer par IPs connues/inconnues
        db: Session de base de données

    Returns:
        Liste des IPs
    """
    query = db.query(IP)

    if status:
        query = query.filter(IP.last_status == status)

    if known is not None:
        query = query.filter(IP.known == known)

    ips = query.all()
    return ips


@router.get("/{ip_address}", response_model=IPResponse)
async def get_ip(ip_address: str, db: Session = Depends(get_db)):
    """
    Récupère les détails d'une IP spécifique

    Args:
        ip_address: Adresse IP
        db: Session de base de données

    Returns:
        Détails de l'IP
    """
    ip = db.query(IP).filter(IP.ip == ip_address).first()

    if not ip:
        raise HTTPException(status_code=404, detail="IP non trouvée")

    return ip


@router.put("/{ip_address}", response_model=IPResponse)
async def update_ip(
    ip_address: str,
    ip_update: IPUpdate,
    db: Session = Depends(get_db)
):
    """
    Met à jour les informations d'une IP

    Args:
        ip_address: Adresse IP
        ip_update: Données à mettre à jour
        db: Session de base de données

    Returns:
        IP mise à jour
    """
    ip = db.query(IP).filter(IP.ip == ip_address).first()

    if not ip:
        raise HTTPException(status_code=404, detail="IP non trouvée")

    # Mettre à jour les champs fournis
    update_data = ip_update.dict(exclude_unset=True)
    old_parent = ip.ip_parent
    new_parent = update_data.get("ip_parent", old_parent)
    for field, value in update_data.items():
        setattr(ip, field, value)

    # Mettre à jour automatiquement network_device si host change
    if 'host' in update_data:
        ip.network_device = (update_data['host'] == 'Network')

    if "ip_enfant" in update_data and update_data["ip_enfant"] is not None:
        ip.ip_enfant = update_data["ip_enfant"]

    if new_parent != old_parent:
        if old_parent:
            parent = db.query(IP).filter(IP.ip == old_parent).first()
            if parent and parent.ip_enfant:
                parent.ip_enfant = [child for child in parent.ip_enfant if child != ip.ip]
        if new_parent:
            parent = db.query(IP).filter(IP.ip == new_parent).first()
            if parent:
                current_children = parent.ip_enfant or []
                if ip.ip not in current_children:
                    parent.ip_enfant = current_children + [ip.ip]

    db.commit()
    db.refresh(ip)

    return ip


@router.delete("/{ip_address}")
async def delete_ip(ip_address: str, db: Session = Depends(get_db)):
    """
    Supprime une IP (et son historique)

    Args:
        ip_address: Adresse IP
        db: Session de base de données

    Returns:
        Message de confirmation
    """
    ip = db.query(IP).filter(IP.ip == ip_address).first()

    if not ip:
        raise HTTPException(status_code=404, detail="IP non trouvée")

    db.delete(ip)
    db.commit()

    return {"message": f"IP {ip_address} supprimée"}


@router.get("/{ip_address}/history", response_model=List[IPHistoryResponse])
async def get_ip_history(
    ip_address: str,
    hours: int = 24,
    db: Session = Depends(get_db)
):
    """
    Récupère l'historique d'une IP

    Args:
        ip_address: Adresse IP
        hours: Nombre d'heures d'historique (défaut: 24h)
        db: Session de base de données

    Returns:
        Liste des événements historiques
    """
    # Vérifier que l'IP existe
    ip = db.query(IP).filter(IP.ip == ip_address).first()
    if not ip:
        raise HTTPException(status_code=404, detail="IP non trouvée")

    # Calculer la date limite
    since = datetime.now() - timedelta(hours=hours)

    # Récupérer l'historique
    history = db.query(IPHistory).filter(
        IPHistory.ip == ip_address,
        IPHistory.timestamp >= since
    ).order_by(desc(IPHistory.timestamp)).all()

    return history


@router.delete("/{ip_address}/history")
async def delete_ip_history(ip_address: str, db: Session = Depends(get_db)):
    """
    Supprime l'historique d'une IP (sans supprimer l'IP elle-même)

    Args:
        ip_address: Adresse IP
        db: Session de base de données

    Returns:
        Message de confirmation avec nombre d'entrées supprimées
    """
    # Vérifier que l'IP existe
    ip = db.query(IP).filter(IP.ip == ip_address).first()
    if not ip:
        raise HTTPException(status_code=404, detail="IP non trouvée")

    # Supprimer tout l'historique de cette IP
    deleted_count = db.query(IPHistory).filter(IPHistory.ip == ip_address).delete()
    db.commit()

    return {"message": f"Historique de {ip_address} supprimé", "deleted_count": deleted_count}


@router.get("/stats/summary")
async def get_stats(db: Session = Depends(get_db)):
    """
    Récupère les statistiques globales du réseau

    Returns:
        Statistiques (total, online, offline, known, unknown)
    """
    total = db.query(IP).count()
    online = db.query(IP).filter(IP.last_status == "online").count()
    offline = db.query(IP).filter(IP.last_status == "offline").count()
    known = db.query(IP).filter(IP.known == True).count()
    unknown = db.query(IP).filter(IP.known == False).count()

    return {
        "total": total,
        "online": online,
        "offline": offline,
        "known": known,
        "unknown": unknown
    }


@router.get("/config/options")
async def get_config_options():
    """
    Récupère les options de configuration (locations, hosts, port_protocols, version, subnets)

    Returns:
        Dictionnaire avec locations, hosts, port_protocols, subnets et version
    """
    config = config_manager.config

    # Récupérer les protocoles de ports depuis la config
    port_protocols = {}
    if hasattr(config.ports, 'protocols') and config.ports.protocols:
        port_protocols = config.ports.protocols

    # Récupérer les subnets
    subnets = []
    if hasattr(config, 'subnets') and config.subnets:
        subnets = [
            {
                "name": s.name,
                "cidr": s.cidr,
                "start": s.start,
                "end": s.end,
                "description": s.description
            }
            for s in config.subnets
        ]

    return {
        "locations": config.locations,
        "hosts": [{"name": h.name, "location": h.location} for h in config.hosts],
        "port_protocols": port_protocols,
        "subnets": subnets,
        "version": config.app.version,
        "hardware_bench_url": getattr(config.links, "hardware_bench_url", None),
        "force_vendor_update": getattr(config.scan, "force_vendor_update", False)
    }


class HardwareBenchConfig(BaseModel):
    """Schéma pour mise à jour du lien hardware bench"""
    url: Optional[str] = None


class ForceVendorConfig(BaseModel):
    """Schéma pour mise à jour du mode force fabricant"""
    enabled: bool = False


@router.post("/config/hardware-bench")
async def update_hardware_bench(config_update: HardwareBenchConfig):
    """
    Met à jour l'URL hardware bench dans config.yaml

    Returns:
        Message de confirmation
    """
    config_path = "./config.yaml"

    try:
        with open(config_path, "r", encoding="utf-8") as f:
            yaml_data = yaml.safe_load(f) or {}

        if "links" not in yaml_data or yaml_data["links"] is None:
            yaml_data["links"] = {}

        url = (config_update.url or "").strip()
        yaml_data["links"]["hardware_bench_url"] = url if url else None

        with open(config_path, "w", encoding="utf-8") as f:
            yaml.safe_dump(yaml_data, f, allow_unicode=True, sort_keys=False)

        config_manager.reload_config()
        return {"message": "Lien hardware bench mis à jour"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur mise à jour config: {str(e)}")


@router.post("/config/force-vendor")
async def update_force_vendor(config_update: ForceVendorConfig):
    """
    Active/désactive le mode force pour le fabricant
    """
    config_path = "./config.yaml"

    try:
        with open(config_path, "r", encoding="utf-8") as f:
            yaml_data = yaml.safe_load(f) or {}

        if "scan" not in yaml_data or yaml_data["scan"] is None:
            yaml_data["scan"] = {}

        yaml_data["scan"]["force_vendor_update"] = bool(config_update.enabled)

        with open(config_path, "w", encoding="utf-8") as f:
            yaml.safe_dump(yaml_data, f, allow_unicode=True, sort_keys=False)

        config_manager.reload_config()
        return {"message": "Mode force fabricant mis à jour"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur mise à jour config: {str(e)}")


@router.get("/config/content")
async def get_config_content():
    """
    Récupère le contenu brut du fichier config.yaml

    Returns:
        Contenu du fichier YAML
    """
    try:
        with open("./config.yaml", "r", encoding="utf-8") as f:
            content = f.read()
        return {"content": content}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur lecture config: {str(e)}")


@router.post("/config/reload")
async def reload_config():
    """
    Recharge la configuration depuis config.yaml

    Returns:
        Message de confirmation
    """
    try:
        config_manager.reload_config()
        return {"message": "Configuration rechargée avec succès"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur rechargement config: {str(e)}")


@router.post("/import/ipscan")
async def import_ipscan(file: UploadFile = File(...), db: Session = Depends(get_db)):
    """
    Importe les données depuis un fichier XML Angry IP Scanner

    Args:
        file: Fichier XML uploadé
        db: Session de base de données

    Returns:
        Statistiques d'import
    """
    if not file.filename.endswith('.xml'):
        raise HTTPException(status_code=400, detail="Le fichier doit être un XML")

    try:
        # Lire le contenu du fichier
        content = await file.read()

        # Essayer de parser le XML avec récupération d'erreurs
        try:
            root = ET.fromstring(content)
        except ET.ParseError as e:
            # Si le parsing échoue, essayer de nettoyer le contenu
            import re
            content_str = content.decode('utf-8', errors='ignore')

            # Supprimer les caractères de contrôle invalides (sauf tab, CR, LF)
            content_str = ''.join(char for char in content_str
                                 if ord(char) >= 32 or char in '\t\r\n')

            try:
                root = ET.fromstring(content_str.encode('utf-8'))
            except ET.ParseError:
                raise HTTPException(status_code=400, detail=f"Fichier XML invalide même après nettoyage: {str(e)}")

        imported = 0
        updated = 0
        errors = []

        # Parser chaque host
        for host in root.findall('.//host'):
            try:
                # Extraire l'adresse IP
                ip_address = host.get('address')
                if not ip_address:
                    continue

                # Extraire les informations
                hostname = None
                mac = None
                vendor = None
                ports = []

                for result in host.findall('result'):
                    name = result.get('name')
                    value = result.text.strip() if result.text else ""

                    # Nettoyer les valeurs [n/a]
                    if value == "[n/a]":
                        value = None

                    if name == "Nom d'hôte" and value:
                        hostname = value
                    elif name == "Adresse MAC" and value:
                        mac = value
                    elif name == "Constructeur MAC" and value:
                        vendor = value
                    elif name == "Ports" and value:
                        # Parser les ports (format: "22,80,443")
                        try:
                            ports = [int(p.strip()) for p in value.split(',') if p.strip().isdigit()]
                        except Exception as e:
                            ports = []

                # Vérifier si l'IP existe déjà
                existing_ip = db.query(IP).filter(IP.ip == ip_address).first()

                if existing_ip:
                    # Mettre à jour avec de nouvelles informations
                    if hostname:
                        if not existing_ip.hostname:
                            existing_ip.hostname = hostname
                        if not existing_ip.name:
                            existing_ip.name = hostname
                    if mac and not existing_ip.mac:
                        existing_ip.mac = mac
                    # Toujours mettre à jour vendor et ports depuis IPScan (plus complet et à jour)
                    if vendor:
                        existing_ip.vendor = vendor
                    if ports:
                        existing_ip.open_ports = ports
                    existing_ip.last_status = "online"
                    existing_ip.last_seen = datetime.now()
                    updated += 1
                else:
                    # Créer une nouvelle entrée
                    new_ip = IP(
                        ip=ip_address,
                        name=hostname,
                        hostname=hostname,
                        mac=mac,
                        vendor=vendor,
                        open_ports=ports or [],
                        last_status="online",
                        known=False,
                        first_seen=datetime.now(),
                        last_seen=datetime.now()
                    )
                    db.add(new_ip)
                    imported += 1

            except Exception as e:
                errors.append(f"Erreur pour {ip_address}: {str(e)}")
                continue

        # Commit des changements
        db.commit()

        return {
            "message": "Import terminé",
            "imported": imported,
            "updated": updated,
            "errors": errors[:10]  # Limiter à 10 erreurs
        }

    except ET.ParseError as e:
        raise HTTPException(status_code=400, detail=f"Fichier XML invalide: {str(e)}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erreur import: {str(e)}")
