import ipaddress
import secrets as _secrets

from flask import Blueprint, request, jsonify
from auth import require_creditfair
from werkzeug.security import generate_password_hash, check_password_hash

from models import (
    db,
    Policy,
    LiveConfig,
    LiveApiLog,
    coerce_bool,
    build_grade_rules,
)

bp = Blueprint('live', __name__)


# ---- helpers ----

def _get_or_create_config():
    config = LiveConfig.query.get(1)
    if not config:
        config = LiveConfig(id=1, enabled=False)
        db.session.add(config)
        db.session.commit()
    return config


def _client_ip():
    """Prefer X-Forwarded-For (first hop) when behind a proxy, else remote_addr."""
    fwd = request.headers.get('X-Forwarded-For', '')
    if fwd:
        return fwd.split(',')[0].strip()
    return request.remote_addr or ''


def _ip_allowed(client_ip, whitelist):
    if not whitelist or whitelist.strip() in ('', '*'):
        return True
    try:
        client = ipaddress.ip_address(client_ip)
    except ValueError:
        return False
    for entry in (e.strip() for e in whitelist.split(',') if e.strip()):
        if entry == '*':
            return True
        try:
            if '/' in entry:
                if client in ipaddress.ip_network(entry, strict=False):
                    return True
            elif client == ipaddress.ip_address(entry):
                return True
        except ValueError:
            continue
    return False


def _log(policy_name, ip, status, error=None):
    try:
        db.session.add(LiveApiLog(
            policy_name=policy_name,
            ip_address=ip,
            status_code=status,
            error=error,
        ))
        db.session.commit()
    except Exception:
        db.session.rollback()


# ---- admin: config ----

@bp.route('/api/live/config', methods=['GET'])
@require_creditfair
def get_live_config():
    config = _get_or_create_config()
    return jsonify(config.to_dict()), 200


@bp.route('/api/live/config', methods=['PUT'])
@require_creditfair
def update_live_config():
    data = request.get_json() or {}
    config = _get_or_create_config()

    if 'apiKey' in data:
        config.api_key = (data.get('apiKey') or '').strip() or None
    if 'ipWhitelist' in data:
        config.ip_whitelist = (data.get('ipWhitelist') or '').strip() or None
    if 'enabled' in data:
        config.enabled = coerce_bool(data['enabled'])

    secret_revealed = None
    if data.get('secret'):
        config.api_secret_hash = generate_password_hash(data['secret'])
        secret_revealed = data['secret']
    elif data.get('generateSecret'):
        new_secret = _secrets.token_urlsafe(32)
        config.api_secret_hash = generate_password_hash(new_secret)
        secret_revealed = new_secret

    db.session.commit()
    return jsonify(config.to_dict(secret_revealed=secret_revealed)), 200


# ---- admin: log ----

@bp.route('/api/live/log', methods=['GET'])
@require_creditfair
def get_live_log():
    logs = (
        LiveApiLog.query
        .order_by(LiveApiLog.requested_at.desc())
        .limit(100)
        .all()
    )
    return jsonify([log.to_dict() for log in logs]), 200


@bp.route('/api/live/log', methods=['DELETE'])
@require_creditfair
def clear_live_log():
    LiveApiLog.query.delete()
    db.session.commit()
    return '', 204


# ---- admin: per-policy live toggle ----

@bp.route('/api/policies/<id>/live', methods=['PUT'])
@require_creditfair
def toggle_policy_live(id):
    policy = Policy.query.get(id)
    if not policy:
        return jsonify({
            "error": "Resource not found",
            "message": f"Policy with id '{id}' not found"
        }), 404

    data = request.get_json() or {}
    if 'isLive' in data:
        policy.is_live = coerce_bool(data['isLive'])
    db.session.commit()
    return jsonify(policy.to_dict()), 200


# ---- public read endpoint ----

@bp.route('/api/live/policy/<policy_id>', methods=['GET'])
def get_live_policy(policy_id):
    config = _get_or_create_config()
    ip = _client_ip()

    if not coerce_bool(config.enabled):
        _log(policy_id, ip, 503, "Live API disabled")
        return jsonify({"error": "Service disabled", "message": "Live API is disabled"}), 503

    if not config.api_key or not config.api_secret_hash:
        _log(policy_id, ip, 503, "Live API not configured")
        return jsonify({"error": "Service disabled", "message": "Live API not configured"}), 503

    api_key = request.headers.get('X-API-Key', '')
    api_secret = request.headers.get('X-API-Secret', '')

    # Compare key first; only then check secret (constant-time via werkzeug).
    if api_key != config.api_key or not check_password_hash(config.api_secret_hash, api_secret):
        _log(policy_id, ip, 401, "Invalid credentials")
        return jsonify({"error": "Unauthorized", "message": "Invalid credentials"}), 401

    if not _ip_allowed(ip, config.ip_whitelist):
        _log(policy_id, ip, 403, f"IP {ip} not whitelisted")
        return jsonify({"error": "Forbidden", "message": "IP not whitelisted"}), 403

    policy = Policy.query.get(policy_id)
    if not policy:
        _log(policy_id, ip, 404, "Policy not found")
        return jsonify({"error": "Not found", "message": f"Policy '{policy_id}' not found"}), 404

    # Now we have the policy — log its human-readable name from here on.
    if not coerce_bool(policy.is_live):
        _log(policy.name, ip, 404, "Policy not live")
        return jsonify({"error": "Not found", "message": f"Policy '{policy_id}' not available"}), 404

    grade = build_grade_rules(policy)
    _log(policy.name, ip, 200)
    return jsonify({"grade": {"rules": grade}}), 200
