from __future__ import annotations

"""Cross-platform utilities to install/uninstall a *background* service that
runs ``portacode connect`` automatically at login / boot.

Platforms implemented:

• Linux (systemd **user** service)        – no root privileges required
• Linux (OpenRC service)                  – system-wide (e.g., Alpine)
• macOS (launchd LaunchAgent plist)       – per-user
• Windows (Task Scheduler *ONLOGON* task) – highest privilege, current user

The service simply executes::

    portacode connect

so any configuration (e.g. gateway URL env var) has to be available in the
user's environment at login.
"""

from pathlib import Path
import platform
import subprocess
import sys
import textwrap
import os
import shlex
from typing import List, Protocol, Union
import shutil
import pwd
import tempfile

from .exit_codes import AUTH_REJECTED_EXIT_CODE

__all__ = [
    "ServiceManager",
    "get_manager",
]


PROJECT_PATH_ENV_PREFIX = "PORTACODE_PROJECT_PATH_"


def _resolve_service_user() -> str:
    override = (os.environ.get("PORTACODE_SERVICE_USER") or "").strip()
    if override:
        return override
    return os.environ.get("SUDO_USER") or os.environ.get("USER") or os.getlogin()


def _resolve_home_for_user(user: str) -> Path:
    try:
        return Path(pwd.getpwnam(user).pw_dir)
    except KeyError:
        return Path("/root") if user == "root" else Path(f"/home/{user}")


def _load_connect_project_paths_from_env() -> List[str]:
    indexed: list[tuple[int, str]] = []
    for key, value in os.environ.items():
        if not key.startswith(PROJECT_PATH_ENV_PREFIX):
            continue
        suffix = key[len(PROJECT_PATH_ENV_PREFIX):]
        if not suffix.isdigit():
            continue
        cleaned = str(value).strip()
        if not cleaned:
            continue
        indexed.append((int(suffix), cleaned))
    return [value for _, value in sorted(indexed)]


def _build_shell_command(argv: List[str]) -> str:
    return shlex.join(argv)


def _build_systemd_execstart(argv: List[str]) -> str:
    escaped_args = [
        shlex.quote(arg.replace("%", "%%").replace("$", "$$"))
        for arg in argv
    ]
    return " ".join(escaped_args)


def _build_connect_command(
    python: str,
    *,
    module: str = "portacode",
    renderer=_build_shell_command,
) -> str:
    argv = [python, "-m", module, "connect", "--non-interactive"]
    for path in _load_connect_project_paths_from_env():
        argv.extend(["--project-path", path])
    return renderer(argv)


class ServiceManager(Protocol):
    """Common interface all platform managers implement."""

    def install(self) -> None:  # noqa: D401 – short description
        """Create + enable the service and start it immediately."""

    def uninstall(self) -> None:
        """Disable + remove the service."""

    def start(self) -> None:
        """Start the service now (if already installed)."""

    def stop(self) -> None:
        """Stop the service if running."""

    def restart(self) -> None:
        """Restart the service (stop then start)."""

    def status(self) -> str:
        """Return short human-readable status string (active/running/inactive)."""

    def status_verbose(self) -> str:  # noqa: D401 – optional detailed info
        """Return multi-line diagnostic information suitable for display."""
        raise NotImplementedError


# ---------------------------------------------------------------------------
# Linux – systemd (user) implementation
# ---------------------------------------------------------------------------

class _SystemdUserService:
    NAME = "portacode"

    def __init__(self, system_mode: bool = False) -> None:
        self.system_mode = system_mode
        if system_mode:
            self.service_path = Path("/etc/systemd/system") / f"{self.NAME}.service"
            self.user = _resolve_service_user()
            self.home = _resolve_home_for_user(self.user)
            # Use the exact interpreter running this CLI. This preserves venv installs
            # (e.g., /opt/portacode-venv/bin/python) for system service ExecStart.
            self.python = sys.executable
        else:
            self.service_path = (
                Path.home() / ".config/systemd/user" / f"{self.NAME}.service"
            )
            self.user = os.environ.get("USER") or os.getlogin()
            self.home = Path.home()
            self.python = sys.executable
        self.runtime_user = (os.environ.get("PORTACODE_DEFAULT_RUNTIME_USER") or self.user).strip() or self.user
        self.xdg_data_home = (os.environ.get("PORTACODE_XDG_DATA_HOME") or "").strip()

    def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
        if self.system_mode:
            sudo_needed = os.geteuid() != 0
            if sudo_needed:
                # If we don't have a TTY (e.g., running from automation), fail fast instead of hanging
                # on a password prompt.
                has_tty = sys.stdin.isatty() and sys.stdout.isatty()
                base = ["sudo", "-n", "systemctl"] if not has_tty else ["sudo", "systemctl"]
            else:
                base = ["systemctl"]
            cmd = [*base, *args]
        else:
            cmd = ["systemctl", "--user", *args]
        return subprocess.run(cmd, text=True, capture_output=True)

    def _run_checked(self, *args: str) -> subprocess.CompletedProcess[str]:
        res = self._run(*args)
        if res.returncode != 0:
            msg = (res.stderr or res.stdout or "").strip() or f"systemctl {' '.join(args)} failed"
            raise RuntimeError(msg)
        return res

    def install(self) -> None:
        self.service_path.parent.mkdir(parents=True, exist_ok=True)
        
        # Capture current SHELL for the service to prevent using /bin/sh in containers/virtualized environments
        current_shell = os.getenv("SHELL", "/bin/bash")
        env_lines = [f"Environment=SHELL={current_shell}"]
        if self.system_mode:
            env_lines.append(f"Environment=PORTACODE_DEFAULT_RUNTIME_USER={self.runtime_user}")
            if self.xdg_data_home:
                env_lines.append(f"Environment=XDG_DATA_HOME={self.xdg_data_home}")
        env_block = "\n                ".join(env_lines)
        
        if self.system_mode:
            unit = textwrap.dedent(f"""
                [Unit]
                Description=Portacode persistent connection (system-wide)
                After=network.target

                [Service]
                Type=simple
                User={self.user}
                WorkingDirectory={self.home}
                {env_block}
                ExecStart={_build_connect_command(self.python, renderer=_build_systemd_execstart)}
                Restart=on-failure
                RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
                RestartSec=5

                [Install]
                WantedBy=multi-user.target
            """).lstrip()
        else:
            unit = textwrap.dedent(f"""
                [Unit]
                Description=Portacode persistent connection
                After=network.target

                [Service]
                Type=simple
                {env_block}
                ExecStart={_build_connect_command(self.python, module="portacode.cli", renderer=_build_systemd_execstart)}
                Restart=on-failure
                RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
                RestartSec=5

                [Install]
                WantedBy=default.target
            """).lstrip()
        self.service_path.write_text(unit)
        if self.system_mode:
            # These should be loud when they fail; otherwise callers think the service exists when it doesn't.
            self._run_checked("daemon-reload")
            self._run_checked("enable", "--now", self.NAME)
        else:
            self._run_checked("daemon-reload")
            self._run_checked("enable", "--now", self.NAME)

    def uninstall(self) -> None:
        if self.system_mode:
            self._run("disable", "--now", self.NAME)
            if self.service_path.exists():
                # Avoid using rm via subprocess; remove file directly when possible.
                try:
                    self.service_path.unlink()
                except PermissionError:
                    prefix = ["sudo"] if os.geteuid() != 0 else []
                    subprocess.run([*prefix, "rm", "-f", str(self.service_path)], text=True, capture_output=True)
            self._run("daemon-reload")
        else:
            self._run("disable", "--now", self.NAME)
            if self.service_path.exists():
                self.service_path.unlink()
            self._run("daemon-reload")

    def start(self) -> None:
        self._run_checked("start", self.NAME)

    def stop(self) -> None:
        self._run_checked("stop", self.NAME)

    def restart(self) -> None:
        self._run_checked("restart", self.NAME)

    def status(self) -> str:
        res = self._run("is-active", self.NAME)
        return (res.stdout.strip() or res.stderr.strip() or "unknown")

    def status_verbose(self) -> str:
        res = self._run("status", "--no-pager", self.NAME)
        status = res.stdout or res.stderr

        if self.system_mode:
            sudo_needed = os.geteuid() != 0
            if sudo_needed:
                has_tty = sys.stdin.isatty() and sys.stdout.isatty()
                prefix = ["sudo", "-n"] if not has_tty else ["sudo"]
            else:
                prefix = []
            journal_cmd = [*prefix, "journalctl", "-n", "20", "-u", f"{self.NAME}.service", "--no-pager"]
        else:
            journal_cmd = ["journalctl", "--user", "-n", "20", "-u", f"{self.NAME}.service", "--no-pager"]

        journal = subprocess.run(journal_cmd, text=True, capture_output=True).stdout
        return (status or "") + "\n--- recent logs ---\n" + (journal or "<no logs>")


# ---------------------------------------------------------------------------
# Linux – OpenRC implementation (e.g., Alpine)
# ---------------------------------------------------------------------------
class _OpenRCService:
    NAME = "portacode"

    def __init__(self) -> None:
        self.init_path = Path("/etc/init.d") / self.NAME
        self.wrapper_path = Path("/usr/local/share/portacode/connect_service.sh")
        self.user = _resolve_service_user()
        self.home = _resolve_home_for_user(self.user)
        # Keep OpenRC behavior aligned with systemd: respect active interpreter/venv.
        self.python = sys.executable
        self.log_dir = Path("/var/log/portacode")
        self.log_path = self.log_dir / "connect.log"
        self.runtime_user = (os.environ.get("PORTACODE_DEFAULT_RUNTIME_USER") or self.user).strip() or self.user
        self.xdg_data_home = (os.environ.get("PORTACODE_XDG_DATA_HOME") or "").strip()

    def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
        if os.geteuid() != 0:
            has_tty = sys.stdin.isatty() and sys.stdout.isatty()
            prefix = ["sudo", "-n"] if not has_tty else ["sudo"]
        else:
            prefix = []
        cmd = [*prefix, *args]
        return subprocess.run(cmd, text=True, capture_output=True)

    def _run_checked(self, *args: str) -> subprocess.CompletedProcess[str]:
        res = self._run(*args)
        if res.returncode != 0:
            msg = (res.stderr or res.stdout or "").strip() or f"{' '.join(args)} failed"
            raise RuntimeError(msg)
        return res

    def _write_init_script(self) -> None:
        self._write_wrapper_script()
        script = textwrap.dedent(f"""
            #!/sbin/openrc-run
            description="Portacode persistent connection"

            command="{self.wrapper_path}"
            command_user="{self.user}"
            pidfile="/run/portacode.pid"
            directory="{self.home}"
            supervisor=supervise-daemon

            depend() {{
                need net
            }}

            start_pre() {{
                checkpath --directory --mode 0755 /var/log/portacode
                checkpath --directory --mode 0755 /usr/local/share/portacode
                touch "{self.log_path}"
                chown {self.user} "{self.log_path}"
                chown {self.user} /var/log/portacode
            }}
        """).lstrip()

        tmp_path = Path(tempfile.gettempdir()) / f"portacode-init-{os.getpid()}"
        tmp_path.write_text(script)
        if os.geteuid() != 0:
            self._run("install", "-m", "755", str(tmp_path), str(self.init_path))
        else:
            self.init_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(tmp_path, self.init_path)
            self.init_path.chmod(0o755)
        try:
            tmp_path.unlink()
        except Exception:
            pass
    
    def _write_wrapper_script(self) -> None:
        xdg_export_line = f'export XDG_DATA_HOME="{self.xdg_data_home}"' if self.xdg_data_home else ""
        connect_command = _build_connect_command(self.python)
        script = textwrap.dedent(f"""
            #!/bin/sh
            export PORTACODE_DEFAULT_RUNTIME_USER="{self.runtime_user}"
            {xdg_export_line}
            cd "{self.home}"
            while true; do
                {connect_command} >> "{self.log_path}" 2>&1
                rc="$?"
                if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
                    echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
                    exit 0
                fi
                if [ "$rc" -eq 0 ]; then
                    exit 0
                fi
                sleep 5
            done
        """).lstrip()
        tmp_path = Path(tempfile.gettempdir()) / f"portacode-wrapper-{os.getpid()}"
        tmp_path.write_text(script)
        if os.geteuid() != 0:
            self._run("install", "-m", "755", str(tmp_path), str(self.wrapper_path))
        else:
            self.wrapper_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(tmp_path, self.wrapper_path)
            self.wrapper_path.chmod(0o755)
        try:
            tmp_path.unlink()
        except Exception:
            pass

    def install(self) -> None:
        self._write_init_script()
        self._run_checked("rc-update", "add", self.NAME, "default")
        self._run_checked("rc-service", self.NAME, "start")

    def uninstall(self) -> None:
        self._run_checked("rc-service", self.NAME, "stop")
        self._run_checked("rc-update", "del", self.NAME, "default")
        if self.init_path.exists():
            if os.geteuid() != 0:
                self._run_checked("rm", "-f", str(self.init_path))
            else:
                self.init_path.unlink()

    def start(self) -> None:
        self._run_checked("rc-service", self.NAME, "start")

    def stop(self) -> None:
        self._run_checked("rc-service", self.NAME, "stop")

    def restart(self) -> None:
        self._run_checked("rc-service", self.NAME, "restart")

    def status(self) -> str:
        res = self._run("rc-service", self.NAME, "status")
        out = (res.stdout or res.stderr or "").strip().lower()
        if "started" in out or "running" in out:
            return "running"
        if "stopped" in out:
            return "stopped"
        return out or "unknown"

    def status_verbose(self) -> str:
        res = self._run("rc-service", self.NAME, "status")
        status = res.stdout or res.stderr or ""
        log_tail = "<no logs>"
        try:
            if self.log_path.exists():
                with self.log_path.open("r", encoding="utf-8", errors="ignore") as fh:
                    lines = fh.readlines()
                log_tail = "".join(lines[-20:]) or "<no logs>"
        except Exception:
            pass
        return (status or "").rstrip() + "\n--- recent logs ---\n" + log_tail


# ---------------------------------------------------------------------------
# macOS – launchd (LaunchAgent) implementation
# ---------------------------------------------------------------------------
class _LaunchdService:
    LABEL = "com.portacode.connect"

    def __init__(self) -> None:
        self.plist_path = (
            Path.home()
            / "Library/LaunchAgents"
            / f"{self.LABEL}.plist"
        )
        self.script_path = Path.home() / ".local" / "share" / "portacode" / "connect_service.sh"
        self.log_path = Path.home() / ".local" / "share" / "portacode" / "connect.log"

    def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
        cmd = ["launchctl", *args]
        return subprocess.run(cmd, text=True, capture_output=True)

    def install(self) -> None:
        self.plist_path.parent.mkdir(parents=True, exist_ok=True)
        self.script_path.parent.mkdir(parents=True, exist_ok=True)
        connect_command = _build_connect_command(sys.executable)
        script = textwrap.dedent(
            f"""
            #!/bin/sh
            cd "$HOME" || exit 1
            while true; do
                {connect_command} >> "{self.log_path}" 2>&1
                rc="$?"
                if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
                    echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
                    exit 0
                fi
                if [ "$rc" -eq 0 ]; then
                    exit 0
                fi
                sleep 5
            done
            """
        ).lstrip()
        self.script_path.write_text(script)
        self.script_path.chmod(0o755)
        plist = textwrap.dedent(
            f"""
            <?xml version="1.0" encoding="UTF-8"?>
            <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
            <plist version="1.0">
            <dict>
                <key>Label</key><string>{self.LABEL}</string>
                <key>ProgramArguments</key>
                <array>
                    <string>{self.script_path}</string>
                </array>
                <key>RunAtLoad</key><true/>
                <key>KeepAlive</key>
                <dict>
                    <key>SuccessfulExit</key><false/>
                </dict>
            </dict>
            </plist>
            """
        ).lstrip()
        self.plist_path.write_text(plist)
        self._run("load", "-w", str(self.plist_path))

    def uninstall(self) -> None:
        self._run("unload", "-w", str(self.plist_path))
        if self.plist_path.exists():
            self.plist_path.unlink()
        if self.script_path.exists():
            self.script_path.unlink()

    def start(self) -> None:
        self._run("start", self.LABEL)

    def stop(self) -> None:
        self._run("stop", self.LABEL)

    def restart(self) -> None:
        self.stop()
        self.start()

    def status(self) -> str:
        res = self._run("list", self.LABEL)
        return "running" if res.returncode == 0 else "stopped"

    def status_verbose(self) -> str:
        res = self._run("list", self.LABEL)
        return res.stdout or res.stderr


# ---------------------------------------------------------------------------
# Windows – Task Scheduler implementation
# ---------------------------------------------------------------------------
if sys.platform.startswith("win"):
    import shlex


class _WindowsTask:
    NAME = "PortacodeConnect"

    def __init__(self) -> None:
        from pathlib import Path as _P
        self._home = _P.home()
        self._script_path = self._home / ".local" / "share" / "portacode" / "connect_service.cmd"
        self.log_path = self._home / ".local" / "share" / "portacode" / "connect.log"

    # ------------------------------------------------------------------

    def _run(self, cmd: Union[str, List[str]]) -> subprocess.CompletedProcess[str]:
        if isinstance(cmd, list):
            return subprocess.run(cmd, text=True, capture_output=True)
        return subprocess.run(cmd, shell=True, text=True, capture_output=True)

    def install(self) -> None:
        python = sys.executable
        from pathlib import Path as _P
        pyw = _P(python).with_name("pythonw.exe")
        use_pyw = pyw.exists()

        # Always use wrapper so we can capture logs reliably
        self._script_path.parent.mkdir(parents=True, exist_ok=True)
        py_cmd = f'"{pyw}"' if use_pyw else f'"{python}"'
        project_args = ""
        for path in _load_connect_project_paths_from_env():
            escaped = path.replace('"', '""')
            project_args += f' --project-path "{escaped}"'
        script = (
            "@echo off\r\n"
            "cd /d %USERPROFILE%\r\n"
            ":loop\r\n"
            f"{py_cmd} -m portacode connect --non-interactive{project_args} >> \"%USERPROFILE%\\.local\\share\\portacode\\connect.log\" 2>>&1\r\n"
            "set \"RC=%ERRORLEVEL%\"\r\n"
            f"if \"%RC%\"==\"{AUTH_REJECTED_EXIT_CODE}\" exit /b 0\r\n"
            "if \"%RC%\"==\"0\" exit /b 0\r\n"
            "timeout /t 5 /nobreak >nul\r\n"
            "goto loop\r\n"
        )
        self._script_path.write_text(script)

        # Use cmd.exe /c to ensure the batch file is found and executed correctly
        action = f'cmd.exe /c "{self._script_path}"'
        cmd = [
            "schtasks", "/Create", "/SC", "ONLOGON", "/RL", "HIGHEST",
            "/TN", self.NAME, "/TR", action, "/F",
        ]
        res = self._run(cmd)
        if res.returncode != 0:
            raise RuntimeError(res.stderr.strip() or res.stdout)

        # Start immediately and verify
        self.start()
        self._wait_until_running()

    def uninstall(self) -> None:
        self._run(["schtasks", "/Delete", "/TN", self.NAME, "/F"])
        # Kill all running portacode connect processes for this user
        self._kill_all_connect()
        try:
            if self._script_path.exists():
                self._script_path.unlink()
        except Exception:
            pass

    def start(self) -> None:
        res = self._run(["schtasks", "/Run", "/TN", self.NAME])
        if res.returncode != 0:
            raise RuntimeError(res.stderr.strip() or res.stdout)
        # wait till running or raise with details
        self._wait_until_running()

    def stop(self) -> None:
        self._run(["schtasks", "/End", "/TN", self.NAME])
        self._kill_all_connect()

    def restart(self) -> None:
        self.stop()
        self.start()

    def status(self) -> str:
        res = self._run(["schtasks", "/Query", "/TN", self.NAME])
        if res.returncode != 0:
            return "stopped"
        # When running, output contains "Running"; else "Ready"
        return "running" if "Running" in res.stdout else "stopped"

    def status_verbose(self) -> str:
        res = self._run(["schtasks", "/Query", "/TN", self.NAME, "/V", "/FO", "LIST"])
        return res.stdout or res.stderr

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _query_task(self) -> dict[str, str]:
        """Return key→value mapping of *schtasks /Query /V /FO LIST* output."""
        res = self._run(["schtasks", "/Query", "/TN", self.NAME, "/V", "/FO", "LIST"])
        if res.returncode != 0:
            return {}
        out: dict[str, str] = {}
        for line in res.stdout.splitlines():
            if ":" in line:
                k, v = line.split(":", 1)
                out[k.strip()] = v.strip()
        return out

    def _tail_log(self, lines: int = 20) -> str:
        try:
            if self.log_path.exists():
                with self.log_path.open("r", encoding="utf-8", errors="ignore") as fh:
                    content = fh.readlines()
                    tail = "".join(content[-lines:])
                    return tail
        except Exception:
            pass
        return "<no log available>"

    def _wait_until_running(self, timeout: int = 15) -> None:
        import time

        deadline = time.time() + timeout
        while time.time() < deadline:
            info = self._query_task()
            status = info.get("Status")
            if status == "Running":
                return  # success
            if status and status != "Ready":
                # Task executed but stopped – raise with last result
                code = info.get("Last Result", "?")
                log_tail = self._tail_log()
                raise RuntimeError(f"Task stopped (LastResult={code}).\n--- log ---\n{log_tail}")
            time.sleep(1)
        # Timeout
        log_tail = self._tail_log()
        raise RuntimeError(f"Task did not reach Running state within {timeout}s.\n--- log ---\n{log_tail}")

    def _kill_all_connect(self):
        import subprocess, os
        try:
            # List all python/pythonw processes for this user
            whoami = os.getlogin()
            for exe in ["python.exe", "pythonw.exe"]:
                out = subprocess.run([
                    "wmic", "process", "where",
                    f"name='{exe}' and CommandLine like '%portacode connect%' and (UserModeTime > 0 or KernelModeTime > 0)",
                    "get", "ProcessId,CommandLine,Name,UserModeTime,KernelModeTime", "/FORMAT:csv"
                ], capture_output=True, text=True)
                for line in out.stdout.splitlines():
                    if "portacode connect" in line and whoami in line:
                        parts = line.split(",")
                        try:
                            pid = int(parts[-1])
                            os.kill(pid, 9)
                        except Exception:
                            pass
        except Exception:
            pass


# ---------------------------------------------------------------------------
# Factory
# ---------------------------------------------------------------------------

def get_manager(system_mode: bool = False) -> ServiceManager:
    system = platform.system().lower()
    if system == "linux":
        if shutil.which("systemctl"):
            return _SystemdUserService(system_mode=system_mode)  # type: ignore[return-value]
        if shutil.which("rc-service") or Path("/sbin/openrc").exists():
            return _OpenRCService()  # type: ignore[return-value]
        raise RuntimeError("Unsupported Linux init system (no systemctl or rc-service found)")
    if system == "darwin":
        return _LaunchdService()      # type: ignore[return-value]
    if system.startswith("windows") or system == "windows":
        return _WindowsTask()         # type: ignore[return-value]
    raise RuntimeError(f"Unsupported platform: {system}") 
