#!/usr/bin/env python3
"""
netIntel Client (Fixed + more reliable)
- Sends payload to /receive with X-Env-Token header
"""

import socket
import os
import platform
import json
import requests
from datetime import datetime
import psutil
import time
import subprocess
import getpass


# ----------------------------
# Helpers
# ----------------------------
def safe_username():
    try:
        return os.getlogin()
    except Exception:
        return getpass.getuser()


def get_local_ip_fallback():
    """
    More reliable local IP than socket.gethostbyname(hostname).
    """
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        try:
            return socket.gethostbyname(socket.gethostname())
        except Exception:
            return "127.0.0.1"


# ----------------------------
# Sysmon-like collector (simulated)
# ----------------------------
class FixedSysmonCollector:
    """Simplified collector - works even without Sysmon installed"""

    def __init__(self):
        self.last_collection = None

    def collect_sysmon_events(self, limit=50):
        events = []
        now = datetime.now()

        try:
            # 1) Process events (Event ID 1)
            for proc in psutil.process_iter(['pid', 'name', 'create_time', 'username']):
                try:
                    pinfo = proc.info
                    ct = pinfo.get('create_time')
                    if ct:
                        create_time = datetime.fromtimestamp(ct)
                        if (not self.last_collection) or (create_time > self.last_collection):
                            events.append({
                                "event_id": 1,
                                "event_type": "Process Creation",
                                "message": f"Process created: {pinfo.get('name')} (PID: {pinfo.get('pid')}, User: {pinfo.get('username')})",
                                "timestamp": create_time.isoformat(),
                                "process_name": pinfo.get('name'),
                                "pid": pinfo.get('pid'),
                                "user": pinfo.get('username'),
                            })
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

                if len(events) >= limit // 2:
                    break

            # 2) Network connections (Event ID 3)
            for conn in psutil.net_connections(kind='inet'):
                try:
                    if conn.raddr and conn.status in ['ESTABLISHED', 'LISTEN']:
                        proc = psutil.Process(conn.pid) if conn.pid else None
                        events.append({
                            "event_id": 3,
                            "event_type": "Network Connection",
                            "message": f"Network {conn.status}: "
                                       f"{conn.laddr.ip if conn.laddr else 'N/A'}:{conn.laddr.port if conn.laddr else 'N/A'} "
                                       f"-> {conn.raddr.ip}:{conn.raddr.port}",
                            "timestamp": now.isoformat(),
                            "local_address": f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else None,
                            "remote_address": f"{conn.raddr.ip}:{conn.raddr.port}",
                            "status": conn.status,
                            "pid": conn.pid,
                            "process_name": proc.name() if proc else "Unknown",
                        })
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

                if len(events) >= limit:
                    break

            # 3) Performance alerts (optional)
            cpu = psutil.cpu_percent(interval=0.1)
            if cpu > 80:
                events.append({
                    "event_id": "PERF_HIGH_CPU",
                    "event_type": "High CPU Usage",
                    "message": f"High CPU usage detected: {cpu:.1f}%",
                    "timestamp": now.isoformat(),
                    "cpu_percent": cpu
                })

            ram = psutil.virtual_memory()
            if ram.percent > 85:
                events.append({
                    "event_id": "PERF_HIGH_RAM",
                    "event_type": "High Memory Usage",
                    "message": f"High RAM usage: {ram.percent:.1f}%",
                    "timestamp": now.isoformat(),
                    "ram_percent": ram.percent
                })

        except Exception as e:
            print(f"FixedSysmon error: {e}")

        self.last_collection = now
        return events

    def collect_network_connections_detailed(self, limit=30):
        connections = []
        now = datetime.now().isoformat()

        try:
            for conn in psutil.net_connections(kind='inet'):
                try:
                    proc = psutil.Process(conn.pid) if conn.pid else None
                    conn_info = {
                        "pid": conn.pid,
                        "status": conn.status,
                        "family": str(conn.family),
                        "type": str(conn.type),
                        "timestamp": now
                    }

                    if conn.laddr:
                        conn_info["local_address"] = f"{conn.laddr.ip}:{conn.laddr.port}"
                        conn_info["local_ip"] = conn.laddr.ip
                        conn_info["local_port"] = conn.laddr.port

                    if conn.raddr:
                        conn_info["remote_address"] = f"{conn.raddr.ip}:{conn.raddr.port}"
                        conn_info["remote_ip"] = conn.raddr.ip
                        conn_info["remote_port"] = conn.raddr.port

                    if proc:
                        conn_info["process_name"] = proc.name()
                        try:
                            cmd = proc.cmdline()
                            conn_info["process_cmdline"] = " ".join(cmd) if cmd else None
                        except Exception:
                            conn_info["process_cmdline"] = None
                        try:
                            conn_info["process_username"] = proc.username()
                        except Exception:
                            conn_info["process_username"] = None

                    connections.append(conn_info)

                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

                if len(connections) >= limit:
                    break

        except Exception as e:
            print(f"Network connections error: {e}")

        return connections

    def collect_all_security_data(self):
        return {
            "sysmon_events": self.collect_sysmon_events(limit=50),
            "network_connections": self.collect_network_connections_detailed(limit=30),
            "file_changes": [],
            "collection_time": datetime.now().isoformat()
        }


# ----------------------------
# Client
# ----------------------------
class EnhancedFixedClient:
    def __init__(self, server_ip="192.168.1.13", port=5555, env_token=None):
        self.server_ip = server_ip
        self.port = port
        self.computer_name = socket.gethostname()
        self.user = safe_username()
        self.sysmon_collector = FixedSysmonCollector()
        self.env_token = env_token

        # warmup cpu_percent so it doesn't start at 0
        try:
            psutil.cpu_percent(interval=None)
        except Exception:
            pass

    def _check_sysmon_installed(self):
        try:
            if platform.system() == "Windows":
                result = subprocess.run(
                    ["powershell", "-Command", "Get-Process -Name 'sysmon' -ErrorAction SilentlyContinue"],
                    capture_output=True, text=True
                )
                return result.returncode == 0
        except Exception:
            pass
        return False

    def get_performance_metrics(self):
        try:
            cpu_percent = psutil.cpu_percent(interval=0.5)
            cpu_freq = psutil.cpu_freq()
            ram = psutil.virtual_memory()
            swap = psutil.swap_memory()
            disk = psutil.disk_usage("C:/" if platform.system() == "Windows" else "/")

            disk_read_mb = 0
            disk_write_mb = 0
            try:
                disk_io = psutil.disk_io_counters()
                if disk_io:
                    disk_read_mb = round(disk_io.read_bytes / (1024 ** 2), 2)
                    disk_write_mb = round(disk_io.write_bytes / (1024 ** 2), 2)
            except Exception:
                pass

            net_io = psutil.net_io_counters()

            boot_dt = datetime.fromtimestamp(psutil.boot_time())
            uptime_hours = round((datetime.now() - boot_dt).total_seconds() / 3600, 2)

            return {
                "cpu_usage": cpu_percent,
                "cpu_cores": psutil.cpu_count(logical=True),
                "cpu_freq_current": cpu_freq.current if cpu_freq else 0,
                "ram_usage": ram.percent,
                "ram_total_gb": round(ram.total / (1024 ** 3), 2),
                "ram_used_gb": round(ram.used / (1024 ** 3), 2),
                "swap_usage": swap.percent if swap else 0,
                "disk_usage": disk.percent,
                "disk_total_gb": round(disk.total / (1024 ** 3), 2),
                "disk_used_gb": round(disk.used / (1024 ** 3), 2),
                "disk_read_mb": disk_read_mb,
                "disk_write_mb": disk_write_mb,
                "network_sent_mb": round(net_io.bytes_sent / (1024 ** 2), 2),
                "network_recv_mb": round(net_io.bytes_recv / (1024 ** 2), 2),
                "network_packets_sent": net_io.packets_sent,
                "network_packets_recv": net_io.packets_recv,
                "boot_time": boot_dt.strftime("%Y-%m-%d %H:%M:%S"),
                "uptime_hours": uptime_hours
            }
        except Exception as e:
            print(f"Performance metrics error: {e}")
            return {}

    def get_hardware_info(self):
        try:
            partitions = []
            for partition in psutil.disk_partitions():
                try:
                    usage = psutil.disk_usage(partition.mountpoint)
                    partitions.append({
                        "device": partition.device,
                        "mountpoint": partition.mountpoint,
                        "fstype": partition.fstype,
                        "total_gb": round(usage.total / (1024 ** 3), 2),
                        "used_gb": round(usage.used / (1024 ** 3), 2),
                        "free_gb": round(usage.free / (1024 ** 3), 2),
                        "percent_used": usage.percent
                    })
                except Exception:
                    continue

            net_if_addrs = psutil.net_if_addrs()
            interfaces = {}
            for interface, addrs in net_if_addrs.items():
                interfaces[interface] = [
                    {
                        "family": str(addr.family),
                        "address": addr.address,
                        "netmask": getattr(addr, "netmask", None)
                    }
                    for addr in addrs if getattr(addr, "address", None)
                ]

            batt = None
            try:
                b = psutil.sensors_battery()
                batt = b.percent if b else None
            except Exception:
                batt = None

            return {
                "cpu_physical_cores": psutil.cpu_count(logical=False),
                "cpu_logical_cores": psutil.cpu_count(logical=True),
                "disks": partitions,
                "network_interfaces": interfaces,
                "battery_percent": batt,
                "temperatures": self._get_temperatures()
            }
        except Exception as e:
            print(f"Hardware info error: {e}")
            return {}

    def _get_temperatures(self):
        try:
            temps = {}
            if hasattr(psutil, "sensors_temperatures"):
                sensors = psutil.sensors_temperatures()
                for name, entries in sensors.items():
                    if entries:
                        temps[name] = entries[0].current
            return temps
        except Exception:
            return {}

    def get_detailed_processes(self, limit=100):
        processes = []
        try:
            for proc in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent',
                                             'create_time', 'status']):
                try:
                    pinfo = proc.info
                    with proc.oneshot():
                        processes.append({
                            "pid": pinfo.get("pid"),
                            "name": pinfo.get("name"),
                            "user": pinfo.get("username"),
                            "cpu_percent": pinfo.get("cpu_percent"),
                            "memory_mb": round(proc.memory_info().rss / (1024 ** 2), 2),
                            "memory_percent": pinfo.get("memory_percent"),
                            "create_time": datetime.fromtimestamp(pinfo["create_time"]).strftime("%Y-%m-%d %H:%M:%S")
                            if pinfo.get("create_time") else None,
                            "status": pinfo.get("status"),
                            "exe": proc.exe() if hasattr(proc, "exe") else None,
                            "cmdline": " ".join(proc.cmdline()) if hasattr(proc, "cmdline") else None,
                            "num_threads": proc.num_threads() if hasattr(proc, "num_threads") else None,
                            "connections_count": 0
                        })
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue
                except Exception:
                    continue

                if len(processes) >= limit:
                    break

        except Exception as e:
            print(f"Process error: {e}")

        return processes

    def get_detailed_info(self):
        info = {
            "computer_name": self.computer_name,
            "user": self.user,
            "ip_address": get_local_ip_fallback(),
            "os": f"{platform.system()} {platform.release()} {platform.version()}",
            "architecture": platform.architecture()[0],
            "timestamp": datetime.now().isoformat(),
            "is_sysmon_available": self._check_sysmon_installed()
        }
        info.update(self.get_performance_metrics())
        info.update(self.get_hardware_info())
        return info

    def collect_and_send(self):
        print(f"\n[{datetime.now().strftime('%H:%M:%S')}] 🔍 Collecting...")

        data = {
            "info": self.get_detailed_info(),
            "processes": self.get_detailed_processes(limit=100),
            "security_data": self.sysmon_collector.collect_all_security_data(),
            "collection_time": datetime.now().isoformat(),
            "client_version": "2.2_fixed"
        }

        info = data["info"]
        sec = data["security_data"]

        print(f"   💻 {info.get('computer_name')} | 👤 {info.get('user')}")
        print(f"   📊 CPU {info.get('cpu_usage', 0)}% | RAM {info.get('ram_usage', 0)}% | Disk {info.get('disk_usage', 0)}%")
        print(f"   🔄 Processes: {len(data['processes'])}")
        print(f"   🛡️ Sysmon events: {len(sec.get('sysmon_events', []))}")
        print(f"   🌐 Net conns: {len(sec.get('network_connections', []))}")

        try:
            headers = {}
            if self.env_token:
                headers["X-Env-Token"] = self.env_token
            headers["X-Client-Id"] = self.computer_name  # optional

            url = f"http://{self.server_ip}:{self.port}/receive"
            r = requests.post(url, json=data, headers=headers, timeout=30)

            if r.ok:
                resp = r.json()
                print("   ✅ Sent OK")
                if "sysmon_events_saved" in resp:
                    print(f"   📝 Saved sysmon: {resp['sysmon_events_saved']}")
                return True

            # errors
            if r.status_code == 401:
                try:
                    msg = r.json()
                except Exception:
                    msg = {"error": r.text[:200]}
                print("   🔒 Token invalid/expired. Generate new token in Admin panel.")
                print(f"   Server says: {msg}")
            else:
                print(f"   ❌ HTTP {r.status_code}: {r.text[:300]}")
            return False

        except Exception as e:
            print(f"   ❌ Connection problem: {e}")
            return False


def main():
    print("=" * 70)
    print("🖥️  netIntel Collector (Fixed)")
    print("=" * 70)
    print(f"Computer: {socket.gethostname()}")
    print(f"User: {safe_username()}")
    print(f"Local IP: {get_local_ip_fallback()}")
    print("=" * 70)
    print("Will send every 30 seconds. Ctrl+C to stop.")
    print("=" * 70)

    server_ip = input("Server IP (default 192.168.1.13): ").strip() or "192.168.1.13"
    env_token = input("ENV TOKEN (from Admin panel): ").strip()
    if not env_token:
        print("❌ ENV TOKEN is required.")
        return

    client = EnhancedFixedClient(server_ip, 5555, env_token=env_token)

    print(f"\n🔌 Testing server {server_ip}...")
    try:
        r = requests.get(f"http://{server_ip}:5555/ping", timeout=5)
        if r.ok:
            print("   ✅ Server reachable")
        else:
            print(f"   ⚠️ Server responded: {r.status_code}")
    except Exception as e:
        print(f"   ❌ Can't reach server: {e}")
        return

    try:
        count = 1
        while True:
            print(f"\n📤 Transmission #{count}")
            client.collect_and_send()
            count += 1

            print("\n⏳ Waiting 30s...")
            for i in range(30, 0, -1):
                if i % 10 == 0 or i <= 5:
                    print(f"\r   {i} seconds remaining...", end="")
                time.sleep(1)
            print("\r" + " " * 40 + "\r", end="")

    except KeyboardInterrupt:
        print("\n\n🛑 Stopped.")
        print(f"📊 Dashboard/API: http://{server_ip}:5555/")


if __name__ == "__main__":
    try:
        import psutil  # noqa
        import requests  # noqa
    except ImportError:
        print("Install modules:")
        print("pip install psutil requests")
        input("Press ENTER to exit...")
        raise SystemExit(1)

    main()
