"""
OmniSec Internet Toolkit
=========================
Comprehensive internet utilities:
  - HTTP client with retry, rate limiting, and session management
  - DNS resolution and reverse lookup
  - Port scanning
  - WHOIS lookup
  - SSL/TLS certificate inspection
  - IP geolocation
  - Website availability monitoring
  - Web scraping helpers
  - robots.txt parser
"""

from __future__ import annotations

import socket
import ssl
import time
import json
import re
import concurrent.futures
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse, urlencode
from omnisec.core.logger import get_logger

log = get_logger("omnisec.internet")


class InternetToolkit:
    """
    All-in-one internet/networking toolkit for HTTP operations,
    DNS queries, port scanning, SSL inspection, and more.

    Example:
        >>> net = InternetToolkit(timeout=10)
        >>> resp = net.get("https://api.example.com/status")
        >>> cert = net.ssl_info("google.com")
        >>> open_ports = net.port_scan("192.168.1.1", ports=[80, 443, 22])
    """

    DEFAULT_HEADERS = {
        "User-Agent": "OmniSec/1.0 (+https://github.com/omnisec/omnisec)",
        "Accept": "application/json, text/html, */*",
    }

    def __init__(
        self,
        timeout: int = 10,
        retries: int = 3,
        retry_delay: float = 1.0,
        verify_ssl: bool = True,
        proxy: Optional[str] = None,
    ):
        self.timeout = timeout
        self.retries = retries
        self.retry_delay = retry_delay
        self.verify_ssl = verify_ssl
        self.proxy = proxy
        self._session = None
        self._init_session()

    def _init_session(self):
        try:
            import requests
            self._session = requests.Session()
            self._session.headers.update(self.DEFAULT_HEADERS)
            if self.proxy:
                self._session.proxies = {"http": self.proxy, "https": self.proxy}
            log.info(f"InternetToolkit initialized (timeout={self.timeout}s)")
        except ImportError:
            log.warning("requests not installed. Run: pip install requests")

    def _require_session(self):
        if not self._session:
            raise RuntimeError("requests not available. Install: pip install requests")

    # ── HTTP Methods ─────────────────────────────────────────────────

    def get(
        self,
        url: str,
        params: Optional[Dict] = None,
        headers: Optional[Dict] = None,
        auth: Optional[Tuple] = None,
    ) -> Dict[str, Any]:
        """
        Perform an HTTP GET request with automatic retries.

        Args:
            url:     Target URL
            params:  Query parameters dict
            headers: Additional headers
            auth:    Optional (username, password) tuple

        Returns:
            Dict with: status_code, headers, body, json (if applicable),
                       url, elapsed_ms, ok

        Example:
            >>> resp = net.get("https://api.ipify.org?format=json")
            >>> print(resp["json"]["ip"])
        """
        self._require_session()
        h = {**self.DEFAULT_HEADERS, **(headers or {})}
        last_error = None

        for attempt in range(self.retries):
            try:
                start = time.time()
                r = self._session.get(
                    url, params=params, headers=h,
                    timeout=self.timeout, verify=self.verify_ssl, auth=auth
                )
                elapsed = int((time.time() - start) * 1000)
                log.debug(f"GET {url} → {r.status_code} ({elapsed}ms)")
                return self._parse_response(r, elapsed)
            except Exception as e:
                last_error = e
                log.warning(f"GET attempt {attempt+1}/{self.retries} failed: {e}")
                time.sleep(self.retry_delay * (attempt + 1))

        raise RuntimeError(f"GET {url} failed after {self.retries} attempts: {last_error}")

    def post(
        self,
        url: str,
        data: Optional[Dict | str] = None,
        json_body: Optional[Dict] = None,
        headers: Optional[Dict] = None,
        auth: Optional[Tuple] = None,
    ) -> Dict[str, Any]:
        """
        Perform an HTTP POST request.

        Args:
            url:       Target URL
            data:      Form data dict or raw string body
            json_body: JSON payload dict
            headers:   Additional headers
            auth:      Optional (username, password) tuple

        Returns:
            Same format as get()

        Example:
            >>> resp = net.post("https://api.example.com/login",
            ...     json_body={"user": "admin", "pass": "secret"})
        """
        self._require_session()
        h = {**self.DEFAULT_HEADERS, **(headers or {})}
        for attempt in range(self.retries):
            try:
                start = time.time()
                r = self._session.post(
                    url, data=data, json=json_body,
                    headers=h, timeout=self.timeout,
                    verify=self.verify_ssl, auth=auth
                )
                elapsed = int((time.time() - start) * 1000)
                log.debug(f"POST {url} → {r.status_code} ({elapsed}ms)")
                return self._parse_response(r, elapsed)
            except Exception as e:
                log.warning(f"POST attempt {attempt+1} failed: {e}")
                time.sleep(self.retry_delay)
        raise RuntimeError(f"POST {url} failed")

    def head(self, url: str) -> Dict[str, Any]:
        """
        Perform an HTTP HEAD request (headers only, no body).

        Example:
            >>> info = net.head("https://example.com")
            >>> print(info["headers"].get("Content-Type"))
        """
        self._require_session()
        r = self._session.head(url, timeout=self.timeout, verify=self.verify_ssl)
        return {"status_code": r.status_code, "headers": dict(r.headers), "ok": r.ok}

    def download_file(self, url: str, local_path: str, chunk_size: int = 8192) -> Dict[str, Any]:
        """
        Download a file from a URL with streaming.

        Args:
            url:        File URL
            local_path: Local save path
            chunk_size: Download chunk size

        Returns:
            Dict with: local_path, size, elapsed_ms

        Example:
            >>> net.download_file("https://example.com/data.zip", "/tmp/data.zip")
        """
        import os
        self._require_session()
        os.makedirs(os.path.dirname(os.path.abspath(local_path)), exist_ok=True)
        start = time.time()
        total = 0
        with self._session.get(url, stream=True, timeout=self.timeout) as r:
            r.raise_for_status()
            with open(local_path, "wb") as f:
                for chunk in r.iter_content(chunk_size=chunk_size):
                    f.write(chunk)
                    total += len(chunk)
        elapsed = int((time.time() - start) * 1000)
        log.info(f"Downloaded {url} → {local_path} ({total} bytes, {elapsed}ms)")
        return {"local_path": local_path, "size": total, "elapsed_ms": elapsed}

    def _parse_response(self, r: Any, elapsed: int) -> Dict[str, Any]:
        result = {
            "status_code": r.status_code,
            "headers": dict(r.headers),
            "body": r.text,
            "json": None,
            "url": r.url,
            "elapsed_ms": elapsed,
            "ok": r.ok,
        }
        content_type = r.headers.get("Content-Type", "")
        if "application/json" in content_type or r.text.strip().startswith("{"):
            try:
                result["json"] = r.json()
            except Exception:
                pass
        return result

    # ── DNS ──────────────────────────────────────────────────────────

    def dns_resolve(self, hostname: str, record_type: str = "A") -> List[str]:
        """
        Resolve DNS records for a hostname.

        Args:
            hostname:    Domain name to resolve
            record_type: DNS record type (A, AAAA, MX, TXT, NS, CNAME)

        Returns:
            List of resolved records

        Example:
            >>> net.dns_resolve("google.com", "MX")
            ['smtp.google.com', ...]
        """
        try:
            import dns.resolver
            answers = dns.resolver.resolve(hostname, record_type)
            return [str(r) for r in answers]
        except ImportError:
            # Fallback: basic A record resolution via socket
            if record_type in ("A", "AAAA"):
                try:
                    family = socket.AF_INET6 if record_type == "AAAA" else socket.AF_INET
                    results = socket.getaddrinfo(hostname, None, family)
                    return list({r[4][0] for r in results})
                except socket.gaierror as e:
                    log.warning(f"DNS resolve failed for {hostname}: {e}")
                    return []
            log.warning("dnspython not installed for full DNS support. Run: pip install dnspython")
            return []

    def reverse_dns(self, ip: str) -> Optional[str]:
        """
        Perform a reverse DNS lookup.

        Returns:
            Hostname string or None

        Example:
            >>> net.reverse_dns("8.8.8.8")
            'dns.google'
        """
        try:
            return socket.gethostbyaddr(ip)[0]
        except socket.herror:
            return None

    def dns_check_propagation(self, hostname: str, expected_ip: str) -> bool:
        """
        Check if DNS has propagated to the expected IP.

        Example:
            >>> net.dns_check_propagation("mysite.com", "1.2.3.4")
        """
        resolved = self.dns_resolve(hostname, "A")
        return expected_ip in resolved

    # ── Port Scanning ────────────────────────────────────────────────

    def port_scan(
        self,
        host: str,
        ports: Optional[List[int]] = None,
        timeout: float = 1.0,
        max_workers: int = 50,
    ) -> Dict[str, Any]:
        """
        Scan TCP ports on a host.

        Args:
            host:        Target hostname or IP
            ports:       List of ports (default: common ports)
            timeout:     Connection timeout per port
            max_workers: Concurrent scan threads

        Returns:
            Dict with: host, open_ports, closed_ports, total_scanned, duration_sec

        Example:
            >>> result = net.port_scan("192.168.1.1", ports=[22, 80, 443, 3306, 5432])
        """
        if ports is None:
            ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 445,
                     3306, 3389, 5432, 6379, 8080, 8443, 27017]

        open_ports = []
        closed_ports = []
        start = time.time()

        def check_port(port: int) -> Tuple[int, bool, str]:
            try:
                with socket.create_connection((host, port), timeout=timeout):
                    return port, True, self._get_service_name(port)
            except (socket.timeout, ConnectionRefusedError, OSError):
                return port, False, ""

        log.info(f"Scanning {len(ports)} ports on {host}...")
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {executor.submit(check_port, p): p for p in ports}
            for future in concurrent.futures.as_completed(futures):
                port, is_open, service = future.result()
                if is_open:
                    open_ports.append({"port": port, "service": service, "state": "open"})
                    log.debug(f"  OPEN: {host}:{port} ({service})")
                else:
                    closed_ports.append(port)

        open_ports.sort(key=lambda x: x["port"])
        duration = round(time.time() - start, 2)
        log.info(f"Scan complete: {len(open_ports)} open ports in {duration}s")

        return {
            "host": host,
            "open_ports": open_ports,
            "closed_count": len(closed_ports),
            "total_scanned": len(ports),
            "duration_sec": duration,
        }

    def _get_service_name(self, port: int) -> str:
        SERVICES = {
            21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP",
            53: "DNS", 80: "HTTP", 110: "POP3", 143: "IMAP",
            443: "HTTPS", 445: "SMB", 3306: "MySQL", 3389: "RDP",
            5432: "PostgreSQL", 6379: "Redis", 8080: "HTTP-Alt",
            8443: "HTTPS-Alt", 27017: "MongoDB",
        }
        return SERVICES.get(port, "unknown")

    # ── SSL/TLS ──────────────────────────────────────────────────────

    def ssl_info(self, hostname: str, port: int = 443) -> Dict[str, Any]:
        """
        Inspect SSL/TLS certificate details for a host.

        Returns:
            Dict with: subject, issuer, valid_from, valid_until,
                       days_remaining, is_expired, sans, protocol_version

        Example:
            >>> cert = net.ssl_info("google.com")
            >>> print(cert["days_remaining"])
        """
        import datetime
        context = ssl.create_default_context()
        try:
            with socket.create_connection((hostname, port), timeout=self.timeout) as sock:
                with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                    cert = ssock.getpeercert()
                    protocol = ssock.version()
        except Exception as e:
            log.error(f"SSL inspection failed for {hostname}: {e}")
            return {"error": str(e)}

        not_after_str = cert.get("notAfter", "")
        not_before_str = cert.get("notBefore", "")

        def parse_date(s):
            try:
                return datetime.datetime.strptime(s, "%b %d %H:%M:%S %Y %Z")
            except Exception:
                return None

        valid_until = parse_date(not_after_str)
        now = datetime.datetime.utcnow()
        days_remaining = (valid_until - now).days if valid_until else None

        subject = dict(x[0] for x in cert.get("subject", []))
        issuer  = dict(x[0] for x in cert.get("issuer", []))

        sans = [v for (k, v) in cert.get("subjectAltName", []) if k == "DNS"]

        return {
            "hostname": hostname,
            "subject": subject,
            "issuer": issuer,
            "valid_from": not_before_str,
            "valid_until": not_after_str,
            "days_remaining": days_remaining,
            "is_expired": days_remaining is not None and days_remaining < 0,
            "expires_soon": days_remaining is not None and 0 <= days_remaining <= 30,
            "sans": sans,
            "protocol_version": protocol,
        }

    # ── IP Intelligence ──────────────────────────────────────────────

    def get_public_ip(self) -> str:
        """
        Get the current machine's public IP address.

        Example:
            >>> print(net.get_public_ip())
        """
        try:
            resp = self.get("https://api.ipify.org?format=json")
            return resp.get("json", {}).get("ip", "unknown")
        except Exception:
            try:
                resp = self.get("https://ifconfig.me/ip")
                return resp.get("body", "").strip()
            except Exception:
                return "unknown"

    def ip_info(self, ip: str) -> Dict[str, Any]:
        """
        Get geolocation and ASN info for an IP address.

        Args:
            ip: IPv4 or IPv6 address

        Returns:
            Dict with: ip, city, region, country, org, timezone, loc

        Example:
            >>> info = net.ip_info("8.8.8.8")
            >>> print(info["org"])  # "AS15169 Google LLC"
        """
        try:
            resp = self.get(f"https://ipinfo.io/{ip}/json")
            return resp.get("json") or {"ip": ip, "error": "No data"}
        except Exception as e:
            return {"ip": ip, "error": str(e)}

    # ── Availability & Health ────────────────────────────────────────

    def ping_http(self, url: str) -> Dict[str, Any]:
        """
        Check if an HTTP endpoint is reachable and measure latency.

        Returns:
            Dict with: url, status_code, ok, elapsed_ms, error

        Example:
            >>> status = net.ping_http("https://google.com")
        """
        try:
            resp = self.get(url)
            return {
                "url": url,
                "status_code": resp["status_code"],
                "ok": resp["ok"],
                "elapsed_ms": resp["elapsed_ms"],
                "error": None,
            }
        except Exception as e:
            return {"url": url, "status_code": None, "ok": False, "elapsed_ms": None, "error": str(e)}

    def monitor_urls(self, urls: List[str]) -> List[Dict[str, Any]]:
        """
        Check availability of multiple URLs concurrently.

        Returns:
            List of ping_http() results

        Example:
            >>> results = net.monitor_urls(["https://google.com", "https://github.com"])
        """
        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
            return list(executor.map(self.ping_http, urls))

    # ── Utilities ────────────────────────────────────────────────────

    def extract_links(self, html: str, base_url: str = "") -> List[str]:
        """
        Extract all hyperlinks from HTML content.

        Example:
            >>> resp = net.get("https://example.com")
            >>> links = net.extract_links(resp["body"], "https://example.com")
        """
        raw = re.findall(r'href=["\']([^"\']+)["\']', html, re.IGNORECASE)
        links = []
        for link in raw:
            if link.startswith("http"):
                links.append(link)
            elif link.startswith("/") and base_url:
                parsed = urlparse(base_url)
                links.append(f"{parsed.scheme}://{parsed.netloc}{link}")
        return list(set(links))

    def parse_robots_txt(self, base_url: str) -> Dict[str, Any]:
        """
        Fetch and parse a site's robots.txt.

        Returns:
            Dict with: disallowed, allowed, sitemaps, crawl_delay

        Example:
            >>> robots = net.parse_robots_txt("https://example.com")
        """
        url = base_url.rstrip("/") + "/robots.txt"
        try:
            resp = self.get(url)
            lines = resp["body"].splitlines()
        except Exception:
            return {"error": "Could not fetch robots.txt"}

        disallowed, allowed, sitemaps = [], [], []
        crawl_delay = None
        for line in lines:
            line = line.strip()
            if line.lower().startswith("disallow:"):
                val = line.split(":", 1)[-1].strip()
                if val:
                    disallowed.append(val)
            elif line.lower().startswith("allow:"):
                allowed.append(line.split(":", 1)[-1].strip())
            elif line.lower().startswith("sitemap:"):
                sitemaps.append(line.split(":", 1)[-1].strip())
            elif line.lower().startswith("crawl-delay:"):
                try:
                    crawl_delay = float(line.split(":", 1)[-1].strip())
                except ValueError:
                    pass

        return {
            "disallowed": disallowed,
            "allowed": allowed,
            "sitemaps": sitemaps,
            "crawl_delay": crawl_delay,
        }

    def __repr__(self) -> str:
        return f"InternetToolkit(timeout={self.timeout}, retries={self.retries})"
