"""
MundiX OSINT Monitor
=====================

Continuous background intelligence gathering that feeds the Knowledge Graph.

Capabilities:
  1. Passive subdomain monitoring (crt.sh certificate transparency)
  2. CVE watch — monitor new CVEs matching discovered services
  3. Shodan passive recon (if API key available)
  4. Periodic re-check of known assets

The monitor runs in a background thread and enriches the KG without
generating traffic to the target (passive only).

Usage:
    from osint_monitor import OSINTMonitor
    
    monitor = OSINTMonitor(knowledge_graph, on_alert=print)
    monitor.start(interval=300)  # Check every 5 minutes
    ...
    monitor.stop()
"""

import json
import os
import re
import threading
import time
import urllib.request
import urllib.error
from datetime import datetime, timedelta
from typing import Any, Callable, Dict, List, Optional, Set

try:
    from knowledge_graph import KnowledgeGraph, NodeType
except ImportError:
    KnowledgeGraph = None
    NodeType = None

try:
    from threat_intel import ThreatIntel
except ImportError:
    ThreatIntel = None


# ---------------------------------------------------------------------------
# OSINT Monitor
# ---------------------------------------------------------------------------

class OSINTMonitor:
    """
    Background OSINT intelligence monitor.
    
    Periodically queries passive sources and feeds new discoveries
    into the Knowledge Graph.
    """

    def __init__(
        self,
        knowledge_graph: 'KnowledgeGraph',
        target: str = None,
        on_alert: Callable[[str, str, Dict], None] = None,
        shodan_key: str = None,
        cache_dir: str = None,
    ):
        """
        Args:
            knowledge_graph: KG to feed discoveries into
            target: Primary target domain (auto-detected from KG if None)
            on_alert: Callback(severity, message, data) for new findings
            shodan_key: Optional Shodan API key
            cache_dir: Cache directory for API responses
        """
        self.kg = knowledge_graph
        self.target = target or knowledge_graph.target
        self.on_alert = on_alert or (lambda s, m, d: None)
        self.shodan_key = shodan_key or os.environ.get("SHODAN_API_KEY", "")
        self.cache_dir = cache_dir or os.path.expanduser("~/.mundix/osint_cache")
        os.makedirs(self.cache_dir, exist_ok=True)

        self.threat_intel = ThreatIntel() if ThreatIntel else None

        # State
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._known_subdomains: Set[str] = set()
        self._known_cves: Set[str] = set()
        self._check_count = 0
        self._last_check: Optional[str] = None
        self._findings_count = 0

        # Pre-populate known state from KG
        self._sync_from_kg()

    def _sync_from_kg(self):
        """Sync known assets from the Knowledge Graph."""
        if not self.kg:
            return
        for node in self.kg.nodes.values():
            if node.type == NodeType.SUBDOMAIN:
                self._known_subdomains.add(node.properties.get("domain", node.label))
            elif node.type == NodeType.HOST:
                self._known_subdomains.add(node.properties.get("hostname", ""))
            elif node.type == NodeType.CVE:
                self._known_cves.add(node.label)

    def start(self, interval: int = 300):
        """Start the background monitor loop."""
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(
            target=self._monitor_loop,
            args=(interval,),
            daemon=True,
            name="mundix-osint-monitor",
        )
        self._thread.start()
        self.on_alert("info", f"OSINT Monitor started for {self.target} (every {interval}s)", {})

    def stop(self):
        """Stop the background monitor."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=10)
            self._thread = None
        self.on_alert("info", f"OSINT Monitor stopped ({self._findings_count} total findings)", {})

    @property
    def is_running(self) -> bool:
        return self._running

    def get_status(self) -> Dict[str, Any]:
        """Get monitor status."""
        return {
            "running": self._running,
            "target": self.target,
            "checks": self._check_count,
            "last_check": self._last_check,
            "known_subdomains": len(self._known_subdomains),
            "known_cves": len(self._known_cves),
            "findings": self._findings_count,
        }

    # --- Monitor Loop ---

    def _monitor_loop(self, interval: int):
        """Main monitoring loop (runs in background thread)."""
        while self._running:
            try:
                self._run_checks()
                self._check_count += 1
                self._last_check = datetime.now().isoformat()
            except Exception as e:
                self.on_alert("error", f"Monitor check failed: {e}", {"error": str(e)})

            # Wait for next interval (check stop flag every 5 seconds)
            for _ in range(interval // 5):
                if not self._running:
                    break
                time.sleep(5)

    def _run_checks(self):
        """Run all passive checks."""
        self._check_cert_transparency()
        self._check_new_cves()
        if self.shodan_key:
            self._check_shodan()

    # --- Certificate Transparency (crt.sh) ---

    def _check_cert_transparency(self):
        """Query crt.sh for new subdomains via certificate transparency logs."""
        domain = self.target
        # Strip protocol if present
        if "://" in domain:
            domain = domain.split("://")[1].split("/")[0]

        url = f"https://crt.sh/?q=%25.{domain}&output=json"
        cache_file = os.path.join(self.cache_dir, f"crtsh_{domain}.json")

        try:
            req = urllib.request.Request(url, headers={
                "User-Agent": "MundiX-OSINT/1.0",
                "Accept": "application/json",
            })
            with urllib.request.urlopen(req, timeout=30) as resp:
                data = json.loads(resp.read().decode())

            # Extract unique subdomains
            new_subs = set()
            for entry in data:
                name = entry.get("name_value", "")
                for sub in name.split("\n"):
                    sub = sub.strip().lower()
                    if sub and "*" not in sub and sub not in self._known_subdomains:
                        new_subs.add(sub)

            if new_subs:
                self._findings_count += len(new_subs)
                for sub in new_subs:
                    self._known_subdomains.add(sub)
                    if self.kg:
                        self.kg.add_domain(sub, source="crt.sh-monitor")

                self.on_alert(
                    "medium",
                    f"🔍 {len(new_subs)} new subdomain(s) found via CT logs",
                    {"subdomains": list(new_subs)[:20]},
                )

            # Cache
            with open(cache_file, 'w') as f:
                json.dump({"checked": datetime.now().isoformat(),
                           "count": len(data)}, f)

        except Exception:
            pass  # Silent fail — passive monitoring shouldn't crash

    # --- CVE Watch ---

    def _check_new_cves(self):
        """Check for new CVEs affecting discovered services."""
        if not self.threat_intel or not self.kg:
            return

        # Get all services from the KG
        services = self.kg.get_nodes_by_type(NodeType.SERVICE)
        if not services:
            return

        for svc_node in services:
            service = svc_node.properties.get("service", "")
            version = svc_node.properties.get("version", "")
            if not service or not version:
                continue

            # Build search keyword
            keyword = f"{service} {version}".strip()

            try:
                results = self.threat_intel.search_nvd(keyword, max_results=5)
                for cve_data in results:
                    cve_id = cve_data.get("cve_id", "")
                    if cve_id and cve_id not in self._known_cves:
                        self._known_cves.add(cve_id)
                        self._findings_count += 1

                        host = svc_node.properties.get("host", self.target)
                        port = svc_node.properties.get("port", 0)
                        cvss = cve_data.get("cvss_score", 0.0)
                        severity = cve_data.get("severity", "unknown")

                        self.kg.add_cve(
                            host, port, cve_id,
                            cvss=cvss, severity=severity,
                            description=cve_data.get("description", "")[:200],
                            source="nvd-monitor",
                        )

                        # Check if it's in CISA KEV
                        kev_match = self.threat_intel.check_cisa_kev(cve_id)

                        alert_severity = "critical" if cvss >= 9.0 else "high" if cvss >= 7.0 else "medium"
                        kev_tag = " ⚠️ CISA KEV!" if kev_match else ""
                        self.on_alert(
                            alert_severity,
                            f"🆕 New CVE: {cve_id} (CVSS {cvss}) for {keyword}{kev_tag}",
                            {"cve": cve_id, "cvss": cvss, "service": keyword,
                             "host": host, "port": port, "kev": bool(kev_match)},
                        )

                # Rate limit between services
                time.sleep(6)
            except Exception:
                pass

    # --- Shodan Passive ---

    def _check_shodan(self):
        """Query Shodan for passive recon on the target."""
        if not self.shodan_key:
            return

        domain = self.target
        if "://" in domain:
            domain = domain.split("://")[1].split("/")[0]

        url = f"https://api.shodan.io/dns/resolve?hostnames={domain}&key={self.shodan_key}"

        try:
            req = urllib.request.Request(url, headers={
                "User-Agent": "MundiX-OSINT/1.0",
            })
            with urllib.request.urlopen(req, timeout=15) as resp:
                data = json.loads(resp.read().decode())

            ip = data.get(domain)
            if ip:
                # Query host info
                host_url = f"https://api.shodan.io/shodan/host/{ip}?key={self.shodan_key}"
                req2 = urllib.request.Request(host_url, headers={
                    "User-Agent": "MundiX-OSINT/1.0",
                })
                with urllib.request.urlopen(req2, timeout=15) as resp2:
                    host_data = json.loads(resp2.read().decode())

                # Feed ports and services into KG
                if self.kg:
                    self.kg.add_host(ip, hostname=domain, source="shodan-monitor")

                    for port_info in host_data.get("data", []):
                        port = port_info.get("port", 0)
                        transport = port_info.get("transport", "tcp")
                        product = port_info.get("product", "")
                        version = port_info.get("version", "")
                        self.kg.add_port(
                            ip, port, transport, product, version,
                            source="shodan-monitor",
                        )

                    # Shodan vulns
                    for vuln_id in host_data.get("vulns", []):
                        if vuln_id not in self._known_cves:
                            self._known_cves.add(vuln_id)
                            self._findings_count += 1
                            self.kg.add_cve(
                                ip, 0, vuln_id, source="shodan-monitor",
                            )
                            self.on_alert(
                                "high",
                                f"🔍 Shodan CVE: {vuln_id} on {ip}",
                                {"cve": vuln_id, "host": ip},
                            )

        except Exception:
            pass

    # --- One-Shot Check (for non-background use) ---

    def run_once(self) -> Dict[str, Any]:
        """Run all checks once (non-background) and return findings."""
        findings_before = self._findings_count
        self._run_checks()
        self._check_count += 1
        self._last_check = datetime.now().isoformat()
        return {
            "new_findings": self._findings_count - findings_before,
            "total_findings": self._findings_count,
            "subdomains": len(self._known_subdomains),
            "cves": len(self._known_cves),
        }
