from datetime import datetime, timedelta
from flask import request
from ipaddress import ip_network, ip_address
from typing import Optional

from app.models.setting import Setting
from app.extensions import db

# Claves de ajustes
RATE_KEYS = {
    'rate.auth.login': '10/minute; 50/hour',
    'rate.auth.register': '5/minute; 20/hour',
    'rate.auth.reset_request': '5/minute; 20/hour',
    'rate.auth.reset': '10/minute; 50/hour',
}

STRICT_KEYS = {
    'ratelimit.strict.auth.login': '5/minute; 20/hour',
    'ratelimit.strict.auth.register': '2/minute; 10/hour',
    'ratelimit.strict.auth.reset_request': '2/minute; 10/hour',
    'ratelimit.strict.auth.reset': '5/minute; 20/hour',
}

STRICT_ENABLED_KEY = 'ratelimit.strict.enabled'
STRICT_EXPIRES_AT_KEY = 'ratelimit.strict.expires_at'  # ISO8601
WHITELIST_KEY = 'ratelimit.whitelist_ips'  # JSON array


def _is_strict_active() -> bool:
    """Devuelve True si el modo estricto está habilitado y no ha expirado."""
    enabled = (Setting.get_value(STRICT_ENABLED_KEY, 'false') or '').lower() in ('1', 'true', 'yes')
    if not enabled:
        return False
    expires_iso = Setting.get_value(STRICT_EXPIRES_AT_KEY)
    if not expires_iso:
        return True
    try:
        expires_at = datetime.fromisoformat(expires_iso)
    except Exception:
        return True
    if datetime.utcnow() >= expires_at:
        # Expirado: desactivar automáticamente
        Setting.set_value(STRICT_ENABLED_KEY, 'false')
        Setting.set_value(STRICT_EXPIRES_AT_KEY, '')
        db.session.commit()
        return False
    return True


def enable_strict_mode(minutes: int = 60):
    expires_at = datetime.utcnow() + timedelta(minutes=max(1, minutes))
    Setting.set_value(STRICT_ENABLED_KEY, 'true')
    Setting.set_value(STRICT_EXPIRES_AT_KEY, expires_at.isoformat())
    db.session.commit()


def disable_strict_mode():
    Setting.set_value(STRICT_ENABLED_KEY, 'false')
    Setting.set_value(STRICT_EXPIRES_AT_KEY, '')
    db.session.commit()


def get_rate_limit(key: str, default: Optional[str] = None) -> str:
    """Obtiene el valor de rate limit para la clave dada, considerando modo estricto."""
    # Strict override
    if _is_strict_active():
        strict_key = None
        # Mapear 'rate.auth.login' -> 'ratelimit.strict.auth.login'
        if key.startswith('rate.'):
            strict_key = 'ratelimit.strict.' + key.split('rate.', 1)[1]
        if strict_key and strict_key in STRICT_KEYS:
            return Setting.get_value(strict_key, STRICT_KEYS[strict_key])
    # Normal
    return Setting.get_value(key, RATE_KEYS.get(key, default or '10/minute'))


def get_whitelist() -> list:
    lst = Setting.get_json(WHITELIST_KEY, default=[])
    return lst or []


def is_ip_whitelisted(ip: str) -> bool:
    """Comprueba si una IP está en la whitelist (admite exacta o CIDR)."""
    try:
        ip_obj = ip_address(ip)
    except Exception:
        return False
    for item in get_whitelist():
        try:
            if '/' in item:
                if ip_obj in ip_network(item, strict=False):
                    return True
            else:
                if ip_obj == ip_address(item):
                    return True
        except Exception:
            continue
    return False


def exempt_when_request_whitelisted() -> bool:
    ip = request.remote_addr or ''
    return is_ip_whitelisted(ip)


def bootstrap_default_limits_if_missing():
    """Inicializa claves por defecto si no existen (idempotente)."""
    changed = False
    for k, v in RATE_KEYS.items():
        if Setting.get_value(k) is None:
            Setting.set_value(k, v)
            changed = True
    for k, v in STRICT_KEYS.items():
        if Setting.get_value(k) is None:
            Setting.set_value(k, v)
            changed = True
    if Setting.get_value(WHITELIST_KEY) is None:
        Setting.set_json(WHITELIST_KEY, [])
        changed = True
    if Setting.get_value(STRICT_ENABLED_KEY) is None:
        Setting.set_value(STRICT_ENABLED_KEY, 'false')
        changed = True
    if Setting.get_value(STRICT_EXPIRES_AT_KEY) is None:
        Setting.set_value(STRICT_EXPIRES_AT_KEY, '')
        changed = True
    if changed:
        db.session.commit()


def strict_status():
    """Devuelve estado del modo estricto y minutos restantes si aplica."""
    active = _is_strict_active()
    minutes_left = None
    if active:
        try:
            expires_at = datetime.fromisoformat(Setting.get_value(STRICT_EXPIRES_AT_KEY))
            delta = expires_at - datetime.utcnow()
            minutes_left = max(0, int(delta.total_seconds() // 60))
        except Exception:
            minutes_left = None
    return {
        'active': active,
        'minutes_left': minutes_left
    }
