import ipaddress
import urllib.request
import json
import asyncio
from datetime import datetime
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from models import GeoIpRule, IpGeoCache, SystemSetting

def get_client_ip(request: Request) -> str:
    """Extrae la IP real del cliente analizando cabeceras de proxy o CDN."""
    headers = [
        "CF-Connecting-IP",
        "X-Real-IP",
        "X-Forwarded-For"
    ]
    for h in headers:
        val = request.headers.get(h)
        if val:
            ip = val.split(",")[0].strip()
            try:
                ipaddress.ip_address(ip)
                return ip
            except ValueError:
                pass
    if request.client and request.client.host:
        return request.client.host
    return "127.0.0.1"

def is_private_ip(ip_str: str) -> bool:
    """Verifica si la IP es local o pertenece a rangos RFC 1918 / loopback."""
    try:
        ip = ipaddress.ip_address(ip_str.strip())
        return ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local
    except ValueError:
        return False

def ip_matches_cidr(ip_str: str, cidr_str: str) -> bool:
    """Verifica si una IP coincide con una IP específica o una red CIDR."""
    ip_str = ip_str.strip()
    cidr_str = cidr_str.strip()
    if ip_str == cidr_str:
        return True
    try:
        if "/" in cidr_str:
            net = ipaddress.ip_network(cidr_str, strict=False)
            ip = ipaddress.ip_address(ip_str)
            return ip in net
        else:
            return ipaddress.ip_address(ip_str) == ipaddress.ip_address(cidr_str)
    except ValueError:
        return False

async def get_ip_country(ip: str, db: AsyncSession) -> dict:
    """Obtiene la geolocalización de una IP con caché persistente en base de datos."""
    ip = ip.strip()
    if is_private_ip(ip):
        return {
            "country_code": "LOCAL",
            "country_name": "Red Local / Privada",
            "city": "Localhost",
            "isp": "LAN"
        }

    # 1. Comprobar caché local
    cached = await db.get(IpGeoCache, ip)
    if cached and cached.country_code:
        return {
            "country_code": cached.country_code,
            "country_name": cached.country_name,
            "city": cached.city or "",
            "isp": cached.isp or ""
        }

    # 2. Consultar servicios externos de geolocalización
    geo_data = {
        "country_code": "XX",
        "country_name": "Desconocido",
        "city": "",
        "isp": ""
    }

    def _fetch_geo():
        apis = [
            f"https://ipapi.co/{ip}/json/",
            f"http://ip-api.com/json/{ip}?fields=status,country,countryCode,city,isp"
        ]
        for url in apis:
            try:
                req = urllib.request.Request(url, headers={"User-Agent": "GeoIP-Firewall-Python/1.0"})
                with urllib.request.urlopen(req, timeout=1.5) as resp:
                    if resp.status == 200:
                        data = json.loads(resp.read().decode("utf-8"))
                        cc = data.get("country_code") or data.get("countryCode")
                        if cc:
                            return {
                                "country_code": str(cc).upper(),
                                "country_name": data.get("country_name") or data.get("country") or str(cc).upper(),
                                "city": data.get("city") or "",
                                "isp": data.get("isp") or data.get("org") or ""
                            }
            except Exception:
                continue
        return geo_data

    try:
        loop = asyncio.get_event_loop()
        res = await loop.run_in_executor(None, _fetch_geo)
        geo_data = res
    except Exception:
        pass

    # 3. Guardar en caché
    try:
        if cached:
            cached.country_code = geo_data["country_code"]
            cached.country_name = geo_data["country_name"]
            cached.city = geo_data["city"]
            cached.isp = geo_data["isp"]
            cached.updated_at = datetime.utcnow()
        else:
            new_cache = IpGeoCache(
                ip=ip,
                country_code=geo_data["country_code"],
                country_name=geo_data["country_name"],
                city=geo_data["city"],
                isp=geo_data["isp"]
            )
            db.add(new_cache)
        await db.commit()
    except Exception:
        await db.rollback()

    return geo_data

async def evaluate_ip_access(ip: str, company_id: int | None, db: AsyncSession) -> tuple[bool, str, dict]:
    """Evalúa si una dirección IP tiene permitido el acceso según las reglas del cortafuegos."""
    ip = ip.strip()

    # Redes locales nunca se bloquean
    if is_private_ip(ip):
        return True, "IP en Red Local / Privada", {
            "country_code": "LOCAL",
            "country_name": "Red Local",
            "city": "Localhost",
            "isp": "LAN"
        }

    # Leer configuración global
    stmt = select(SystemSetting).where(SystemSetting.key.in_([
        "geo_blocking_enabled", "geo_mode", "geo_allow_company_exceptions"
    ]))
    res = await db.execute(stmt)
    settings = {s.key: s.value for s in res.scalars().all()}

    geo_enabled = settings.get("geo_blocking_enabled", "1") == "1"
    geo_mode = settings.get("geo_mode", "whitelist")
    allow_company_exceptions = settings.get("geo_allow_company_exceptions", "0") == "1"

    # 1. Comprobar Lista Blanca de IPs (Bypass directo)
    stmt_wl = select(GeoIpRule.valor).where(
        GeoIpRule.tipo == "ip_whitelist",
        (GeoIpRule.company_id == None) | (GeoIpRule.company_id == company_id)
    )
    wl_res = await db.execute(stmt_wl)
    for rule_ip in wl_res.scalars().all():
        if ip_matches_cidr(ip, rule_ip):
            geo = await get_ip_country(ip, db)
            return True, "IP autorizada en Lista Blanca", geo

    # 2. Comprobar Lista Negra de IPs
    stmt_bl = select(GeoIpRule.valor).where(
        GeoIpRule.tipo == "ip_blacklist",
        (GeoIpRule.company_id == None) | (GeoIpRule.company_id == company_id)
    )
    bl_res = await db.execute(stmt_bl)
    for rule_ip in bl_res.scalars().all():
        if ip_matches_cidr(ip, rule_ip):
            geo = await get_ip_country(ip, db)
            return False, "Dirección IP bloqueada en Lista Negra", geo

    # Si el cortafuegos está desactivado y no está en blacklist, se permite
    geo = await get_ip_country(ip, db)
    if not geo_enabled:
        return True, "Cortafuegos GeoIP desactivado", geo

    country_code = geo["country_code"]
    if country_code == "LOCAL":
        return True, "Red Local", geo

    # 3. Comprobar política de países
    target_type = "country_whitelist" if geo_mode == "whitelist" else "country_blacklist"
    
    if allow_company_exceptions and company_id:
        stmt_c = select(GeoIpRule.valor).where(
            GeoIpRule.tipo == target_type,
            (GeoIpRule.company_id == None) | (GeoIpRule.company_id == company_id)
        )
    else:
        stmt_c = select(GeoIpRule.valor).where(
            GeoIpRule.tipo == target_type,
            GeoIpRule.company_id == None
        )
    
    c_res = await db.execute(stmt_c)
    country_list = [c.upper() for c in c_res.scalars().all()]

    if geo_mode == "whitelist":
        if country_code in country_list:
            return True, f"País ({geo['country_name']}) autorizado en Lista Blanca", geo
        else:
            return False, f"Acceso restringido: País ({geo['country_name']}) no figura en la Lista Blanca", geo
    else:
        if country_code in country_list:
            return False, f"Acceso bloqueado: País ({geo['country_name']}) está en la Lista Negra", geo
        else:
            return True, f"País ({geo['country_name']}) fuera de Lista Negra", geo

async def init_default_geo_rules(db: AsyncSession):
    """Inicializa la configuración por defecto y la regla de Chile si la tabla está vacía."""
    stmt = select(GeoIpRule).where(GeoIpRule.tipo == "country_whitelist")
    res = await db.execute(stmt)
    if not res.scalars().first():
        default_rule = GeoIpRule(
            company_id=None,
            tipo="country_whitelist",
            valor="CL",
            pais_nombre="Chile",
            descripcion="Regla de acceso inicial por defecto",
            creado_por="sistema"
        )
        db.add(default_rule)
        await db.commit()
