"""Local capability state manager for IuVe Connect Avatar Permission System V1.

All capability grants originate from the local Agent UI.
The server cannot independently escalate capabilities.
No local consent = No privileged execution.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any


CAPABILITY_DEFINITIONS: dict[str, dict[str, Any]] = {
    "file_access": {
        "risk_level": "low",
        "scope": "Local user files (read/write within user home)",
        "default_enabled": False,
        "icon": "folder-symbolic",
    },
    "application_control": {
        "risk_level": "medium",
        "scope": "Launch and close user-space applications",
        "default_enabled": False,
        "icon": "application-x-executable-symbolic",
    },
    "mouse_control": {
        "risk_level": "high",
        "scope": "Desktop pointer and click input",
        "default_enabled": False,
        "icon": "input-mouse-symbolic",
    },
    "keyboard_control": {
        "risk_level": "high",
        "scope": "Desktop keyboard input",
        "default_enabled": False,
        "icon": "input-keyboard-symbolic",
    },
    "network_control": {
        "risk_level": "medium",
        "scope": "Network diagnostics and read-only configuration",
        "default_enabled": False,
        "icon": "network-wired-symbolic",
    },
    "software_management": {
        "risk_level": "high",
        "scope": "Package installation and software updates",
        "default_enabled": False,
        "icon": "system-software-install-symbolic",
    },
    "root_operations": {
        "risk_level": "critical",
        "scope": "Privileged OS operations (requires explicit time-boxed grant)",
        "default_enabled": False,
        "icon": "security-high-symbolic",
    },
    "screen_vision": {
        "risk_level": "high",
        "scope": "Bounded desktop screenshot for result verification (read-only)",
        "default_enabled": False,
        "icon": "camera-photo-symbolic",
    },
}

# Expiry presets. "reboot" = device shutdown; "app_exit" = until console exits.
EXPIRY_PRESETS: dict[str, int | str | None] = {
    "15m": 15,
    "1h": 60,
    "24h": 1440,
    "reboot": "reboot",
    "app_exit": "app_exit",
    "permanent": None,
    "one_shot": "one_shot",
}

SCHEMA_VERSION = "0.1.21"

HIGH_RISK_CAPABILITIES: frozenset[str] = frozenset(
    {"mouse_control", "keyboard_control", "software_management", "screen_vision"}
)
CRITICAL_CAPABILITIES: frozenset[str] = frozenset({"root_operations"})


def _get_boot_time_utc() -> datetime | None:
    """Return approximate system boot time in UTC via /proc/uptime."""
    try:
        uptime_seconds = float(Path("/proc/uptime").read_text().split()[0])
        return datetime.now(timezone.utc) - timedelta(seconds=uptime_seconds)
    except Exception:
        return None


def _is_rebooted_since(granted_at_str: str | None) -> bool:
    """Return True if the system has rebooted since the given timestamp."""
    if not granted_at_str:
        return True
    try:
        boot_time = _get_boot_time_utc()
        if boot_time is None:
            return False
        granted = datetime.fromisoformat(granted_at_str)
        if granted.tzinfo is None:
            granted = granted.replace(tzinfo=timezone.utc)
        return boot_time > granted
    except Exception:
        return False


@dataclass
class CapabilityState:
    capability_key: str
    enabled: bool = False
    granted_at: str | None = None
    expires_at: str | None = None
    revoked_at: str | None = None
    granted_by: str = "local_user"
    risk_level: str = "medium"
    scope: str = ""
    expiry_type: str = "permanent"  # timestamp | permanent | reboot | app_exit | one_shot
    expiry_preset: str = "permanent"
    one_shot_remaining: int | None = None

    def is_expired(self) -> bool:
        if not self.enabled:
            return False
        if self.expiry_type == "one_shot":
            return self.one_shot_remaining is not None and self.one_shot_remaining <= 0
        if self.expiry_type == "reboot":
            return _is_rebooted_since(self.granted_at)
        if not self.expires_at:
            return False
        try:
            exp = datetime.fromisoformat(self.expires_at)
            if exp.tzinfo is None:
                exp = exp.replace(tzinfo=timezone.utc)
            return datetime.now(timezone.utc) > exp
        except Exception:
            return False

    def status_label(self) -> str:
        if self.is_expired():
            return "expired"
        if self.enabled:
            return "enabled"
        return "disabled"

    def to_dict(self) -> dict[str, Any]:
        return {
            "capability_key": self.capability_key,
            "enabled": self.enabled and not self.is_expired(),
            "granted_at": self.granted_at,
            "expires_at": self.expires_at,
            "revoked_at": self.revoked_at,
            "granted_by": self.granted_by,
            "risk_level": self.risk_level,
            "scope": self.scope,
            "expiry_type": self.expiry_type,
            "expiry_preset": self.expiry_preset,
            "one_shot_remaining": self.one_shot_remaining,
        }


class CapabilityStateManager:
    """Manages local capability grants persisted in agent config directory.

    Authority source: LOCAL only.
    Server can read evidence; it cannot write grants.
    """

    DEFAULT_PATH = Path.home() / ".config" / "iuve-connect-agent" / "capability-state.json"
    AUDIT_PATH = Path.home() / ".config" / "iuve-connect-agent" / "capability-audit.jsonl"

    def __init__(self, state_path: Path | None = None) -> None:
        self._path = Path(state_path or self.DEFAULT_PATH).expanduser()
        self._state: dict[str, CapabilityState] = {}
        self._revoked_all_at: str | None = None
        self._always_allow_types: list[str] = []
        self._loaded_mtime_ns: int | None = None
        self._load()

    # --- Public API ---

    def reload_if_stale(self) -> bool:
        """Reload persisted grants when another process updated capability-state.json."""
        try:
            if not self._path.exists():
                return False
            mtime_ns = self._path.stat().st_mtime_ns
            if mtime_ns == self._loaded_mtime_ns:
                return False
            self._load()
            return True
        except Exception:
            return False

    def get(self, key: str) -> CapabilityState | None:
        self.reload_if_stale()
        cap = self._state.get(key)
        if cap is None:
            return None
        if cap.is_expired():
            self._mark_expired(key)
        return self._state.get(key)

    def all(self) -> dict[str, CapabilityState]:
        self.reload_if_stale()
        for key in list(self._state):
            if self._state[key].is_expired():
                self._mark_expired(key)
        return dict(self._state)

    def grant(self, key: str, *, expiry_preset: str = "permanent") -> CapabilityState | None:
        if key not in CAPABILITY_DEFINITIONS:
            return None
        preset_value = EXPIRY_PRESETS.get(expiry_preset)
        expires_at: str | None = None
        expiry_type = "permanent"
        one_shot_remaining: int | None = None
        if preset_value == "reboot":
            expiry_type = "reboot"
        elif preset_value == "app_exit":
            expiry_type = "app_exit"
        elif preset_value == "one_shot":
            expiry_type = "one_shot"
            one_shot_remaining = 1
        elif isinstance(preset_value, int):
            expires_at = (datetime.now(timezone.utc) + timedelta(minutes=preset_value)).isoformat()
            expiry_type = "timestamp"
        now = datetime.now(timezone.utc).isoformat()
        cap = self._state[key]
        cap.enabled = True
        cap.granted_at = now
        cap.revoked_at = None
        cap.expires_at = expires_at
        cap.expiry_type = expiry_type
        cap.expiry_preset = expiry_preset
        cap.one_shot_remaining = one_shot_remaining
        self._save()
        self._audit("capability_granted", key=key, expiry_preset=expiry_preset, expires_at=expires_at, expiry_type=expiry_type)
        return cap

    def revoke(self, key: str) -> CapabilityState | None:
        if key not in self._state:
            return None
        now = datetime.now(timezone.utc).isoformat()
        cap = self._state[key]
        cap.enabled = False
        cap.revoked_at = now
        self._save()
        self._audit("capability_revoked", key=key)
        return cap

    def is_enabled(self, key: str) -> bool:
        cap = self.get(key)
        return bool(cap and cap.enabled and not cap.is_expired())

    @property
    def always_allow_types(self) -> tuple[str, ...]:
        return tuple(self._always_allow_types)

    def add_always_allow_type(self, command_type: str) -> bool:
        cleaned = str(command_type or "").strip()
        if not cleaned:
            return False
        if cleaned in self._always_allow_types:
            return True
        self._always_allow_types.append(cleaned)
        self._save()
        self._audit("always_allow_type_added", command_type=cleaned)
        return True

    def consume_one_shot(self, key: str) -> CapabilityState | None:
        if key not in self._state:
            return None
        cap = self._state[key]
        if cap.expiry_type != "one_shot" or not cap.enabled:
            return cap
        if cap.one_shot_remaining is None:
            cap.one_shot_remaining = 0
        else:
            cap.one_shot_remaining = max(0, cap.one_shot_remaining - 1)
        if cap.one_shot_remaining <= 0:
            cap.enabled = False
            cap.revoked_at = datetime.now(timezone.utc).isoformat()
            self._audit("capability_one_shot_consumed", key=key)
        self._save()
        return cap

    def set_revoked_all_at(self, timestamp: str) -> None:
        self._revoked_all_at = timestamp
        self._save()
        self._audit("capabilities_revoked_all", revoked_all_at=timestamp)

    def snapshot_for_backend(self) -> dict[str, Any]:
        """Evidence-only payload for backend. Backend cannot use this to grant."""
        return {
            "source": "local_agent",
            "authority": "local_user_only",
            "snapshot_at": datetime.now(timezone.utc).isoformat(),
            "capabilities": {k: v.to_dict() for k, v in self.all().items()},
        }

    def read_audit_log(self, max_lines: int = 100) -> list[dict[str, Any]]:
        """Read recent audit events for UI display."""
        try:
            if not self.AUDIT_PATH.exists():
                return []
            lines = self.AUDIT_PATH.read_text(encoding="utf-8").splitlines()
            records = []
            for line in reversed(lines[-max_lines:]):
                line = line.strip()
                if line:
                    try:
                        records.append(json.loads(line))
                    except Exception:
                        pass
            return records
        except Exception:
            return []

    # --- Internal ---

    def _load(self) -> None:
        if not self._path.exists():
            self._state = self._default_state()
            self._loaded_mtime_ns = None
            return
        try:
            raw = json.loads(self._path.read_text(encoding="utf-8"))
            if not isinstance(raw, dict):
                self._state = self._default_state()
                return
            caps_raw = raw.get("capabilities", {})
            self._revoked_all_at = str(raw["revoked_all_at"]) if raw.get("revoked_all_at") else None
            always_allow = raw.get("always_allow_types")
            self._always_allow_types = list(always_allow) if isinstance(always_allow, list) else []
            self._state = self._default_state()
            for key, data in caps_raw.items():
                if key in CAPABILITY_DEFINITIONS and isinstance(data, dict):
                    defn = CAPABILITY_DEFINITIONS[key]
                    one_shot_raw = data.get("one_shot_remaining")
                    one_shot_remaining = (
                        int(one_shot_raw) if one_shot_raw is not None else None
                    )
                    self._state[key] = CapabilityState(
                        capability_key=key,
                        enabled=bool(data.get("enabled", False)),
                        granted_at=str(data["granted_at"]) if data.get("granted_at") else None,
                        expires_at=str(data["expires_at"]) if data.get("expires_at") else None,
                        revoked_at=str(data["revoked_at"]) if data.get("revoked_at") else None,
                        granted_by=str(data.get("granted_by") or "local_user"),
                        risk_level=str(data.get("risk_level") or defn["risk_level"]),
                        scope=str(data.get("scope") or defn["scope"]),
                        expiry_type=str(data.get("expiry_type") or "permanent"),
                        expiry_preset=str(data.get("expiry_preset") or "permanent"),
                        one_shot_remaining=one_shot_remaining,
                    )
        except Exception:
            self._state = self._default_state()
        try:
            self._loaded_mtime_ns = self._path.stat().st_mtime_ns if self._path.exists() else None
        except Exception:
            self._loaded_mtime_ns = None

    def _default_state(self) -> dict[str, CapabilityState]:
        return {
            key: CapabilityState(
                capability_key=key,
                enabled=False,
                risk_level=str(defn["risk_level"]),
                scope=str(defn["scope"]),
            )
            for key, defn in CAPABILITY_DEFINITIONS.items()
        }

    def _save(self) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        payload: dict[str, Any] = {
            "authority_source": "local_agent_ui",
            "schema_version": SCHEMA_VERSION,
            "updated_at": datetime.now(timezone.utc).isoformat(),
            "capabilities": {key: cap.to_dict() for key, cap in self._state.items()},
            "always_allow_types": list(self._always_allow_types),
            "revoked_all_at": self._revoked_all_at,
        }
        self._path.write_text(json.dumps(payload, indent=2, ensure_ascii=True), encoding="utf-8")
        try:
            self._loaded_mtime_ns = self._path.stat().st_mtime_ns
        except Exception:
            self._loaded_mtime_ns = None

    def _mark_expired(self, key: str) -> None:
        cap = self._state.get(key)
        if cap is None:
            return
        now = datetime.now(timezone.utc).isoformat()
        cap.enabled = False
        cap.revoked_at = now
        self._save()
        self._audit("capability_expired", key=key)

    def _audit(self, event: str, **fields: Any) -> None:
        record: dict[str, Any] = {
            "event": event,
            "at": datetime.now(timezone.utc).isoformat(),
            "authority": "local_user",
            **fields,
        }
        try:
            self.AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
            with open(self.AUDIT_PATH, "a", encoding="utf-8") as fh:
                fh.write(json.dumps(record, ensure_ascii=True) + "\n")
        except Exception:
            pass

