from __future__ import annotations

import json
import logging
import os
import shutil
import stat
import subprocess
import tempfile
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

from .config import AppPaths
from .models import TaskDefinition, TaskResult
from .system_info import SystemInfo


class SecureCheckError(RuntimeError):
    """Raised when a task cannot be completed."""


@dataclass
class CommandResult:
    command: list[str]
    returncode: int
    stdout: str
    stderr: str


@dataclass
class PackageOperation:
    requested: list[str]
    installed: list[str]
    already_present: list[str]

    @property
    def changed(self) -> bool:
        return bool(self.installed)


@dataclass
class ExecutionContext:
    paths: AppPaths
    system: SystemInfo
    logger: logging.Logger
    dry_run: bool = False
    assume_yes: bool = True

    def __post_init__(self) -> None:
        self.runner = CommandRunner(self)

    def info(self, message: str) -> None:
        print(message)
        self.logger.info(message)

    def warning(self, message: str) -> None:
        print(f"WARNING: {message}")
        self.logger.warning(message)

    def error(self, message: str) -> None:
        print(f"ERROR: {message}")
        self.logger.error(message)

    def make_result(
        self,
        task: TaskDefinition,
        *,
        success: bool,
        changed: bool,
        started_at: datetime,
        details: list[str] | None = None,
        error: str | None = None,
    ) -> TaskResult:
        return TaskResult(
            key=task.key,
            label=task.label,
            success=success,
            changed=changed,
            started_at=started_at,
            finished_at=datetime.now(),
            details=details or [],
            error=error,
        )


class CommandRunner:
    def __init__(self, context: ExecutionContext) -> None:
        self.context = context
        self._package_index_updated = False

    def command_exists(self, command: str) -> bool:
        return shutil.which(command) is not None

    def run(
        self,
        command: list[str],
        *,
        requires_root: bool = False,
        run_as_user: str | None = None,
        check: bool = True,
        capture_output: bool = True,
        env: dict[str, str] | None = None,
        input_text: str | None = None,
    ) -> CommandResult:
        final_command = list(command)
        if run_as_user and os.geteuid() == 0 and run_as_user != "root":
            final_command = ["sudo", "-u", run_as_user] + final_command
        elif requires_root and os.geteuid() != 0:
            final_command = ["sudo"] + final_command

        rendered = " ".join(final_command)
        self.context.logger.info("Commande: %s", rendered)
        if self.context.dry_run:
            self.context.info(f"[dry-run] {rendered}")
            return CommandResult(command=final_command, returncode=0, stdout="", stderr="")

        completed = subprocess.run(
            final_command,
            text=True,
            capture_output=capture_output,
            env=env,
            input=input_text,
            check=False,
        )
        stdout = completed.stdout or ""
        stderr = completed.stderr or ""
        if stdout.strip():
            self.context.logger.info("stdout:\n%s", stdout.rstrip())
        if stderr.strip():
            self.context.logger.warning("stderr:\n%s", stderr.rstrip())

        if check and completed.returncode != 0:
            raise SecureCheckError(f"Echec de la commande ({completed.returncode}): {rendered}")

        return CommandResult(
            command=final_command,
            returncode=completed.returncode,
            stdout=stdout,
            stderr=stderr,
        )

    def update_package_index(self) -> None:
        if self._package_index_updated:
            return

        manager = self.context.system.package_manager
        if manager == "apt-get":
            self.run(["apt-get", "update"], requires_root=True)
        elif manager in {"dnf", "yum"}:
            self.run([manager, "makecache"], requires_root=True)
        elif manager == "pacman":
            self.run(["pacman", "-Sy"], requires_root=True)
        else:
            raise SecureCheckError("Gestionnaire de paquets non supporté")
        self._package_index_updated = True

    def is_package_installed(self, package: str) -> bool:
        manager = self.context.system.package_manager
        if manager == "apt-get":
            return subprocess.run(["dpkg", "-s", package], capture_output=True).returncode == 0
        if manager in {"dnf", "yum"}:
            return subprocess.run(["rpm", "-q", package], capture_output=True).returncode == 0
        if manager == "pacman":
            return subprocess.run(["pacman", "-Q", package], capture_output=True).returncode == 0
        return False

    def package_available(self, package: str) -> bool:
        manager = self.context.system.package_manager
        if manager == "apt-get":
            self.update_package_index()
            return subprocess.run(["apt-cache", "show", package], capture_output=True).returncode == 0
        if manager in {"dnf", "yum"}:
            return subprocess.run([manager, "info", package], capture_output=True).returncode == 0
        if manager == "pacman":
            return subprocess.run(["pacman", "-Si", package], capture_output=True).returncode == 0
        return False

    def ensure_packages(self, packages: list[str]) -> bool:
        return self.ensure_packages_report(packages).changed

    def ensure_packages_report(self, packages: list[str]) -> PackageOperation:
        if not packages:
            return PackageOperation(requested=[], installed=[], already_present=[])

        manager = self.context.system.package_manager
        self.update_package_index()
        already_present = [package for package in packages if self.is_package_installed(package)]
        missing = [package for package in packages if package not in already_present]

        if not missing:
            return PackageOperation(requested=packages, installed=[], already_present=already_present)

        if manager == "apt-get":
            command = ["apt-get", "install", "-y", *missing]
        elif manager in {"dnf", "yum"}:
            command = [manager, "install", "-y", *missing]
        elif manager == "pacman":
            command = ["pacman", "-S", "--noconfirm", *missing]
        else:
            raise SecureCheckError("Installation de paquets non supportée")

        self.run(command, requires_root=True)
        return PackageOperation(requested=packages, installed=missing, already_present=already_present)

    def upgrade_system(self) -> None:
        manager = self.context.system.package_manager
        if manager == "apt-get":
            self.run(["apt-get", "dist-upgrade", "-y"], requires_root=True)
            self.run(["apt-get", "autoremove", "-y"], requires_root=True)
            self.run(["apt-get", "autoclean"], requires_root=True)
        elif manager in {"dnf", "yum"}:
            self.run([manager, "upgrade", "-y", "--refresh"], requires_root=True)
        elif manager == "pacman":
            self.run(["pacman", "-Syu", "--noconfirm"], requires_root=True)
        else:
            raise SecureCheckError("Mise à jour système non supportée")

    def ensure_directory(self, path: Path, mode: int = 0o755, *, requires_root: bool = False) -> None:
        if self.context.dry_run:
            self.context.info(f"[dry-run] mkdir -p {path}")
            return
        if requires_root and os.geteuid() != 0:
            self.run(["install", "-d", "-m", f"{mode:o}", str(path)], requires_root=True)
            return
        path.mkdir(parents=True, exist_ok=True)
        path.chmod(mode)
        if requires_root and os.geteuid() == 0:
            os.chown(path, 0, 0)

    def write_text_file(
        self,
        path: Path,
        content: str,
        *,
        mode: int = 0o644,
        requires_root: bool = False,
        owner_uid: int | None = None,
        owner_gid: int | None = None,
    ) -> bool:
        try:
            current = path.read_text(encoding="utf-8") if path.exists() else None
        except OSError:
            current = None
        if current == content:
            return False

        self.context.logger.info("Ecriture du fichier %s", path)
        if self.context.dry_run:
            self.context.info(f"[dry-run] write {path}")
            return True

        if requires_root and os.geteuid() != 0:
            self._write_text_file_as_root(path, content, mode=mode, owner_uid=owner_uid, owner_gid=owner_gid)
            return True

        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")
        os.chmod(path, mode)

        if requires_root and os.geteuid() == 0:
            os.chown(path, 0, 0)
        elif owner_uid is not None and owner_gid is not None and os.geteuid() == 0:
            os.chown(path, owner_uid, owner_gid)
        return True

    def _write_text_file_as_root(
        self,
        path: Path,
        content: str,
        *,
        mode: int,
        owner_uid: int | None,
        owner_gid: int | None,
    ) -> None:
        self.ensure_directory(path.parent, requires_root=True)
        tmp_dir = self.context.paths.state_dir
        tmp_dir.mkdir(parents=True, exist_ok=True)
        tmp_path: Path | None = None
        try:
            with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, dir=tmp_dir) as handle:
                handle.write(content)
                tmp_path = Path(handle.name)
            self.run(["install", "-m", f"{mode:o}", str(tmp_path), str(path)], requires_root=True)
            if owner_uid is not None and owner_gid is not None:
                self.run(["chown", f"{owner_uid}:{owner_gid}", str(path)], requires_root=True)
        finally:
            if tmp_path and tmp_path.exists():
                tmp_path.unlink(missing_ok=True)

    def ensure_user_shell(self, shell_path: str) -> bool:
        passwd_file = Path("/etc/passwd").read_text(encoding="utf-8")
        for line in passwd_file.splitlines():
            if line.startswith(f"{self.context.system.target_user}:"):
                current_shell = line.rsplit(":", 1)[-1]
                if current_shell == shell_path:
                    return False
                break

        self.run(["chsh", "-s", shell_path, self.context.system.target_user], requires_root=os.geteuid() == 0)
        return True

    def enable_service(self, service: str) -> None:
        self.run(["systemctl", "enable", "--now", service], requires_root=True)

    def restart_service(self, service: str) -> None:
        self.run(["systemctl", "restart", service], requires_root=True)

    def service_is_active(self, service: str) -> bool:
        result = self.run(["systemctl", "is-active", service], requires_root=True, check=False)
        return result.returncode == 0

    def read_memory_mb(self) -> int:
        for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines():
            if line.startswith("MemTotal:"):
                parts = line.split()
                return int(parts[1]) // 1024
        return 1024

    def write_json_file(self, path: Path, payload: dict, *, mode: int = 0o644, requires_root: bool = False) -> bool:
        content = json.dumps(payload, indent=2) + "\n"
        return self.write_text_file(path, content, mode=mode, requires_root=requires_root)

    def ensure_executable(self, path: Path, *, requires_root: bool = False) -> None:
        if self.context.dry_run:
            return
        current = stat.S_IMODE(path.stat().st_mode)
        path.chmod(current | 0o111)
        if requires_root and os.geteuid() == 0:
            os.chown(path, 0, 0)

    def download_text(self, url: str, *, timeout: int = 20) -> str:
        self.context.logger.info("Téléchargement: %s", url)
        if self.context.dry_run:
            self.context.info(f"[dry-run] download {url}")
            return ""
        with urllib.request.urlopen(url, timeout=timeout) as response:
            return response.read().decode("utf-8")


def execute_tasks(context: ExecutionContext, tasks: list[TaskDefinition]) -> list[TaskResult]:
    results: list[TaskResult] = []
    total = len(tasks)

    for index, task in enumerate(tasks, start=1):
        started_at = datetime.now()
        context.info(f"[{index}/{total}] {task.label}")
        try:
            result = task.handler(context)
            results.append(result)
            status = "OK" if result.success else "ECHEC"
            context.info(f"  -> {status} ({result.duration_seconds:.1f}s)")
        except Exception as exc:  # noqa: BLE001
            context.logger.exception("Task failed: %s", task.key)
            results.append(
                context.make_result(
                    task,
                    success=False,
                    changed=False,
                    started_at=started_at,
                    details=[],
                    error=str(exc),
                )
            )
            context.error(f"  -> ECHEC: {exc}")

    return results
