"""
Doit Agent - Security Layer
Handles encryption, authorization, lock files, and injection protection.
"""
from __future__ import annotations

import json
import logging
import os
import sys
from pathlib import Path
from typing import Any, Optional

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64

from core.config import CONFIG_PATH, LOCK_FILE, BLOCKED_PATHS, BLOCKED_COMMANDS

logger = logging.getLogger("doit.security")


class ConfigStore:
    """Encrypted configuration storage using Fernet symmetric encryption."""

    def __init__(self, config_path: Path = None, key_path: Path = None):
        from core.config import CONFIG_PATH
        self.config_path = config_path or CONFIG_PATH
        self.KEY_FILE = key_path or (self.config_path.parent / ".keyfile")
        self._fernet: Optional[Fernet] = None
        self._data: dict = {}

    def _ensure_key(self) -> Fernet:
        if self._fernet:
            return self._fernet
        self.KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
        if self.KEY_FILE.exists():
            key = self.KEY_FILE.read_bytes()
        else:
            key = Fernet.generate_key()
            self.KEY_FILE.write_bytes(key)
            # Restrict permissions on Unix
            if sys.platform != "win32":
                os.chmod(self.KEY_FILE, 0o600)
        self._fernet = Fernet(key)
        return self._fernet

    def load(self) -> dict:
        """Load and decrypt configuration."""
        if not self.config_path.exists():
            self._data = {}
            return self._data
        try:
            f = self._ensure_key()
            encrypted = self.config_path.read_bytes()
            decrypted = f.decrypt(encrypted)
            self._data = json.loads(decrypted)
        except Exception as e:
            logger.error("Failed to decrypt config: %s", e)
            self._data = {}
        return self._data

    def save(self, data: dict) -> None:
        """Encrypt and persist configuration."""
        self.config_path.parent.mkdir(parents=True, exist_ok=True)
        f = self._ensure_key()
        plaintext = json.dumps(data).encode()
        encrypted = f.encrypt(plaintext)
        self.config_path.write_bytes(encrypted)
        if sys.platform != "win32":
            os.chmod(self.config_path, 0o600)
        self._data = data

    def get(self, key: str, default: Any = None) -> Any:
        return self._data.get(key, default)

    def set(self, key: str, value: Any) -> None:
        self._data[key] = value
        self.save(self._data)

    def all(self) -> dict:
        return dict(self._data)


class LockFile:
    """Prevent duplicate instances via PID lock file."""

    def __init__(self, path: Path = LOCK_FILE):
        self.path = path

    def acquire(self) -> bool:
        """Acquire lock. Returns False if another instance is running."""
        if self.path.exists():
            try:
                pid = int(self.path.read_text().strip())
                if _pid_running(pid):
                    logger.error("Another doit instance is running (PID %d)", pid)
                    return False
                else:
                    # Stale lock
                    self.path.unlink()
            except (ValueError, OSError):
                self.path.unlink(missing_ok=True)

        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(str(os.getpid()))
        return True

    def release(self) -> None:
        self.path.unlink(missing_ok=True)


class AuthorizationManager:
    """Single-user authorization enforcement."""

    def __init__(self, config: ConfigStore):
        self._config = config

    def authorized_user_id(self) -> Optional[int]:
        uid = self._config.get("telegram_user_id")
        return int(uid) if uid else None

    def is_authorized(self, user_id: int) -> bool:
        authorized = self.authorized_user_id()
        return authorized is not None and user_id == authorized

    def deny_message(self) -> str:
        return "⛔ Unauthorized. This bot is private."


class InjectionGuard:
    """Protects against prompt injection and command injection attacks."""

    # Suspicious patterns in user messages that might try to override system behavior
    INJECTION_PATTERNS = [
        "ignore previous instructions",
        "ignore all previous",
        "disregard your instructions",
        "you are now",
        "new persona",
        "jailbreak",
        "dan mode",
        "developer mode",
        "override safety",
        "bypass restrictions",
        "ignore your training",
        "pretend you are",
        "act as if you have no restrictions",
    ]

    @classmethod
    def check_prompt(cls, text: str) -> tuple[bool, str]:
        """
        Check for prompt injection attempts.
        Returns (is_safe, reason).
        """
        lower = text.lower()
        for pattern in cls.INJECTION_PATTERNS:
            if pattern in lower:
                return False, f"Potential injection detected: '{pattern}'"
        return True, ""

    @classmethod
    def check_path(cls, path: str) -> tuple[bool, str]:
        """Validate file path against blocked paths."""
        import os
        try:
            resolved = str(Path(path).resolve())
        except Exception:
            return False, "Invalid path"

        for blocked in BLOCKED_PATHS:
            if resolved.startswith(blocked) or resolved == blocked:
                return False, f"Blocked path: {blocked}"

        # Prevent path traversal
        if ".." in path:
            return False, "Path traversal attempt"

        return True, ""

    @classmethod
    def check_command(cls, cmd: str) -> tuple[bool, str]:
        """Validate shell command against blocklist."""
        lower = cmd.lower().strip()
        for blocked in BLOCKED_COMMANDS:
            if blocked.lower() in lower:
                return False, f"Blocked command pattern: {blocked}"
        return True, ""


def _pid_running(pid: int) -> bool:
    """Check if a process with the given PID is running."""
    try:
        if sys.platform == "win32":
            import ctypes
            PROCESS_QUERY_INFORMATION = 0x0400
            handle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
            if handle:
                ctypes.windll.kernel32.CloseHandle(handle)
                return True
            return False
        else:
            os.kill(pid, 0)
            return True
    except (OSError, ProcessLookupError):
        return False


# Singleton instances
_config_store: Optional[ConfigStore] = None


def get_config() -> ConfigStore:
    global _config_store
    if _config_store is None:
        _config_store = ConfigStore()
        _config_store.load()
    return _config_store
