"""
IP address management and access control for RateLoader HTTP server.

Tracks connecting IP addresses, enforces access rules, and provides
management interface for IP-based access control.
"""

import json
import os
import time
import threading
import shutil
from typing import Dict, Any, Optional
from datetime import datetime
import ipaddress

from core.config import get_config
from core.logger import get_logger_manager


class IPManager:
    """Manages IP address access control for RateLoader HTTP server."""
    
    def __init__(self, ip_file: str = "ip_access.json"):
        """
        Initialize IP manager with storage file.
        
        Args:
            ip_file: Path to IP access data file
        """
        self.config = get_config()
        self.logger_manager = get_logger_manager()
        self.logger = self.logger_manager.get_logger("IPMANAGER")
        self.ip_file = ip_file
        self.data = {}
        self.lock = threading.RLock()
        self._load_data()
    
    def _load_data(self):
        """Load IP data from file or create default structure."""
        try:
            with self.lock:
                if os.path.exists(self.ip_file):
                    try:
                        with open(self.ip_file, 'r', encoding='utf-8') as f:
                            self.data = json.load(f)
                        self.logger.info(f"IP data loaded from {self.ip_file}")
                    except (json.JSONDecodeError, IOError) as e:
                        self.logger.error(f"Error loading IP data from {self.ip_file}: {str(e)}")
                        self._create_backup_and_default()
                else:
                    self.logger.info(f"IP data file {self.ip_file} not found, creating default")
                    self._create_default_data()
        except Exception as e:
            self.logger.error(f"Unexpected error loading IP data: {str(e)}")
            self._create_default_data()
    
    def _create_backup_and_default(self):
        """Create backup of corrupted file and create default data."""
        try:
            if os.path.exists(self.ip_file):
                backup_file = f"{self.ip_file}.backup.{int(time.time())}"
                shutil.copy2(self.ip_file, backup_file)
                self.logger.info(f"Created backup of corrupted IP file: {backup_file}")
        except Exception as e:
            self.logger.error(f"Failed to create backup: {str(e)}")
        
        self._create_default_data()
    
    def _create_default_data(self):
        """Create default IP data structure."""
        with self.lock:
            self.data = {
                "global_access": "allowed",  # "allowed" or "restricted"
                "ips": {}
            }
            self._save_data()
    
    def _save_data(self):
        """Save IP data to file with backup."""
        try:
            # Create backup before saving
            if os.path.exists(self.ip_file):
                backup_file = f"{self.ip_file}.backup"
                shutil.copy2(self.ip_file, backup_file)
            
            # Write new data
            with open(self.ip_file, 'w', encoding='utf-8') as f:
                json.dump(self.data, f, indent=2, ensure_ascii=False)
            
            self.logger.debug(f"IP data saved to {self.ip_file}")
        except Exception as e:
            self.logger.error(f"Error saving IP data to {self.ip_file}: {str(e)}")
    
    def is_ip_allowed(self, ip_address: str) -> bool:
        """
        Check if IP address is allowed to access the server.
        
        Args:
            ip_address: IP address to check
            
        Returns:
            True if IP is allowed, False otherwise
        """
        try:
            # Validate IP address format
            if not self._validate_ip(ip_address):
                self.logger.warning(f"Invalid IP address format: {ip_address}")
                return False
            
            with self.lock:
                global_access = self.data.get('global_access', 'allowed')
                ips = self.data.get('ips', {})
                
                ip_data = ips.get(ip_address, {})
                ip_status = ip_data.get('status', 'allowed')
                
                # Apply access control logic
                if global_access == "restricted":
                    # Whitelist mode: only explicitly allowed IPs can access
                    # New IPs (not in list) should be denied in restricted mode
                    if ip_address not in ips:
                        allowed = False
                    else:
                        allowed = ip_status == "allowed"
                else:  # global_access == "allowed"
                    # Blacklist mode: all IPs can access unless explicitly restricted
                    # New IPs (not in list) should be allowed in allowed mode
                    if ip_address not in ips:
                        allowed = True
                    else:
                        allowed = ip_status != "restricted"
                
                # Log denied requests if configured
                if not allowed and self.config.get_bool('log_denied_requests', fallback=True):
                    self.logger.warning(f"Access denied for IP {ip_address} (global: {global_access}, ip: {ip_status})")
                
                return allowed
                
        except Exception as e:
            self.logger.error(f"Error checking IP access for {ip_address}: {str(e)}")
            # Fail open - allow access if there's an error
            return True
    
    def record_request(self, ip_address: str, request_type: str):
        """
        Record a request from an IP address.
        
        Args:
            ip_address: IP address making the request
            request_type: Type of request (GET, POST, etc.)
        """
        try:
            if not self._validate_ip(ip_address):
                return
            
            with self.lock:
                ips = self.data.get('ips', {})
                ip_data = ips.get(ip_address, {})
                
                current_time = datetime.utcnow().isoformat() + 'Z'
                
                # Update IP data
                if not ip_data:
                    # New IP - add with default values
                    # In restricted mode, new IPs should default to restricted
                    global_access = self.data.get('global_access', 'allowed')
                    default_status = "restricted" if global_access == "restricted" else "allowed"
                    
                    ip_data = {
                        "status": default_status,
                        "first_seen": current_time,
                        "last_seen": current_time,
                        "request_count": 1,
                        "last_request_type": request_type
                    }
                    self.logger.info(f"New IP address added: {ip_address} with status: {default_status}")
                else:
                    # Existing IP - update metadata
                    ip_data["last_seen"] = current_time
                    ip_data["request_count"] = ip_data.get("request_count", 0) + 1
                    ip_data["last_request_type"] = request_type
                
                ips[ip_address] = ip_data
                self.data["ips"] = ips
                
                # Enforce IP limit if configured
                self._enforce_ip_limit()
                
                # Save data
                self._save_data()
                
        except Exception as e:
            self.logger.error(f"Error recording request from {ip_address}: {str(e)}")
    
    def set_ip_status(self, ip_address: str, status: str):
        """
        Set the status of an IP address.
        
        Args:
            ip_address: IP address to update
            status: New status ("allowed" or "restricted")
        """
        try:
            if not self._validate_ip(ip_address):
                raise ValueError(f"Invalid IP address: {ip_address}")
            
            if status not in ["allowed", "restricted"]:
                raise ValueError(f"Invalid status: {status}. Must be 'allowed' or 'restricted'")
            
            with self.lock:
                ips = self.data.get('ips', {})
                
                if ip_address not in ips:
                    # Add new IP with specified status
                    current_time = datetime.utcnow().isoformat() + 'Z'
                    ips[ip_address] = {
                        "status": status,
                        "first_seen": current_time,
                        "last_seen": current_time,
                        "request_count": 0,
                        "last_request_type": "N/A"
                    }
                    self.logger.info(f"Added new IP {ip_address} with status: {status}")
                else:
                    # Update existing IP status
                    old_status = ips[ip_address].get('status', 'allowed')
                    ips[ip_address]["status"] = status
                    self.logger.info(f"Updated IP {ip_address} status: {old_status} -> {status}")
                
                self.data["ips"] = ips
                self._save_data()
                
        except Exception as e:
            self.logger.error(f"Error setting IP status for {ip_address}: {str(e)}")
            raise
    
    def set_global_access(self, status: str):
        """
        Set global access mode.
        
        Args:
            status: Global access mode ("allowed" or "restricted")
        """
        try:
            if status not in ["allowed", "restricted"]:
                raise ValueError(f"Invalid status: {status}. Must be 'allowed' or 'restricted'")
            
            with self.lock:
                old_status = self.data.get('global_access', 'allowed')
                self.data["global_access"] = status
                self._save_data()
                self.logger.info(f"Global access mode changed: {old_status} -> {status}")
                
        except Exception as e:
            self.logger.error(f"Error setting global access mode: {str(e)}")
            raise
    
    def get_all_ips(self) -> Dict[str, Dict[str, Any]]:
        """
        Get all tracked IP addresses with their metadata.
        
        Returns:
            Dictionary containing all IP data
        """
        with self.lock:
            return self.data.copy()
    
    def remove_ip(self, ip_address: str):
        """
        Remove an IP address from tracking.
        
        Args:
            ip_address: IP address to remove
        """
        try:
            if not self._validate_ip(ip_address):
                raise ValueError(f"Invalid IP address: {ip_address}")
            
            with self.lock:
                ips = self.data.get('ips', {})
                
                if ip_address in ips:
                    del ips[ip_address]
                    self.data["ips"] = ips
                    self._save_data()
                    self.logger.info(f"Removed IP address from tracking: {ip_address}")
                else:
                    self.logger.warning(f"IP address not found for removal: {ip_address}")
                    
        except Exception as e:
            self.logger.error(f"Error removing IP {ip_address}: {str(e)}")
            raise
    
    def _validate_ip(self, ip_address: str) -> bool:
        """
        Validate IP address format.
        
        Args:
            ip_address: IP address to validate
            
        Returns:
            True if valid, False otherwise
        """
        try:
            ipaddress.ip_address(ip_address)
            return True
        except ValueError:
            return False
    
    def _enforce_ip_limit(self):
        """Enforce maximum IP limit with LRU eviction."""
        try:
            max_ips = self.config.get_int('max_tracked_ips', fallback=1000)
            ips = self.data.get('ips', {})
            
            if len(ips) <= max_ips:
                return
            
            # Sort IPs by last_seen time (oldest first)
            sorted_ips = sorted(
                ips.items(),
                key=lambda x: x[1].get('last_seen', '')
            )
            
            # Remove oldest IPs to stay within limit
            to_remove = len(ips) - max_ips
            for i in range(to_remove):
                ip_to_remove = sorted_ips[i][0]
                del ips[ip_to_remove]
                self.logger.info(f"Removed old IP due to limit: {ip_to_remove}")
            
            self.data["ips"] = ips
            
        except Exception as e:
            self.logger.error(f"Error enforcing IP limit: {str(e)}")


# Global IP manager instance
_ip_manager = None


def get_ip_manager() -> IPManager:
    """
    Get the global IP manager instance.
    
    Returns:
        Global IPManager instance
    """
    global _ip_manager
    if _ip_manager is None:
        _ip_manager = IPManager()
    return _ip_manager