import os
import subprocess
import sys
import tarfile
from pathlib import Path
from subprocess import CalledProcessError, TimeoutExpired
from typing import Optional, Sequence
from urllib.error import URLError
from urllib.request import urlcleanup, urlretrieve

import click
import requests
from semver import VersionInfo

from ..version import __version__
from .config import Config
from .os import has_command, reset_fds

REQ_TIMEOUT = 1
INSTALL_TIMEOUT = 30


class SymUpdater:
    def __init__(self):
        self.dir = Path(__file__).resolve().parent.parent.parent.parent
        self.latest_version: Optional[VersionInfo] = None
        self.url: Optional[dict] = None

    def _log(self, s, **kwargs):
        click.secho(s, err=True, **kwargs)

    def should_check(self) -> bool:
        return not self.is_local() and Config.check_for_update()

    def get_current_version(self) -> VersionInfo:
        return VersionInfo.parse(__version__)

    def get_latest_version(self) -> Optional[VersionInfo]:
        try:
            resp = requests.get(
                "https://pypi.org/pypi/sym-cli/json",
                timeout=REQ_TIMEOUT,
            ).json()
            version = VersionInfo.parse(resp["info"]["version"])

            self.latest_version = version
            self.url = next(
                (u for u in resp["urls"] if u["python_version"] == "source"),
                None,
            )
            return version
        except (requests.exceptions.RequestException, ValueError, KeyError):
            return None

    def needs_update(self) -> bool:
        if (version := self.get_latest_version()) :
            return version > self.get_current_version()
        return False

    def is_local(self):
        return (self.dir / ".git").exists() or self.get_current_version() == "0.0.0"

    def is_pipx(self):
        return (Path.home() / ".local" / "pipx") in self.dir.parents

    def is_brew(self):
        return Path("/usr/local/Cellar") in self.dir.parents

    def _update_with_command(self, command: Sequence[str]) -> bool:
        if not has_command(command[0]):
            return False
        try:
            subprocess.run(
                command,
                timeout=INSTALL_TIMEOUT,
                check=True,
                capture_output=True,
            )
            return True
        except (CalledProcessError, TimeoutExpired):
            return False

    def update_pipx(self) -> bool:
        return self._update_with_command(["pipx", "upgrade", "sym-cli", "--force"])

    def update_brew(self) -> bool:
        return self._update_with_command(["brew", "upgrade", "sym-cli"])

    def update_other(self) -> bool:
        if not self.url:
            return False

        try:
            filename, _ = urlretrieve(self.url["url"])
        except URLError:
            urlcleanup()
            return False

        try:
            prefix = f"sym-cli-{self.latest_version}/"
            with tarfile.open(filename) as tar:
                for member in tar.getmembers():
                    if not member.name.startswith(prefix):
                        continue
                    member.name = str(Path(member.name).relative_to(prefix))
                    tar.extract(member, path=self.dir)
            return True
        except tarfile.TarError:
            return False

    def _update(self) -> bool:
        if self.is_pipx():
            return self.update_pipx()
        if self.is_brew():
            return self.update_brew()
        return self.update_other()

    def update(self, exec=False):
        self._log("Quickly checking for Sym CLI updates...", fg="white", nl=False)
        if not self.needs_update():
            self._log("no updates found, carry on!", fg="white")
            return
        self._log(f"found newer version {self.latest_version}!", fg="white", bold=True)

        self._log("Updating...", nl=False)
        if self._update():
            self._log("finished!", fg="green")
            if exec:
                reset_fds()
                os.execvp(sys.argv[0], sys.argv)
        else:
            self._log("failed :(", fg="red")

    def auto_update(self):
        try:
            if not self.should_check():
                return
            self.update(exec=True)
        except:
            return  # just to be safe

    def manual_update(self):
        if self.is_local():
            self._log("Auto-updating is not supported in dev environments!", fg="red")
            return
        self.update()
