"""OS-specific behavior for wrappers, venv layout, and runtime paths."""

from __future__ import annotations

import os
import platform
import site
import stat
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Mapping, Protocol, Sequence, runtime_checkable


def _generated_timestamp() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _sh_quote(value: str) -> str:
    if not value:
        return "''"
    return "'" + value.replace("'", "'\\''") + "'"


def _sh_escape_double(value: str) -> str:
    result: list[str] = []
    for ch in value:
        if ch in ('$', '`', '"', '\\'):
            result.append('\\')
        result.append(ch)
    return "".join(result)


def _cmd_escape(value: str) -> str:
    """Escape a value for use in `set "NAME=value"` batch syntax."""
    return value.replace("^", "^^").replace("%", "%%").replace('"', '""')


@runtime_checkable
class DekkOS(Protocol):
    """OS-specific strategy surface for dekk."""

    name: str
    path_separator: str

    def default_wrapper_install_dir(self) -> Path: ...
    def wrapper_filename(self, name: str) -> str: ...
    def conda_runtime_paths(self, prefix: Path) -> tuple[Path, ...]: ...
    def cmake_package_dir(self, prefix: Path, package: str) -> Path: ...
    def cmake_runtime_dir(self, prefix: Path) -> Path: ...
    def venv_bin_dir(self, venv_path: Path) -> Path: ...
    def venv_python(self, venv_path: Path) -> Path: ...
    def venv_pip(self, venv_path: Path) -> Path: ...
    def make_wrapper_executable(self, path: Path) -> None: ...
    def render_wrapper(
        self,
        *,
        target: Path,
        env_vars: Mapping[str, str],
        path_prepends: Sequence[str],
        project_name: str,
        prepend_vars: Mapping[str, str] | None = None,
        python: Path | None = None,
    ) -> str: ...


@dataclass(frozen=True)
class PosixDekkOS:
    name: str = "posix"
    path_separator: str = ":"

    def default_wrapper_install_dir(self) -> Path:
        return Path(site.getuserbase()) / "bin"

    def wrapper_filename(self, name: str) -> str:
        return name

    def conda_runtime_paths(self, prefix: Path) -> tuple[Path, ...]:
        return (prefix / "bin",)

    def cmake_package_dir(self, prefix: Path, package: str) -> Path:
        return prefix / "lib" / "cmake" / package

    def cmake_runtime_dir(self, prefix: Path) -> Path:
        return prefix / "lib"

    def venv_bin_dir(self, venv_path: Path) -> Path:
        return venv_path / "bin"

    def venv_python(self, venv_path: Path) -> Path:
        bin_dir = self.venv_bin_dir(venv_path)
        primary = bin_dir / "python3"
        return primary if primary.exists() else bin_dir / "python"

    def venv_pip(self, venv_path: Path) -> Path:
        return self.venv_bin_dir(venv_path) / "pip"

    def make_wrapper_executable(self, path: Path) -> None:
        path.chmod(
            path.stat().st_mode
            | stat.S_IXUSR
            | stat.S_IXGRP
            | stat.S_IXOTH
        )

    def render_wrapper(
        self,
        *,
        target: Path,
        env_vars: Mapping[str, str],
        path_prepends: Sequence[str],
        project_name: str,
        prepend_vars: Mapping[str, str] | None = None,
        python: Path | None = None,
    ) -> str:
        lines: list[str] = [
            "#!/bin/sh",
            f"# Wrapper for {project_name}",
            f"# Generated by dekk on {_generated_timestamp()}",
            f"# Regenerate with: dekk wrap {project_name} {target}"
            + (f" --python {python}" if python else ""),
            "#",
            "# This script bakes in the full project environment so the",
            "# tool can be invoked without manually activating anything.",
            "",
        ]

        if env_vars:
            lines.append("# --- Environment variables ---")
            for name, value in env_vars.items():
                if name == "PATH":
                    continue
                lines.append(f"export {name}={_sh_quote(value)}")
            lines.append("")

        if prepend_vars:
            for name, value in prepend_vars.items():
                escaped = _sh_escape_double(value)
                lines.append(f'export {name}="{escaped}:${name}"')
            lines.append("")

        if path_prepends:
            lines.append("# --- PATH ---")
            raw_joined = ":".join(path_prepends)
            lines.append(f'export PATH="{_sh_escape_double(raw_joined)}:$PATH"')
            lines.append("")

        lines.append("# --- Exec (replaces this process) ---")
        if python:
            lines.append(f'exec {_sh_quote(str(python))} {_sh_quote(str(target))} "$@"')
        else:
            lines.append(f'exec {_sh_quote(str(target))} "$@"')
        lines.append("")

        return "\n".join(lines)


@dataclass(frozen=True)
class WindowsDekkOS:
    name: str = "windows"
    path_separator: str = ";"

    def default_wrapper_install_dir(self) -> Path:
        return Path(site.getuserbase()) / "Scripts"

    def wrapper_filename(self, name: str) -> str:
        suffix = Path(name).suffix.lower()
        if suffix in {".cmd", ".bat"} or suffix:
            return name
        return f"{name}.cmd"

    def conda_runtime_paths(self, prefix: Path) -> tuple[Path, ...]:
        return (
            prefix,
            prefix / "Library" / "mingw-w64" / "bin",
            prefix / "Library" / "usr" / "bin",
            prefix / "Library" / "bin",
            prefix / "Scripts",
            prefix / "bin",
        )

    def cmake_package_dir(self, prefix: Path, package: str) -> Path:
        return prefix / "Library" / "lib" / "cmake" / package

    def cmake_runtime_dir(self, prefix: Path) -> Path:
        return prefix / "Library" / "bin"

    def venv_bin_dir(self, venv_path: Path) -> Path:
        return venv_path / "Scripts"

    def venv_python(self, venv_path: Path) -> Path:
        scripts_dir = self.venv_bin_dir(venv_path)
        primary = scripts_dir / "python.exe"
        return primary if primary.exists() else scripts_dir / "python"

    def venv_pip(self, venv_path: Path) -> Path:
        scripts_dir = self.venv_bin_dir(venv_path)
        primary = scripts_dir / "pip.exe"
        return primary if primary.exists() else scripts_dir / "pip"

    def make_wrapper_executable(self, path: Path) -> None:
        # File extension drives executability on Windows.
        return None

    def render_wrapper(
        self,
        *,
        target: Path,
        env_vars: Mapping[str, str],
        path_prepends: Sequence[str],
        project_name: str,
        prepend_vars: Mapping[str, str] | None = None,
        python: Path | None = None,
    ) -> str:
        lines: list[str] = [
            "@echo off",
            "setlocal",
            f"REM Wrapper for {project_name}",
            f"REM Generated by dekk on {_generated_timestamp()}",
            f"REM Regenerate with: dekk wrap {project_name} {target}"
            + (f" --python {python}" if python else ""),
            "REM This script bakes in the full project environment.",
            "",
        ]

        for name, value in env_vars.items():
            if name == "PATH":
                continue
            lines.append(f'set "{name}={_cmd_escape(value)}"')

        combined_prepends = dict(prepend_vars or {})
        if path_prepends:
            combined_prepends["PATH"] = self.path_separator.join(path_prepends)

        for name, value in combined_prepends.items():
            escaped = _cmd_escape(value)
            lines.append(f"if defined {name} (")
            lines.append(f'  set "{name}={escaped}{self.path_separator}%{name}%"')
            lines.append(") else (")
            lines.append(f'  set "{name}={escaped}"')
            lines.append(")")

        command = [str(target)] if python is None else [str(python), str(target)]
        lines.append("call " + subprocess.list2cmdline(command) + " %*")
        lines.append("set EXIT_CODE=%ERRORLEVEL%")
        lines.append("endlocal & exit /b %EXIT_CODE%")
        lines.append("")
        return "\r\n".join(lines)


def get_dekk_os(system_name: str | None = None) -> DekkOS:
    """Return the host or requested OS implementation."""
    detected = system_name if system_name is not None else platform.system()
    if detected == "Windows":
        return WindowsDekkOS()
    return PosixDekkOS()
