"""
RBSP Cross-Platform Abstraction Layer.

Provides unified file system operations, process management, terminal
detection, and platform-specific optimizations across Windows, Linux,
and macOS.
"""

from __future__ import annotations

import os
import sys
import stat
import shutil
import subprocess
import threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import (
    Callable,
    Dict,
    Iterator,
    List,
    Optional,
    Set,
    Tuple,
)


# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class PlatformInfo:
    """Immutable snapshot of the current platform characteristics."""

    system: str          # "Windows", "Linux", "Darwin"
    is_windows: bool
    is_linux: bool
    is_macos: bool
    path_separator: str  # os.sep
    supports_symlinks: bool
    supports_ansi: bool
    pid: int
    cpu_count: int

    @classmethod
    def detect(cls) -> "PlatformInfo":
        """Build a ``PlatformInfo`` for the running platform."""
        system = _detect_system()
        is_windows = system == "Windows"
        return cls(
            system=system,
            is_windows=is_windows,
            is_linux=system == "Linux",
            is_macos=system == "Darwin",
            path_separator=os.sep,
            supports_symlinks=_check_symlink_support(),
            supports_ansi=_check_ansi_support(),
            pid=os.getpid(),
            cpu_count=os.cpu_count() or 1,
        )


def _detect_system() -> str:
    """Return a canonical system name."""
    name = sys.platform
    if name.startswith("win"):
        return "Windows"
    if name.startswith("linux"):
        return "Linux"
    if name == "darwin":
        return "Darwin"
    return name.capitalize()


def _check_symlink_support() -> bool:
    """Return True if the OS can create symbolic links."""
    if sys.platform.startswith("win"):
        # On Windows, symlinks require either admin privileges or
        # Developer Mode.  We conservatively report False unless the
        # environment tells us otherwise.
        return os.environ.get("RBSP_SYMLINKS", "0") == "1"
    return True


def _check_ansi_support() -> bool:
    """Return True if the terminal likely supports ANSI escape codes."""
    # Non-interactive sessions (pipes, CI) -> no color
    if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
        return False

    # Explicit user override
    if os.environ.get("NO_COLOR"):
        return False
    if os.environ.get("FORCE_COLOR"):
        return True

    # Windows 10+ supports ANSI via VT processing
    if sys.platform.startswith("win"):
        return _enable_windows_ansi()

    # Unix terminals almost universally support ANSI
    term = os.environ.get("TERM", "")
    return term != "dumb"


def _enable_windows_ansi() -> bool:
    """Try to enable VT processing on Windows and return success."""
    try:
        import ctypes  # noqa: WPS433 — conditional import is intentional
        kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
        STD_OUTPUT_HANDLE = -11
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
        handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
        mode = ctypes.c_ulong()
        kernel32.GetConsoleMode(handle, ctypes.byref(mode))
        kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
        return True
    except Exception:
        return False


# Singleton — created once on import
PLATFORM = PlatformInfo.detect()


# ---------------------------------------------------------------------------
# ANSI color helpers
# ---------------------------------------------------------------------------

class _AnsiCodes:
    """ANSI escape code constants, disabled when the terminal lacks support."""

    def __init__(self, enabled: bool) -> None:
        self.enabled = enabled

    def _code(self, value: str) -> str:
        return value if self.enabled else ""

    @property
    def reset(self) -> str:
        return self._code("\033[0m")

    @property
    def bold(self) -> str:
        return self._code("\033[1m")

    @property
    def dim(self) -> str:
        return self._code("\033[2m")

    @property
    def red(self) -> str:
        return self._code("\033[31m")

    @property
    def green(self) -> str:
        return self._code("\033[32m")

    @property
    def yellow(self) -> str:
        return self._code("\033[33m")

    @property
    def blue(self) -> str:
        return self._code("\033[34m")

    @property
    def magenta(self) -> str:
        return self._code("\033[35m")

    @property
    def cyan(self) -> str:
        return self._code("\033[36m")

    @property
    def white(self) -> str:
        return self._code("\033[37m")

    @property
    def gray(self) -> str:
        return self._code("\033[90m")


ANSI = _AnsiCodes(PLATFORM.supports_ansi)


# ---------------------------------------------------------------------------
# File system abstraction
# ---------------------------------------------------------------------------

class FileSystem:
    """Cross-platform file system operations.

    All paths are normalized to ``pathlib.Path`` internally and returned
    as ``Path`` objects.
    """

    # File extensions considered "text" for indexing purposes
    TEXT_EXTENSIONS: Set[str] = {
        ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cpp", ".h",
        ".hpp", ".cs", ".go", ".rs", ".rb", ".php", ".swift", ".kt",
        ".scala", ".lua", ".r", ".m", ".pl", ".sh", ".bash", ".zsh",
        ".fish", ".ps1", ".bat", ".cmd",
        ".html", ".htm", ".css", ".scss", ".less", ".xml", ".svg",
        ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
        ".md", ".rst", ".txt", ".tex", ".adoc",
        ".sql", ".graphql", ".proto",
        ".dockerfile", ".makefile", ".cmake",
    }

    IGNORED_DIRS: Set[str] = {
        ".git", ".svn", ".hg", "__pycache__", "node_modules",
        ".tox", ".nox", ".mypy_cache", ".pytest_cache",
        ".rbsp", "venv", ".venv", "env", ".env",
        "dist", "build", "egg-info",
    }

    @staticmethod
    def normalize(path: str | Path) -> Path:
        """Return an absolute, resolved path."""
        return Path(path).resolve()

    @classmethod
    def walk_files(
        cls,
        root: str | Path,
        extensions: Optional[Set[str]] = None,
        ignored_dirs: Optional[Set[str]] = None,
    ) -> Iterator[Path]:
        """Yield all files under *root* matching the given extensions.

        Directories in *ignored_dirs* are skipped entirely.
        """
        root_path = cls.normalize(root)
        exts = extensions if extensions is not None else cls.TEXT_EXTENSIONS
        skip = ignored_dirs if ignored_dirs is not None else cls.IGNORED_DIRS

        for dirpath, dirnames, filenames in os.walk(root_path, topdown=True):
            # Prune ignored directories in-place
            dirnames[:] = [
                d for d in dirnames
                if d.lower() not in skip and not d.startswith(".")
            ]
            for fname in filenames:
                if not exts or Path(fname).suffix.lower() in exts:
                    yield Path(dirpath) / fname

    @staticmethod
    def read_text(path: str | Path, encoding: str = "utf-8") -> str:
        """Read a text file, returning an empty string on decode errors."""
        p = Path(path)
        try:
            return p.read_text(encoding=encoding, errors="replace")
        except OSError:
            return ""

    @staticmethod
    def read_bytes(path: str | Path) -> bytes:
        """Read a file as raw bytes."""
        return Path(path).read_bytes()

    @staticmethod
    def write_text(path: str | Path, content: str, encoding: str = "utf-8") -> None:
        """Write text to a file, creating parent directories if needed."""
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(content, encoding=encoding)

    @staticmethod
    def write_bytes(path: str | Path, data: bytes) -> None:
        """Write bytes to a file, creating parent directories if needed."""
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_bytes(data)

    @staticmethod
    def ensure_dir(path: str | Path) -> Path:
        """Create directory (and parents) if it doesn't exist. Return the Path."""
        p = Path(path)
        p.mkdir(parents=True, exist_ok=True)
        return p

    @staticmethod
    def remove(path: str | Path) -> bool:
        """Remove a file or directory tree. Return True if something was removed."""
        p = Path(path)
        if p.is_file():
            p.unlink()
            return True
        if p.is_dir():
            shutil.rmtree(p)
            return True
        return False

    @staticmethod
    def file_info(path: str | Path) -> Optional[Dict[str, object]]:
        """Return basic file metadata, or None if the file doesn't exist."""
        p = Path(path)
        try:
            st = p.stat()
            return {
                "path": str(p),
                "size": st.st_size,
                "modified": st.st_mtime,
                "created": st.st_ctime,
                "is_file": p.is_file(),
                "is_dir": p.is_dir(),
                "is_symlink": p.is_symlink(),
            }
        except OSError:
            return None

    @staticmethod
    def relative_to(path: str | Path, base: str | Path) -> str:
        """Return a POSIX-style relative path string from *base* to *path*."""
        try:
            return Path(path).resolve().relative_to(Path(base).resolve()).as_posix()
        except ValueError:
            return str(path)


# ---------------------------------------------------------------------------
# Process management
# ---------------------------------------------------------------------------

class ProcessManager:
    """Manage background processes (e.g., file watchers)."""

    def __init__(self) -> None:
        self._processes: Dict[str, subprocess.Popen] = {}  # type: ignore[type-arg]
        self._threads: Dict[str, threading.Thread] = {}
        self._lock = threading.Lock()

    def start_thread(
        self,
        name: str,
        target: Callable[..., None],
        args: Tuple[object, ...] = (),
        daemon: bool = True,
    ) -> bool:
        """Start a named daemon thread. Return False if *name* is already running."""
        with self._lock:
            if name in self._threads and self._threads[name].is_alive():
                return False
            t = threading.Thread(target=target, args=args, name=name, daemon=daemon)
            self._threads[name] = t
            t.start()
            return True

    def stop_thread(self, name: str, timeout: float = 5.0) -> bool:
        """Signal a thread to stop (by name). Returns True if it ended."""
        with self._lock:
            t = self._threads.pop(name, None)
        if t is None:
            return True
        t.join(timeout=timeout)
        return not t.is_alive()

    def is_running(self, name: str) -> bool:
        """Check whether a named thread is alive."""
        with self._lock:
            t = self._threads.get(name)
            return t is not None and t.is_alive()

    def list_running(self) -> List[str]:
        """Return names of all running threads."""
        with self._lock:
            return [
                name for name, t in self._threads.items() if t.is_alive()
            ]

    def cleanup(self) -> None:
        """Remove references to dead threads."""
        with self._lock:
            self._threads = {
                name: t for name, t in self._threads.items() if t.is_alive()
            }
