import os
import platform
import shutil
import stat
import subprocess
import tarfile
import tempfile
import urllib.error
import urllib.request


DEFAULT_BASE_URL = "http://localhost:8080"
DEFAULT_RELEASE_REPO = "sincover/stele-dev"


class InstallError(RuntimeError):
    pass


def _env(key):
    return (os.environ.get(key) or "").strip()


def _cache_dir():
    # Prefer XDG cache; fall back to ~/.cache.
    raw = _env("STELE_CACHE_DIR") or _env("XDG_CACHE_HOME")
    if raw:
        return raw
    home = os.path.expanduser("~")
    return os.path.join(home, ".cache")


def _data_dir():
    # Mirror the default used by `cmd/stele-mcp/main.go` (STELE_DATA_DIR, XDG_DATA_HOME, ~/.local/share/stele).
    raw = _env("STELE_DATA_DIR")
    if raw:
        return raw
    raw = _env("XDG_DATA_HOME")
    if raw:
        return os.path.join(raw, "stele")
    home = os.path.expanduser("~")
    return os.path.join(home, ".local", "share", "stele")


def _state_dir():
    # Mirror the default used by `cmd/stele-mcp/main.go` / `cmd/stele/main.go` (STELE_STATE_DIR, XDG_STATE_HOME, ~/.local/state/stele).
    raw = _env("STELE_STATE_DIR")
    if raw:
        return raw
    raw = _env("XDG_STATE_HOME")
    if raw:
        return os.path.join(raw, "stele")
    home = os.path.expanduser("~")
    return os.path.join(home, ".local", "state", "stele")


def _platform_tag():
    sysname = platform.system().lower()
    if sysname.startswith("linux"):
        os_tag = "linux"
    elif sysname.startswith("darwin") or sysname.startswith("mac"):
        os_tag = "darwin"
    elif sysname.startswith("windows"):
        os_tag = "windows"
    else:
        raise InstallError(f"unsupported platform: {platform.system()}")

    machine = (platform.machine() or "").lower()
    if machine in ("x86_64", "amd64"):
        arch = "amd64"
    elif machine in ("aarch64", "arm64"):
        arch = "arm64"
    else:
        raise InstallError(f"unsupported arch: {platform.machine()}")

    return os_tag, arch


def _asset_name(os_tag, arch):
    return f"stele-single-{os_tag}-{arch}.tar.gz"


def _download_url(repo, asset_name):
    base = _env("STELE_RELEASE_BASE_URL")
    if base:
        return base.rstrip("/") + "/" + asset_name
    return f"https://github.com/{repo}/releases/latest/download/{asset_name}"


def _bin_dir():
    raw = _env("STELE_BIN_DIR")
    if raw:
        return raw
    os_tag, arch = _platform_tag()
    return os.path.join(_cache_dir(), "stele", "bin", f"{os_tag}-{arch}")


def _exe_name(name):
    os_tag, _arch = _platform_tag()
    if os_tag == "windows" and not name.endswith(".exe"):
        return name + ".exe"
    return name


def _chmod_x(path):
    try:
        mode = os.stat(path).st_mode
        os.chmod(path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
    except OSError:
        # Best-effort: some FS/OS combos may not support chmod in the expected way.
        pass


def find_binary(name, bin_dir=None):
    root = bin_dir or _bin_dir()
    path = os.path.join(root, _exe_name(name))
    if os.path.exists(path):
        return path
    # Allow dev overrides by PATH for local hacking.
    return shutil.which(_exe_name(name)) or ""


def install(repo=DEFAULT_RELEASE_REPO, bin_dir=None):
    repo = _env("STELE_RELEASE_REPO") or repo
    os_tag, arch = _platform_tag()
    asset = _asset_name(os_tag, arch)
    url = _download_url(repo, asset)

    out_dir = bin_dir or _bin_dir()
    os.makedirs(out_dir, exist_ok=True)

    stele_path = os.path.join(out_dir, _exe_name("stele"))
    if os.path.exists(stele_path):
        return out_dir

    with tempfile.TemporaryDirectory() as td:
        archive_path = os.path.join(td, asset)
        try:
            with urllib.request.urlopen(url, timeout=60) as resp, open(archive_path, "wb") as f:
                f.write(resp.read())
        except urllib.error.HTTPError as exc:
            raise InstallError(f"download failed ({exc.code}) for {url}") from exc
        except Exception as exc:
            raise InstallError(f"download failed for {url}: {exc}") from exc

        with tarfile.open(archive_path, "r:gz") as tf:
            members = tf.getmembers()
            for m in members:
                # Avoid path traversal when extracting remote archives.
                name = m.name or ""
                if name.startswith("/") or name.startswith("\\"):
                    raise InstallError(f"unsafe archive member path: {name}")
                dest = os.path.abspath(os.path.join(out_dir, name))
                if not dest.startswith(os.path.abspath(out_dir) + os.sep) and dest != os.path.abspath(out_dir):
                    raise InstallError(f"unsafe archive member path: {name}")
            tf.extractall(out_dir)

    # Ensure executables are marked runnable on unix.
    for name in ("stele", "steled", "stele-mcp", "stele-cli", "stele-embedder-adapter"):
        p = os.path.join(out_dir, _exe_name(name))
        if os.path.exists(p):
            _chmod_x(p)

    if not os.path.exists(stele_path):
        raise InstallError(f"install succeeded but {stele_path} is missing (asset={asset})")
    return out_dir


def _health_ok(base_url):
    base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
    url = base_url + "/health"
    try:
        with urllib.request.urlopen(url, timeout=0.6) as resp:
            return 200 <= resp.status < 300
    except Exception:
        return False


def start(
    base_url=None,
    repo=DEFAULT_RELEASE_REPO,
    bin_dir=None,
    store_path="",
    log_path="",
    wait_seconds=10,
):
    base_url = (base_url or _env("STELE_BASE_URL") or DEFAULT_BASE_URL).strip()
    if _health_ok(base_url):
        return {"ok": True, "already_running": True, "base_url": base_url}

    out_dir = bin_dir or install(repo=repo, bin_dir=bin_dir)
    stele_bin = find_binary("stele", bin_dir=out_dir)
    if not stele_bin:
        raise InstallError("stele binary not found after install")

    if not store_path:
        store_path = _env("STELE_STORE_PATH") or os.path.join(_data_dir(), "stele.db")
    if not log_path:
        log_path = _env("STELE_LOG_PATH") or os.path.join(_state_dir(), "logs", "steled.log")

    os.makedirs(os.path.dirname(store_path), exist_ok=True)
    os.makedirs(os.path.dirname(log_path), exist_ok=True)

    cmd = [
        stele_bin,
        "start",
        "-base-url",
        base_url,
        "-store-backend",
        "sqlite",
        "-store-path",
        store_path,
        "-log",
        log_path,
        "-wait",
        f"{int(wait_seconds)}s",
    ]
    env = os.environ.copy()
    # The launcher sets env for steled; we only set defaults that make installed usage stable.
    env.setdefault("STELE_STORE_BACKEND", "sqlite")
    env.setdefault("STELE_STORE_PATH", store_path)

    proc = subprocess.run(cmd, env=env, capture_output=True, text=True)
    if proc.returncode != 0:
        raise RuntimeError((proc.stderr or proc.stdout or "").strip() or "stele start failed")
    return {
        "ok": True,
        "already_running": False,
        "base_url": base_url,
        "store_path": store_path,
        "log_path": log_path,
    }


def ensure_running(**kwargs):
    return start(**kwargs)
