import requests
import json
import time
from scapy.all import *
import ipaddress
from datetime import datetime, timedelta
import os

# Configuration
LOCAL_IP = "192.168.0.12"  # Replace with your local IPv4
TARGET_PORT = 7777 # Dying Light UDP Port
RATE_LIMIT_DELAY = 1  # seconds between requests
CACHE_DURATION = 180  # Cache entries for 3 minutes (180 seconds)

# IP cache to store recent lookups
ip_cache = {}
last_logged = {}  # Track when we last logged each IP

# Colors for CMD (Windows compatible)
class Colors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def clear_screen():
    """Clear the screen"""
    os.system('cls' if os.name == 'nt' else 'clear')

def print_header():
    """Print a nice header"""
    print("=" * 80)
    print(f"{Colors.HEADER}{Colors.BOLD}🌐 DYING LIGHT NETWORK MONITOR 🌐{Colors.ENDC}")
    print("=" * 80)
    print(f"{Colors.OKCYAN}Port: {TARGET_PORT} | Local IP: {LOCAL_IP} | Cache: {CACHE_DURATION}s{Colors.ENDC}")
    print(f"{Colors.WARNING}Monitoring NEW connections only...{Colors.ENDC}")
    print("=" * 80)

def get_country_flag(country):
    """Get flag emoji for country (basic mapping)"""
    flags = {
        'Germany': '🇩🇪', 'Turkey': '🇹🇷', 'Türkiye': '🇹🇷', 'United States': '🇺🇸',
        'United Kingdom': '🇬🇧', 'France': '🇫🇷', 'Italy': '🇮🇹', 'Spain': '🇪🇸',
        'Russia': '🇷🇺', 'Poland': '🇵🇱', 'Netherlands': '🇳🇱', 'Canada': '🇨🇦',
        'Brazil': '🇧🇷', 'Japan': '🇯🇵', 'China': '🇨🇳', 'South Korea': '🇰🇷',
        'Australia': '🇦🇺', 'Sweden': '🇸🇪', 'Norway': '🇳🇴', 'Denmark': '🇩🇰'
    }
    return flags.get(country, '🌍')

def format_timestamp():
    """Get formatted timestamp"""
    return datetime.now().strftime("%H:%M:%S")

def format_isp(isp):
    """Format ISP name to be shorter"""
    if len(isp) > 30:
        return isp[:27] + "..."
    return isp

def is_private_ip(ip):
    """Check if IP is private/local"""
    try:
        return ipaddress.ip_address(ip).is_private
    except:
        return False

def clean_old_cache():
    """Remove old entries from cache"""
    current_time = datetime.now()
    expired_ips = []
    
    for ip, (data, timestamp) in ip_cache.items():
        if current_time - timestamp > timedelta(seconds=CACHE_DURATION):
            expired_ips.append(ip)
    
    for ip in expired_ips:
        del ip_cache[ip]
        if ip in last_logged:
            del last_logged[ip]

def should_log_ip(ip):
    """Check if we should log this IP (not logged in last 3 minutes)"""
    current_time = datetime.now()
    
    if ip in last_logged:
        time_diff = current_time - last_logged[ip]
        return time_diff > timedelta(seconds=CACHE_DURATION)
    
    return True

def get_geo_info(ip):
    """Get geolocation info using ipapi.co with caching"""
    current_time = datetime.now()
    
    # Check cache first
    if ip in ip_cache:
        data, timestamp = ip_cache[ip]
        if current_time - timestamp < timedelta(seconds=CACHE_DURATION):
            return data
    
    # Clean old cache entries periodically
    clean_old_cache()
    
    try:
        # Add delay to respect rate limits
        time.sleep(RATE_LIMIT_DELAY)
        
        response = requests.get(f"https://ipapi.co/{ip}/json/", timeout=10)
        
        if response.status_code == 200:
            data = response.json()
            geo_info = {
                'country': data.get('country_name', 'Unknown'),
                'region': data.get('region', 'Unknown'),
                'city': data.get('city', 'Unknown'),
                'org': data.get('org', 'Unknown')  # ISP/Organization info
            }
            
            # Cache the result
            ip_cache[ip] = (geo_info, current_time)
            return geo_info
            
        elif response.status_code == 429:
            print(f"Rate limit hit for {ip}. Trying cached data...")
            cached_data = ip_cache.get(ip)
            if cached_data:
                return cached_data[0]
            return None
        else:
            print(f"{Colors.FAIL}API error for {ip}: Status {response.status_code}{Colors.ENDC}")
            return None
            
    except requests.exceptions.RequestException as e:
        print(f"{Colors.FAIL}Network error for {ip}: {e}{Colors.ENDC}")
        return None
    except Exception as e:
        print(f"{Colors.FAIL}Geolocation error for {ip}: {e}{Colors.ENDC}")
        return None

def process_packet(packet):
    """Process captured UDP packet"""
    if packet.haslayer(IP) and packet.haslayer(UDP):
        src_ip = packet[IP].src
        dst_ip = packet[IP].dst
        
        # Skip if destination is local IP
        if dst_ip == LOCAL_IP:
            return
            
        # Skip private IPs (they won't have meaningful geolocation)
        if is_private_ip(dst_ip):
            print(f"{Colors.WARNING}[{format_timestamp()}] ⚠️  Skipping private IP: {dst_ip}{Colors.ENDC}")
            return
            
        # Check if we should log this IP (not logged recently)
        if not should_log_ip(dst_ip):
            return  # Skip if logged recently
        
        print(f"{Colors.OKCYAN}[{format_timestamp()}] 🔍 Looking up: {dst_ip}...{Colors.ENDC}")
        
        # Get geolocation info
        geo_info = get_geo_info(dst_ip)
        
        if geo_info:
            # Update last logged time
            last_logged[dst_ip] = datetime.now()
            
            flag = get_country_flag(geo_info['country'])
            isp_formatted = format_isp(geo_info['org'])
            
            print(f"{Colors.OKGREEN}╔══════════════════════════════════════════════════════════════════════════════╗{Colors.ENDC}")
            print(f"{Colors.OKGREEN}║ {Colors.BOLD}🎯 NEW CONNECTION DETECTED{Colors.ENDC}{Colors.OKGREEN}                                                ║{Colors.ENDC}")
            print(f"{Colors.OKGREEN}╠══════════════════════════════════════════════════════════════════════════════╣{Colors.ENDC}")
            print(f"{Colors.OKGREEN}║ {Colors.BOLD}IP Address:{Colors.ENDC} {Colors.OKBLUE}{dst_ip:<15}{Colors.ENDC}{Colors.OKGREEN}                                                ║{Colors.ENDC}")
            print(f"{Colors.OKGREEN}║ {Colors.BOLD}Country:{Colors.ENDC} {flag} {geo_info['country']:<30}{Colors.OKGREEN}                                    ║{Colors.ENDC}")
            
            if geo_info['city'] != 'Unknown':
                print(f"{Colors.OKGREEN}║ {Colors.BOLD}Location:{Colors.ENDC} {geo_info['city']}, {geo_info['region']:<35}{Colors.OKGREEN}                           ║{Colors.ENDC}")
            
            print(f"{Colors.OKGREEN}║ {Colors.BOLD}ISP:{Colors.ENDC} {isp_formatted:<45}{Colors.OKGREEN}                                        ║{Colors.ENDC}")
            print(f"{Colors.OKGREEN}╚══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}")
            print()
        else:
            print(f"{Colors.FAIL}[{format_timestamp()}] ❌ Failed to get info for: {dst_ip}{Colors.ENDC}")
            print()

def main():
    """Main monitoring loop"""
    clear_screen()
    print_header()
    
    connection_count = 0
    
    try:
        while True:
            # Capture packets
            packets = sniff(
                filter=f"udp and port {TARGET_PORT}",
                prn=lambda pkt: process_packet_wrapper(pkt, connection_count),
                store=0,  # Don't store packets in memory
                count=1,
                timeout=10  # Add timeout to prevent hanging
            )
            
    except KeyboardInterrupt:
        print(f"\n{Colors.WARNING}{'='*80}{Colors.ENDC}")
        print(f"{Colors.WARNING}📊 MONITORING STOPPED BY USER{Colors.ENDC}")
        print(f"{Colors.WARNING}{'='*80}{Colors.ENDC}")
        print(f"{Colors.OKCYAN}📈 Total cached IPs: {len(ip_cache)}{Colors.ENDC}")
        print(f"{Colors.OKCYAN}📋 Unique connections logged: {len(last_logged)}{Colors.ENDC}")
        print(f"{Colors.OKGREEN}Thank you for using the network monitor!{Colors.ENDC}")
    except Exception as e:
        print(f"{Colors.FAIL}Error: {e}{Colors.ENDC}")

def process_packet_wrapper(packet, connection_count):
    """Wrapper to track connection count"""
    global connection_count_global
    connection_count_global = getattr(process_packet_wrapper, 'counter', 0)
    process_packet_wrapper.counter = connection_count_global + 1
    process_packet(packet)

if __name__ == "__main__":
    main()