#!/usr/bin/env python3
"""
Services module for BigLinux Swap Manager.

Handles all system interactions: config parsing, memory monitoring,
swap management, storage detection, and service control.
"""

from __future__ import annotations

import contextlib
import glob
import logging
import os
import subprocess
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING

from biglinux_swap.i18n import _

import gi

gi.require_version("GLib", "2.0")
from gi.repository import GLib

if TYPE_CHECKING:
    from biglinux_swap.config import SwapConfig

from biglinux_swap.config import (
    CHART_UPDATE_INTERVAL_MS,
    CONFIG_FILE,
    DEFAULT_CONFIG,
    MEMINFO_PATH,
    Compressor,
    DiscardPolicy,
    SwapConfig,
    SwapFileInfo,
    SwapMode,
)

logger = logging.getLogger(__name__)


# =============================================================================
# Shared /proc/swaps parser
# =============================================================================


@dataclass
class ProcSwapEntry:
    """A single entry from /proc/swaps."""

    filename: str
    swap_type: str
    size_kb: int
    used_kb: int
    priority: int

    @property
    def size_bytes(self) -> int:
        return self.size_kb * 1024

    @property
    def used_bytes(self) -> int:
        return self.used_kb * 1024

    @property
    def is_zram(self) -> bool:
        return "zram" in self.filename

    @property
    def is_file(self) -> bool:
        return self.swap_type == "file" or "loop" in self.filename


def _parse_proc_swaps() -> list[ProcSwapEntry]:
    """Parse /proc/swaps into a list of swap entries."""
    entries: list[ProcSwapEntry] = []
    try:
        content = Path("/proc/swaps").read_text(encoding="utf-8")
        for line in content.strip().split("\n")[1:]:  # Skip header
            parts = line.split()
            if len(parts) >= 5:
                entries.append(
                    ProcSwapEntry(
                        filename=parts[0],
                        swap_type=parts[1],
                        size_kb=int(parts[2]),
                        used_kb=int(parts[3]),
                        priority=int(parts[4]),
                    )
                )
    except (OSError, ValueError) as e:
        logger.debug("Error parsing /proc/swaps: %s", e)
    return entries


def _get_backing_disk_bytes(device_path: str) -> int:
    """Get actual on-disk bytes for a swap device's backing file.

    For loop devices, reads the backing file path from sysfs and uses
    st_blocks to get actual compressed bytes on disk (reflects btrfs zstd).
    """
    try:
        dev_name = device_path.rsplit("/", 1)[-1]  # "/dev/loop0" → "loop0"
        backing_path = Path(f"/sys/block/{dev_name}/loop/backing_file")
        if backing_path.exists():
            backing_file = backing_path.read_text(encoding="utf-8").strip()
            st = os.stat(backing_file)
            return st.st_blocks * 512
    except (OSError, ValueError):
        pass
    return 0


# =============================================================================
# Configuration Service
# =============================================================================


class ConfigService:
    """Service for managing systemd-swap configuration files."""

    def __init__(self) -> None:
        self._config: SwapConfig | None = None

    def load(self) -> SwapConfig:
        """Load configuration from system files."""
        config = SwapConfig()

        if DEFAULT_CONFIG.exists():
            default_values = self._parse_config_file(DEFAULT_CONFIG)
            config = self._apply_values(config, default_values)

        if CONFIG_FILE.exists():
            user_values = self._parse_config_file(CONFIG_FILE)
            config = self._apply_values(config, user_values)

        self._config = config
        logger.info("Configuration loaded")
        return config

    def get(self) -> SwapConfig:
        """Get current config, loading if necessary."""
        if self._config is None:
            return self.load()
        return self._config

    def generate_config_content(self, config: SwapConfig) -> str:
        """Generate configuration file content from SwapConfig."""
        # If mode is AUTO or DISABLED, comment out all subsystem options
        is_auto_or_disabled = config.mode in (SwapMode.AUTO, SwapMode.DISABLED)
        pfx = "#" if is_auto_or_disabled else ""

        lines = [
            "# Configuration for systemd-swap",
            "# Generated by BigLinux Swap Manager",
            "",
            f"swap_mode={config.mode.value}",
            "",
            "# Zswap",
            f"{pfx}zswap_compressor={config.zswap.compressor.value}",
            f"{pfx}zswap_zpool={config.zswap.zpool}",
            f"{pfx}zswap_max_pool_percent={config.zswap.max_pool_percent}",
            f"{pfx}zswap_shrinker_enabled={1 if config.zswap.shrinker_enabled else 0}",
            f"{pfx}zswap_accept_threshold={config.zswap.accept_threshold}",
            "",
            "# Zram",
            f"{pfx}zram_size={config.zram.size_percent}%",
            f"{pfx}zram_alg={config.zram.alg.value}",
            f"{pfx}zram_prio={config.zram.priority}",
            "",
            "# SwapFile",
            f"{pfx}swapfile_enabled={1 if config.swapfile.enabled else 0}",
            f"{pfx}swapfile_path={config.swapfile.path}",
            f"{pfx}swapfile_chunk_size={config.swapfile.chunk_size}",
            f"{pfx}swapfile_max_count={config.swapfile.max_count}",
            f"{pfx}swapfile_min_count={config.swapfile.min_count}",
            f"{pfx}swapfile_free_ram_perc={config.swapfile.free_ram_perc}",
            f"{pfx}swapfile_free_swap_perc={config.swapfile.free_swap_perc}",
            f"{pfx}swapfile_remove_free_swap_perc={config.swapfile.remove_free_swap_perc}",
            f"{pfx}swapfile_discard={config.swapfile.discard_policy.value}",
            "",
        ]
        return "\n".join(lines)

    def _parse_config_file(self, path: Path) -> dict[str, str]:
        """Parse a configuration file into key-value pairs."""
        values: dict[str, str] = {}
        try:
            with open(path, encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    if "=" in line:
                        key, _, value = line.partition("=")
                        key = key.strip()
                        value = value.strip()
                        if "#" in value:
                            value = value.split("#")[0].strip()
                        if value.startswith(('"', "'")) and value.endswith(('"', "'")):
                            value = value[1:-1]
                        values[key] = value
        except OSError as e:
            logger.error("Error reading config file %s: %s", path, e)
        return values

    def _apply_values(self, config: SwapConfig, values: dict[str, str]) -> SwapConfig:
        """Apply parsed values to a SwapConfig object."""
        if "swap_mode" in values:
            with contextlib.suppress(ValueError):
                config.mode = SwapMode(values["swap_mode"].lower())

        if "zswap_compressor" in values:
            with contextlib.suppress(ValueError):
                config.zswap.compressor = Compressor(values["zswap_compressor"])

        if "zswap_max_pool_percent" in values:
            config.zswap.max_pool_percent = self._parse_int(
                values["zswap_max_pool_percent"], config.zswap.max_pool_percent
            )

        if "zswap_zpool" in values:
            config.zswap.zpool = values["zswap_zpool"]

        if "zswap_shrinker_enabled" in values:
            config.zswap.shrinker_enabled = self._parse_bool(
                values["zswap_shrinker_enabled"]
            )

        if "zswap_accept_threshold" in values:
            config.zswap.accept_threshold = self._parse_int(
                values["zswap_accept_threshold"], config.zswap.accept_threshold
            )

        if "zram_size" in values:
            config.zram.size_percent = self._parse_percent(
                values["zram_size"], config.zram.size_percent
            )

        if "zram_alg" in values:
            with contextlib.suppress(ValueError):
                config.zram.alg = Compressor(values["zram_alg"])

        if "zram_prio" in values:
            config.zram.priority = self._parse_int(
                values["zram_prio"], config.zram.priority
            )

        if "swapfile_enabled" in values:
            config.swapfile.enabled = self._parse_bool(values["swapfile_enabled"])

        if "swapfile_path" in values:
            config.swapfile.path = values["swapfile_path"]

        if "swapfile_chunk_size" in values:
            config.swapfile.chunk_size = values["swapfile_chunk_size"]

        if "swapfile_max_count" in values:
            config.swapfile.max_count = self._parse_int(
                values["swapfile_max_count"], config.swapfile.max_count
            )

        # swapfile_min_count (with swapfc_ legacy fallback)
        min_count_val = values.get("swapfile_min_count") or values.get(
            "swapfc_min_count"
        )
        if min_count_val is not None:
            config.swapfile.min_count = self._parse_int(
                min_count_val, config.swapfile.min_count
            )

        # swapfc_ legacy fallbacks for keys already parsed above
        if "swapfile_chunk_size" not in values and "swapfc_chunk_size" in values:
            config.swapfile.chunk_size = values["swapfc_chunk_size"]
        if "swapfile_max_count" not in values and "swapfc_max_count" in values:
            config.swapfile.max_count = self._parse_int(
                values["swapfc_max_count"], config.swapfile.max_count
            )
        if "swapfile_enabled" not in values and "swapfc_enabled" in values:
            config.swapfile.enabled = self._parse_bool(values["swapfc_enabled"])
        if "swapfile_path" not in values and "swapfc_path" in values:
            config.swapfile.path = values["swapfc_path"]

        # Dynamic thresholds (with swapfc_ legacy fallback)
        for key, attr in [
            ("swapfile_free_ram_perc", "free_ram_perc"),
            ("swapfile_free_swap_perc", "free_swap_perc"),
            ("swapfile_remove_free_swap_perc", "remove_free_swap_perc"),
        ]:
            legacy = key.replace("swapfile_", "swapfc_")
            val = values.get(key) or values.get(legacy)
            if val is not None:
                setattr(
                    config.swapfile,
                    attr,
                    self._parse_int(val, getattr(config.swapfile, attr)),
                )

        discard_val = values.get("swapfile_discard") or values.get("swapfc_discard")
        if discard_val is not None:
            with contextlib.suppress(ValueError):
                config.swapfile.discard_policy = DiscardPolicy(discard_val.lower())

        return config

    def _parse_int(self, value: str, default: int) -> int:
        try:
            return int(value.rstrip("%"))
        except ValueError:
            return default

    def _parse_percent(self, value: str, default: int) -> int:
        try:
            return int(value.rstrip("%"))
        except ValueError:
            return default

    def _parse_bool(self, value: str) -> bool:
        return value.lower() in ("yes", "y", "1", "true")


# =============================================================================
# Memory Statistics
# =============================================================================


@dataclass
class SwapDeviceStats:
    """Statistics for a single swap device."""

    path: str = ""
    size_bytes: int = 0
    used_bytes: int = 0
    priority: int = 0

    @property
    def is_zram(self) -> bool:
        return "zram" in self.path


@dataclass
class MemoryStats:
    """Memory statistics from /proc/meminfo and /proc/swaps."""

    mem_total: int = 0
    mem_available: int = 0
    mem_buffers: int = 0
    mem_cached: int = 0
    swap_total: int = 0
    swap_free: int = 0
    zswap_pool: int = 0
    zswap_stored: int = 0
    zram_used: int = 0
    swapfile_used: int = 0
    swapfile_disk_actual: int = 0  # actual compressed bytes on disk (from st_blocks)
    swap_devices: list[SwapDeviceStats] = field(default_factory=list)

    @property
    def mem_used(self) -> int:
        return self.mem_total - self.mem_available

    @property
    def swap_used(self) -> int:
        return self.swap_total - self.swap_free

    @property
    def swap_disk_used(self) -> int:
        if self.zswap_stored > 0:
            return max(0, self.swap_used - self.zswap_stored)
        elif self.zram_used > 0:
            return self.swapfile_used
        return self.swap_used

    @property
    def swap_in_ram(self) -> int:
        if self.zswap_stored > 0:
            return self.zswap_stored
        return self.zram_used

    @property
    def mem_used_percent(self) -> float:
        if self.mem_total == 0:
            return 0.0
        return (self.mem_used / self.mem_total) * 100.0

    @property
    def swap_used_percent(self) -> float:
        if self.swap_total == 0:
            return 0.0
        return (self.swap_used / self.swap_total) * 100.0

    def format_size(self, size_bytes: int) -> str:
        units = ["B", "KiB", "MiB", "GiB", "TiB"]
        unit_index = 0
        size = float(size_bytes)
        while size >= 1024 and unit_index < len(units) - 1:
            size /= 1024
            unit_index += 1
        if unit_index == 0:
            return f"{int(size)} {units[unit_index]}"
        return f"{size:.1f} {units[unit_index]}"

    @property
    def mem_total_formatted(self) -> str:
        return self.format_size(self.mem_total)

    @property
    def mem_used_formatted(self) -> str:
        return self.format_size(self.mem_used)

    @property
    def swap_total_formatted(self) -> str:
        return self.format_size(self.swap_total)

    @property
    def swap_ram_formatted(self) -> str:
        return self.format_size(self.swap_in_ram)

    @property
    def swap_disk_formatted(self) -> str:
        logical = self.swap_disk_used
        actual = self.swapfile_disk_actual
        if actual > 0 and actual < logical:
            return f"{self.format_size(actual)} ({self.format_size(logical)} logical)"
        return self.format_size(logical)

    @property
    def swap_ram_percent(self) -> float:
        if self.swap_total == 0:
            return 0.0
        return (self.swap_in_ram / self.swap_total) * 100.0

    @property
    def swap_disk_percent(self) -> float:
        if self.swap_total == 0:
            return 0.0
        return (self.swap_disk_used / self.swap_total) * 100.0


class MeminfoService:
    """Service for monitoring memory usage from /proc/meminfo."""

    def __init__(self) -> None:
        self._timer_id: int = 0
        self._callback: Callable[[MemoryStats], None] | None = None

    def get_stats(self) -> MemoryStats:
        """Read current memory statistics."""
        stats = MemoryStats()

        try:
            with open(MEMINFO_PATH, encoding="utf-8") as f:
                for line in f:
                    parts = line.split()
                    if len(parts) < 2:
                        continue
                    key = parts[0].rstrip(":")
                    value_bytes = int(parts[1]) * 1024

                    if key == "MemTotal":
                        stats.mem_total = value_bytes
                    elif key == "MemFree":
                        stats.mem_free = value_bytes
                    elif key == "MemAvailable":
                        stats.mem_available = value_bytes
                    elif key == "Buffers":
                        stats.mem_buffers = value_bytes
                    elif key == "Cached":
                        stats.mem_cached = value_bytes
                    elif key == "SwapTotal":
                        stats.swap_total = value_bytes
                    elif key == "SwapFree":
                        stats.swap_free = value_bytes
                    elif key == "Zswap":
                        stats.zswap_pool = value_bytes
                    elif key == "Zswapped":
                        stats.zswap_stored = value_bytes
        except (OSError, ValueError) as e:
            logger.error("Error reading meminfo: %s", e)

        self._read_swap_devices(stats)
        return stats

    def _read_swap_devices(self, stats: MemoryStats) -> None:
        """Read /proc/swaps to get detailed swap device info."""
        entries = _parse_proc_swaps()
        zram_used = 0
        disk_used = 0
        disk_actual = 0
        devices: list[SwapDeviceStats] = []

        for entry in entries:
            device = SwapDeviceStats(
                path=entry.filename,
                size_bytes=entry.size_bytes,
                used_bytes=entry.used_bytes,
                priority=entry.priority,
            )
            devices.append(device)

            if entry.is_zram:
                zram_used += entry.used_bytes
            else:
                disk_used += entry.used_bytes
                disk_actual += _get_backing_disk_bytes(entry.filename)

        stats.zram_used = zram_used
        stats.swapfile_used = disk_used
        stats.swapfile_disk_actual = disk_actual
        stats.swap_devices = devices

    def start_monitoring(
        self,
        callback: Callable[[MemoryStats], None],
        interval_ms: int = CHART_UPDATE_INTERVAL_MS,
    ) -> None:
        """Start periodic monitoring."""
        self.stop_monitoring()
        self._callback = callback

        def _update() -> bool:
            if self._callback is not None:
                stats = self.get_stats()
                self._callback(stats)
            return True

        self._timer_id = GLib.timeout_add(interval_ms, _update)
        _update()

    def stop_monitoring(self) -> None:
        """Stop periodic monitoring."""
        if self._timer_id:
            GLib.source_remove(self._timer_id)
            self._timer_id = 0
        self._callback = None


# =============================================================================
# Swap Service Status
# =============================================================================


class ServiceState(Enum):
    """systemd service states."""

    ACTIVE = "active"
    INACTIVE = "inactive"
    FAILED = "failed"
    UNKNOWN = "unknown"


@dataclass
class ZswapStatus:
    """Zswap status information."""

    enabled: bool = False
    compressor: str = ""
    zpool: str = ""
    max_pool_percent: int = 0
    pool_size_bytes: int = 0
    stored_data_bytes: int = 0


@dataclass
class ZramStatus:
    """Zram status information."""

    enabled: bool = False
    total_size_bytes: int = 0
    used_bytes: int = 0


@dataclass
class SwapFileStatus:
    """SwapFile status information."""

    enabled: bool = False
    file_count: int = 0
    max_files: int = 32
    total_size_bytes: int = 0
    files: list[SwapFileInfo] = field(default_factory=list)


@dataclass
class SwapStatus:
    """Complete swap status information."""

    service_state: ServiceState = ServiceState.UNKNOWN
    mode: str = ""
    zswap: ZswapStatus = field(default_factory=ZswapStatus)
    zram: ZramStatus = field(default_factory=ZramStatus)
    swapfile: SwapFileStatus = field(default_factory=SwapFileStatus)


class SwapService:
    """Service for interacting with systemd-swap."""

    def get_status(self, mem_stats: MemoryStats | None = None) -> SwapStatus:
        """Get current swap status. Pass mem_stats to avoid re-reading /proc/meminfo."""
        status = SwapStatus()
        status.service_state = self._get_service_state()

        try:
            result = subprocess.run(
                ["systemd-swap", "status"],
                capture_output=True,
                text=True,
                timeout=10,
            )
            if result.returncode == 0:
                self._parse_status_output(status, result.stdout)
        except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as e:
            logger.warning("systemd-swap status: %s", e)

        # Read zswap statistics from sysfs (pool/stored from mem_stats if available)
        self._read_zswap_stats(status, mem_stats)

        # Read zram + swap file info from sysfs and /proc/swaps (single read)
        self._read_zram_and_swapfile_stats(status)

        return status

    def _read_zswap_stats(
        self, status: SwapStatus, mem_stats: MemoryStats | None = None
    ) -> None:
        """Read zswap parameters from sysfs. Pool/stored bytes from mem_stats if available."""
        try:
            # Check if zswap is enabled via sysfs
            zswap_enabled_path = Path("/sys/module/zswap/parameters/enabled")
            if zswap_enabled_path.exists():
                enabled_val = zswap_enabled_path.read_text().strip()
                status.zswap.enabled = enabled_val in ("Y", "1")

            # Use pre-read meminfo values if available, else read /proc/meminfo
            if mem_stats is not None:
                status.zswap.pool_size_bytes = mem_stats.zswap_pool
                status.zswap.stored_data_bytes = mem_stats.zswap_stored
            else:
                with open(MEMINFO_PATH, encoding="utf-8") as f:
                    for line in f:
                        parts = line.split()
                        if len(parts) < 2:
                            continue
                        key = parts[0].rstrip(":")
                        value_bytes = int(parts[1]) * 1024
                        if key == "Zswap":
                            status.zswap.pool_size_bytes = value_bytes
                        elif key == "Zswapped":
                            status.zswap.stored_data_bytes = value_bytes

            # Read additional parameters from sysfs
            params_path = Path("/sys/module/zswap/parameters")
            if params_path.is_dir():
                compressor_file = params_path / "compressor"
                if compressor_file.exists():
                    status.zswap.compressor = compressor_file.read_text().strip()

                max_pool_file = params_path / "max_pool_percent"
                if max_pool_file.exists():
                    status.zswap.max_pool_percent = int(
                        max_pool_file.read_text().strip()
                    )

        except (OSError, ValueError) as e:
            logger.debug("Error reading zswap stats: %s", e)

    def _read_zram_and_swapfile_stats(self, status: SwapStatus) -> None:
        """Read zram + swap file stats from sysfs and /proc/swaps in a single pass."""
        # Read zram total capacity from sysfs
        try:
            total_zram_size = 0
            for disksize_path in glob.glob("/sys/block/zram*/disksize"):
                with open(disksize_path, encoding="utf-8") as f:
                    total_zram_size += int(f.read().strip())
            if total_zram_size > 0:
                status.zram.enabled = True
                status.zram.total_size_bytes = total_zram_size
        except (OSError, ValueError) as e:
            logger.debug("Error reading zram sysfs: %s", e)

        # Use shared parser for /proc/swaps
        entries = _parse_proc_swaps()
        zram_used = 0
        swap_files: list[SwapFileInfo] = []

        for entry in entries:
            if entry.is_zram:
                zram_used += entry.used_bytes
            elif entry.is_file:
                swap_files.append(
                    SwapFileInfo(
                        path=entry.filename,
                        size_bytes=entry.size_bytes,
                        used_bytes=entry.used_bytes,
                        is_active=True,
                        priority=entry.priority,
                    )
                )

        status.zram.used_bytes = zram_used
        status.swapfile.files = swap_files
        status.swapfile.file_count = len(swap_files)
        if swap_files:
            status.swapfile.enabled = True
            status.swapfile.total_size_bytes = sum(f.size_bytes for f in swap_files)

    def _get_service_state(self) -> ServiceState:
        """Get the state of the systemd-swap service."""
        try:
            result = subprocess.run(
                ["systemctl", "is-active", "systemd-swap"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            state = result.stdout.strip().lower()
            return {
                "active": ServiceState.ACTIVE,
                "inactive": ServiceState.INACTIVE,
                "failed": ServiceState.FAILED,
            }.get(state, ServiceState.UNKNOWN)
        except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
            return ServiceState.UNKNOWN

    def _parse_status_output(self, status: SwapStatus, output: str) -> None:
        """Parse the output of systemd-swap status."""
        current_section = None
        for line in output.strip().split("\n"):
            line = line.strip()
            if line.startswith("Zswap:"):
                current_section = "zswap"
                status.zswap.enabled = True
            elif line.startswith("Zram:"):
                current_section = "zram"
                status.zram.enabled = True
            elif line.lower().startswith(("swapfc:", "swapfile:")):
                current_section = "swapfile"
                status.swapfile.enabled = True

            if ":" in line and current_section == "zswap":
                key, _, value = line.partition(":")
                key = key.strip().lower().replace(" ", "_")
                value = value.strip()
                if key == "compressor":
                    status.zswap.compressor = value
                elif key == "zpool":
                    status.zswap.zpool = value
                elif key == "max_pool_percent":
                    status.zswap.max_pool_percent = int(value.rstrip("%") or 0)

    def restart_service(self) -> tuple[bool, str]:
        """Restart the systemd-swap service."""
        try:
            result = subprocess.run(
                ["systemctl", "restart", "systemd-swap"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                return True, _("Service restarted")
            return False, result.stderr.strip() or _("Unknown error")
        except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as e:
            return False, str(e)

    def has_any_swap_in_use(self) -> bool:
        """
        Check if there is ANY swap (files, zram, partitions) with data in use.

        More conservative check - if any swap has data, it's safer to restart
        the system than to risk freezes by stopping active swap.
        """
        entries = _parse_proc_swaps()
        if not entries:
            # If we can't read, assume swap is in use for safety
            return True
        return any(entry.used_kb > 0 for entry in entries)


# =============================================================================
# Public API
# =============================================================================

__all__ = [
    "ConfigService",
    "MeminfoService",
    "MemoryStats",
    "ServiceState",
    "SwapDeviceStats",
    "SwapFileStatus",
    "SwapService",
    "SwapStatus",
    "ZramStatus",
    "ZswapStatus",
]
