#!/usr/bin/env python3
"""Operator audit trail — append-only log with HMAC integrity chain.

Logs all operator actions: SSH sessions, CLI commands, module activations,
config changes.  Each row includes an HMAC of the previous row, forming
a tamper-evident chain.  Subscribes to MODULE_STARTED, MODULE_STOPPED,
and KILL_SWITCH events.
"""

import hashlib
import hmac
import json
import logging
import os
import sqlite3
import subprocess
import threading
import time
from typing import Optional

from modules.base import BaseModule

logger = logging.getLogger(__name__)

_SCHEMA = """
CREATE TABLE IF NOT EXISTS audit_log (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp   REAL    NOT NULL,
    action      TEXT    NOT NULL,
    operator    TEXT    DEFAULT '',
    details     TEXT    DEFAULT '',
    hmac        TEXT    NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_audit_ts     ON audit_log(timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
"""

# Action types
ACTION_MODULE_START   = "module_start"
ACTION_MODULE_STOP    = "module_stop"
ACTION_KILL_SWITCH    = "kill_switch"
ACTION_CONFIG_CHANGE  = "config_change"
ACTION_SSH_SESSION    = "ssh_session"
ACTION_CLI_COMMAND    = "cli_command"
ACTION_CRED_EXPORT    = "cred_export"
ACTION_DATA_EXFIL     = "data_exfil"
ACTION_BASELINE_BUILD = "baseline_build"
ACTION_MANUAL         = "manual"


class OperatorAudit(BaseModule):
    """Append-only operator audit trail with HMAC integrity chain."""

    name = "operator_audit"
    module_type = "intel"
    priority = 50  # Critical — always runs
    requires_root = False

    def __init__(self, bus, state, config, engine=None):
        super().__init__(bus, state, config, engine)
        self._db_path = ""
        self._conn: Optional[sqlite3.Connection] = None
        self._lock = threading.Lock()
        self._hmac_key = b""
        self._last_hmac = ""
        self._entry_count = 0

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

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

        base_dir = self.config.get("data_dir", os.path.expanduser("~/.implant"))
        self._db_path = os.path.join(base_dir, "operator_audit.db")
        os.makedirs(os.path.dirname(self._db_path), exist_ok=True)

        self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA synchronous=NORMAL")
        self._conn.row_factory = sqlite3.Row
        self._conn.executescript(_SCHEMA)

        # HMAC key: from config or derive from machine ID
        key_config = self.config.get("audit_hmac_key", "")
        if key_config:
            self._hmac_key = key_config.encode() if isinstance(key_config, str) else key_config
        else:
            self._hmac_key = self._derive_machine_key()

        # Load last HMAC from chain
        self._last_hmac = self._get_last_hmac()

        # Subscribe to events
        self.bus.subscribe(self._on_module_started, "MODULE_STARTED")
        self.bus.subscribe(self._on_module_stopped, "MODULE_STOPPED")
        self.bus.subscribe(self._on_kill_switch, "KILL_SWITCH")

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

        # Log our own start
        self.log_action(
            ACTION_MODULE_START,
            details="OperatorAudit started",
        )

        logger.info(
            "OperatorAudit started — db=%s, chain_length=%d",
            self._db_path, self._entry_count,
        )

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

        # Log our own stop before shutting down
        self.log_action(
            ACTION_MODULE_STOP,
            details="OperatorAudit stopping",
        )

        self._running = False

        self.bus.unsubscribe(self._on_module_started, "MODULE_STARTED")
        self.bus.unsubscribe(self._on_module_stopped, "MODULE_STOPPED")
        self.bus.unsubscribe(self._on_kill_switch, "KILL_SWITCH")

        if self._conn:
            self._conn.close()
            self._conn = None

        self.state.set_module_status(self.name, "stopped")
        logger.info(
            "OperatorAudit stopped — %d entries in chain", self._entry_count
        )

    def status(self) -> dict:
        return {
            "running": self._running,
            "pid": self._pid,
            "uptime": time.time() - self._start_time if self._start_time else 0,
            "entries": self._entry_count,
            "chain_valid": self.verify_chain(),
        }

    def configure(self, config: dict) -> None:
        old_config = json.dumps(self.config, sort_keys=True, default=str)
        self.config.update(config)
        new_config = json.dumps(self.config, sort_keys=True, default=str)

        if old_config != new_config:
            self.log_action(
                ACTION_CONFIG_CHANGE,
                details=f"Config updated",
            )

    # ------------------------------------------------------------------
    # Event handlers
    # ------------------------------------------------------------------

    def _on_module_started(self, event) -> None:
        p = event.payload
        module_name = p.get("module", event.source_module or "unknown")
        if module_name == self.name:
            return  # Skip our own start event (already logged)
        self.log_action(
            ACTION_MODULE_START,
            details=f"Module started: {module_name} (pid={p.get('pid', '?')})",
        )

    def _on_module_stopped(self, event) -> None:
        p = event.payload
        module_name = p.get("module", event.source_module or "unknown")
        if module_name == self.name:
            return
        self.log_action(
            ACTION_MODULE_STOP,
            details=f"Module stopped: {module_name}",
        )

    def _on_kill_switch(self, event) -> None:
        p = event.payload
        self.log_action(
            ACTION_KILL_SWITCH,
            details=f"KILL SWITCH activated: reason={p.get('reason', 'unknown')}",
        )

    # ------------------------------------------------------------------
    # Core logging
    # ------------------------------------------------------------------

    def log_action(self, action: str, operator: str = "",
                   details: str = "") -> None:
        """Log an operator action with HMAC chain integrity.

        Args:
            action: Action type (use ACTION_* constants).
            operator: Operator identifier (SSH key fingerprint, username, etc).
            details: Free-text description of the action.
        """
        if not self._conn:
            return

        if not operator:
            operator = self._detect_operator()

        ts = time.time()

        # Compute HMAC: H(key, previous_hmac | timestamp | action | operator | details)
        msg = f"{self._last_hmac}|{ts}|{action}|{operator}|{details}"
        entry_hmac = hmac.new(
            self._hmac_key, msg.encode(), hashlib.sha256
        ).hexdigest()

        with self._lock:
            try:
                self._conn.execute(
                    """INSERT INTO audit_log
                       (timestamp, action, operator, details, hmac)
                       VALUES (?, ?, ?, ?, ?)""",
                    (ts, action, operator, details, entry_hmac),
                )
                self._conn.commit()
                self._last_hmac = entry_hmac
                self._entry_count += 1
            except Exception:
                logger.exception("Failed to log audit action: %s", action)

    def log_ssh_session(self, remote_ip: str, key_fingerprint: str = "",
                        event: str = "connect") -> None:
        """Log an SSH session event."""
        self.log_action(
            ACTION_SSH_SESSION,
            operator=key_fingerprint or remote_ip,
            details=f"SSH {event} from {remote_ip} "
                    f"(key={key_fingerprint or 'password'})",
        )

    def log_cli_command(self, command: str, operator: str = "") -> None:
        """Log a CLI command execution."""
        self.log_action(
            ACTION_CLI_COMMAND,
            operator=operator,
            details=f"Command: {command}",
        )

    # ------------------------------------------------------------------
    # HMAC chain
    # ------------------------------------------------------------------

    def _derive_machine_key(self) -> bytes:
        """Derive HMAC key from machine-specific identifiers."""
        components = []

        # CPU serial (RPi)
        try:
            with open("/proc/cpuinfo", "r") as f:
                for line in f:
                    if line.startswith("Serial"):
                        components.append(line.strip())
                        break
        except (IOError, PermissionError):
            pass

        # Machine ID
        try:
            with open("/etc/machine-id", "r") as f:
                components.append(f.read().strip())
        except (IOError, PermissionError):
            pass

        if not components:
            components.append("bigbrother-default-key")

        combined = "|".join(components)
        return hashlib.sha256(combined.encode()).digest()

    def _get_last_hmac(self) -> str:
        """Get the HMAC of the last entry in the chain."""
        with self._lock:
            row = self._conn.execute(
                "SELECT hmac FROM audit_log ORDER BY id DESC LIMIT 1"
            ).fetchone()

            count_row = self._conn.execute(
                "SELECT COUNT(*) FROM audit_log"
            ).fetchone()
            self._entry_count = count_row[0] if count_row else 0

        if row:
            return row["hmac"]
        return ""  # Genesis — empty for first entry

    def _detect_operator(self) -> str:
        """Attempt to detect the current operator identity."""
        # Check SSH connection
        ssh_client = os.environ.get("SSH_CLIENT", "")
        if ssh_client:
            parts = ssh_client.split()
            remote_ip = parts[0] if parts else "unknown"

            # Try to get SSH key fingerprint
            try:
                result = subprocess.run(
                    ["who", "-m"],
                    capture_output=True, timeout=2,
                )
                who = result.stdout.decode().strip()
                if who:
                    return f"ssh:{remote_ip}:{who.split()[0]}"
            except Exception:
                pass

            return f"ssh:{remote_ip}"

        # Local operator
        user = os.environ.get("USER", os.environ.get("LOGNAME", "local"))
        return f"local:{user}"

    # ------------------------------------------------------------------
    # Chain verification
    # ------------------------------------------------------------------

    def verify_chain(self) -> bool:
        """Verify the entire HMAC chain integrity.

        Returns:
            True if the chain is valid, False if tampered.
        """
        if not self._conn:
            return False

        with self._lock:
            rows = self._conn.execute(
                "SELECT timestamp, action, operator, details, hmac "
                "FROM audit_log ORDER BY id ASC"
            ).fetchall()

        if not rows:
            return True  # Empty chain is valid

        prev_hmac = ""
        for row in rows:
            ts = row["timestamp"]
            action = row["action"]
            operator = row["operator"]
            details = row["details"]
            stored_hmac = row["hmac"]

            msg = f"{prev_hmac}|{ts}|{action}|{operator}|{details}"
            computed = hmac.new(
                self._hmac_key, msg.encode(), hashlib.sha256
            ).hexdigest()

            if computed != stored_hmac:
                logger.error(
                    "HMAC chain broken at ts=%.3f action=%s — "
                    "expected %s, got %s",
                    ts, action, computed[:16], stored_hmac[:16],
                )
                return False

            prev_hmac = stored_hmac

        return True

    # ------------------------------------------------------------------
    # Export
    # ------------------------------------------------------------------

    def export_log(self, since: float = 0, limit: int = 10000) -> list:
        """Export audit log entries for engagement reports."""
        with self._lock:
            rows = self._conn.execute(
                """SELECT * FROM audit_log
                   WHERE timestamp > ?
                   ORDER BY id ASC LIMIT ?""",
                (since, limit),
            ).fetchall()

        return [dict(r) for r in rows]

    def export_json(self, since: float = 0) -> str:
        """Export audit log as JSON string."""
        entries = self.export_log(since)
        return json.dumps(entries, indent=2, default=str)

    def get_summary(self) -> dict:
        """Return audit log summary."""
        with self._lock:
            rows = self._conn.execute(
                """SELECT action, COUNT(*) FROM audit_log
                   GROUP BY action ORDER BY COUNT(*) DESC"""
            ).fetchall()

            operators = self._conn.execute(
                """SELECT DISTINCT operator FROM audit_log
                   WHERE operator != ''"""
            ).fetchall()

        return {
            "total_entries": self._entry_count,
            "chain_valid": self.verify_chain(),
            "by_action": {a: c for a, c in rows},
            "operators": [r[0] for r in operators],
        }
