"""360Shield Guard — Django
Drop this file in your project root. Add two lines to settings.py. Done.

    # settings.py
    MIDDLEWARE = ['guard_django.ShieldMiddleware', ...]
    SHIELD_AGENT_KEY = "YOUR_AGENT_KEY"
    SHIELD_DOMAIN = "example.com"

The key MUST be an AGENT key (ada_agent_*), NOT a master key (ada_live_*).
Generate one per domain from your dashboard at https://360shield.net/dashboard
("Guard & API" tab) — the dashboard auto-creates a key for each verified domain.

Open source — https://github.com/asimetry/360shield-guard
Version: 1.0.1 | License: MIT | Zero dependencies (stdlib only)
"""

import os, sys, json, time, hashlib, hmac, threading, collections
from datetime import datetime, timezone
from urllib.request import Request, urlopen
from urllib.error import URLError

__version__ = '1.0.3'
_API = 'https://360shield.net/v1/agent'
_HB_INTERVAL = 30
_FLOOD_LIMIT = 50
_FLOOD_WINDOW = 300
_BUFFER_SIZE = 500

# Module-level state (shared across requests)
_buf = collections.deque(maxlen=_BUFFER_SIZE)
_flood = {}
_blocked = set()
_executed_cmd_ids = []
_maintenance = False
_lock = threading.Lock()
_agent_id = None
_booted = False


def _ip(request):
    for h in ('HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'):
        v = request.META.get(h)
        if v:
            return v.split(',')[0].strip()
    return request.META.get('REMOTE_ADDR', '')


def _is_bot(ua):
    ua = ua.lower()
    bots = ('bot', 'crawler', 'spider', 'scraper', 'curl', 'wget',
            'python-requests', 'go-http', 'headless', 'phantom',
            'selenium', 'puppeteer', 'postman', 'httpie')
    return any(b in ua for b in bots) or not ua


def _api_call(key, endpoint, body, sign=False):
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {key}',
        'X-Guard-Version': __version__,
        'User-Agent': f'360Shield-Guard/{__version__}',
    }
    if sign:
        headers['X-Guard-Signature'] = hmac.new(
            key.encode(), body, hashlib.sha256).hexdigest()
    req = Request(_API + endpoint, data=body, headers=headers, method='POST')
    try:
        with urlopen(req, timeout=10) as r:
            return json.loads(r.read())
    except URLError as e:
        print(f'[360Shield] API error: {e}', file=sys.stderr)
        return None


def _boot(key, domain):
    global _agent_id, _booted
    if _booted:
        return
    _booted = True

    def _run():
        global _agent_id
        time.sleep(2)
        # Register
        try:
            body = json.dumps({'domain': domain}).encode()
            resp = _api_call(key, '/register', body)
            if resp and resp.get('agent_id'):
                _agent_id = resp['agent_id']
                print(f'[360Shield] Guard registered: {_agent_id}')
        except Exception as e:
            print(f'[360Shield] register failed: {e}', file=sys.stderr)

        # Heartbeat loop
        while True:
            try:
                _heartbeat(key, domain)
            except Exception as e:
                print(f'[360Shield] heartbeat error: {e}', file=sys.stderr)
            time.sleep(_HB_INTERVAL)

    t = threading.Thread(target=_run, daemon=True)
    t.start()


def _heartbeat(key, domain):
    global _executed_cmd_ids, _maintenance
    reqs = list(_buf)
    now = int(time.time())
    window = [r for r in reqs if now - r['time'] < 300]
    total = len(window)

    # Sample recent_requests for server-side AoM
    sampled = window[-500:] if len(window) > 500 else window
    recent_requests = [{
        'ts': float(r.get('time', 0)),
        'ip': r.get('ip', ''),
        'method': r.get('method', 'GET'),
        'path': r.get('path', '/'),
        'status': r.get('status') or 200,
        'is_bot': bool(r.get('is_bot')),
        'ua': r.get('ua', ''),
    } for r in sampled]

    security = {'platform': 'django'}
    try:
        from django.conf import settings as s
        security['debug_mode'] = getattr(s, 'DEBUG', False)
        security['secret_key_length'] = len(str(getattr(s, 'SECRET_KEY', '')))
        security['csrf_protection'] = 'django.middleware.csrf.CsrfViewMiddleware' in getattr(s, 'MIDDLEWARE', [])
        security['session_cookie_httponly'] = getattr(s, 'SESSION_COOKIE_HTTPONLY', False)
        security['session_cookie_secure'] = getattr(s, 'SESSION_COOKIE_SECURE', False)
        security['session_cookie_samesite'] = getattr(s, 'SESSION_COOKIE_SAMESITE', '')
        security['max_content_length'] = getattr(s, 'DATA_UPLOAD_MAX_MEMORY_SIZE', None)
    except Exception:
        pass

    sent_acks = list(_executed_cmd_ids)
    _executed_cmd_ids = []

    payload = {
        'agent_id': _agent_id or 'pending',
        'domain': domain,
        'guard_version': __version__,
        'timestamp': datetime.now(timezone.utc).isoformat(),
        'traffic': {
            'total_requests': total,
            'bot_requests': sum(1 for r in window if r.get('is_bot')),
            'unique_ips': len(set(r['ip'] for r in window)),
            'status_codes': {},
            'rpm': round(total / 5, 1),
        },
        'recent_requests': recent_requests,
        'security': security,
        'guard_state': {
            'blocked_count': len(_blocked),
            'buffer_size': len(_buf),
            'flood_tracked_ips': len(_flood),
            'maintenance_mode': _maintenance,
            'bot_patterns': 25,
        },
        'acked_command_ids': sent_acks,
    }

    body = json.dumps(payload, default=str).encode()
    resp = _api_call(key, '/heartbeat', body, sign=True)
    if resp:
        for cmd in resp.get('commands', []):
            cid = cmd.get('id')
            action = cmd.get('action', '')
            try:
                if action == 'block_ip' and cmd.get('ip'):
                    _blocked.add(cmd['ip'])
                elif action == 'unblock_ip':
                    _blocked.discard(cmd.get('ip', ''))
                elif action == 'reset_blocked':
                    _blocked.clear()
                elif action == 'set_maintenance':
                    _maintenance = bool(cmd.get('enabled', False))
                elif action == 'force_scan':
                    pass  # no-op; next heartbeat sends fresh data
                if cid is not None:
                    _executed_cmd_ids.append(cid)
            except Exception:
                pass
    else:
        # Heartbeat failed — restore acks for retry
        _executed_cmd_ids = sent_acks + _executed_cmd_ids


class ShieldMiddleware:
    """Django middleware — drop-in Guard protection.

        # settings.py
        MIDDLEWARE = ['guard_django.ShieldMiddleware', ...]
        SHIELD_AGENT_KEY = "ada_agent_xxx"  # or use os.environ
        SHIELD_DOMAIN = "example.com"
    """

    def __init__(self, get_response):
        self.get_response = get_response
        from django.conf import settings
        self.key = getattr(settings, 'SHIELD_AGENT_KEY', None) or os.environ.get('SHIELD_AGENT_KEY', '')
        self.domain = getattr(settings, 'SHIELD_DOMAIN', '') or os.environ.get('SHIELD_DOMAIN', '')
        if not self.key or not self.domain:
            print('[360Shield] WARNING: SHIELD_AGENT_KEY and SHIELD_DOMAIN required. Guard NOT active.', file=sys.stderr)
        else:
            _boot(self.key, self.domain)

    def __call__(self, request):
        from django.http import JsonResponse

        path = request.path
        if path.startswith('/static/') or path == '/favicon.ico':
            return self.get_response(request)

        ip = _ip(request)

        # Blocked?
        if ip in _blocked:
            return JsonResponse({'error': 'blocked'}, status=403)

        # Flood check
        now = int(time.time())
        with _lock:
            ts = _flood.get(ip, [])
            ts = [t for t in ts if now - t < _FLOOD_WINDOW]
            ts.append(now)
            _flood[ip] = ts[-200:]
            if len(ts) >= _FLOOD_LIMIT:
                return JsonResponse({'error': 'rate_limited', 'retry_after': 60}, status=429)

        # Log
        ua = request.META.get('HTTP_USER_AGENT', '')
        entry = {
            'time': now, 'ip': ip, 'method': request.method,
            'path': path[:80], 'ua': ua[:120],
            'is_bot': _is_bot(ua), 'status': None, 'ms': None,
        }
        _buf.append(entry)

        t0 = time.time()
        response = self.get_response(request)

        # Update entry
        entry['status'] = response.status_code
        entry['ms'] = int((time.time() - t0) * 1000)

        # Security headers
        response['X-Content-Type-Options'] = 'nosniff'
        response['X-Frame-Options'] = 'DENY'
        response['X-XSS-Protection'] = '1; mode=block'
        response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
        return response
