"""
hawkSoar.common.updater
~~~~~~~~~~~~~~~~~~~~~~~
Auto-upgrade support for the hawk-soard agent.

The AutoUpgrader runs as a background daemon thread. On each interval it
fetches a JSON version manifest from the HAWK download server and compares the
published version to the currently-running version. When a newer version is
found the package is downloaded, its SHA-256 digest is verified, the archive
is extracted in-place, and the service is restarted so the new code takes
effect immediately.

Manifest format expected at
  https://downloads.hawkdefense.com/agent/version.json

  {
      "version":  "1.2.3",
      "filename": "hawk-soard-1.2.3.zip",
      "sha256":   "<lowercase-hex-sha256-of-the-zip>"
  }

The zip should contain the replacement files rooted at the install directory,
e.g.:

  hawk-soard              <- main entry-point script
  hawkSoar/__init__.py
  hawkSoar/...

HAWK Network Defense, Inc.
"""

import hashlib
import logging
import os
import subprocess
import sys
import tempfile
import threading
import zipfile

logger = logging.getLogger("hawk-soard.updater")

UPDATE_BASE_URL = "https://downloads.hawkdefense.com/agent"
VERSION_MANIFEST_URL = UPDATE_BASE_URL + "/version.json"

# Default check interval (seconds).  Override via config key
# 'auto_upgrade_interval'.  A 1-hour default keeps churn minimal while still
# delivering updates promptly.
DEFAULT_CHECK_INTERVAL = 3600

# How long to wait after startup before the first check.  This gives the
# service time to fully initialise before we attempt network I/O.
STARTUP_DELAY = 90


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

def _version_tuple(v):
    """Convert a version string like '1.2.3' to a comparable tuple."""
    try:
        return tuple(int(x) for x in str(v).strip().lstrip("v").split("."))
    except Exception:
        return (0,)


def _sha256_of_file(path):
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def _extract_zip(zip_path, dest_dir):
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(dest_dir)


# ---------------------------------------------------------------------------
# Platform-specific service restart
# ---------------------------------------------------------------------------

def _restart_service_windows(service_name):
    logger.info("AutoUpgrader: restarting Windows service '%s'..." % service_name)
    try:
        subprocess.run(
            [
                "powershell", "-NoProfile", "-NonInteractive", "-Command",
                "Restart-Service -Name '%s' -Force -ErrorAction SilentlyContinue"
                % service_name.replace("'", "''"),
            ],
            timeout=60,
            check=False,
        )
    except Exception as exc:
        logger.error("AutoUpgrader: Windows service restart failed: %s" % exc)


def _restart_service_linux(service_name):
    logger.info("AutoUpgrader: restarting Linux service '%s'..." % service_name)
    # Try systemctl first, then SysV service wrapper, then init.d directly.
    for cmd in [
        ["systemctl", "restart", service_name],
        ["service", service_name, "restart"],
        ["/etc/init.d/" + service_name, "restart"],
    ]:
        try:
            result = subprocess.run(cmd, timeout=30, check=False)
            if result.returncode == 0:
                return
        except FileNotFoundError:
            continue
        except Exception as exc:
            logger.warning("AutoUpgrader: restart attempt %s failed: %s" % (cmd, exc))


# ---------------------------------------------------------------------------
# AutoUpgrader thread
# ---------------------------------------------------------------------------

class AutoUpgrader(threading.Thread):
    """
    Background daemon thread that polls the HAWK download server for new agent
    versions and applies them automatically.

    Parameters
    ----------
    current_version : str
        The version string of the currently-running agent (e.g. ``'1.0.0'``).
    install_dir : str, optional
        Filesystem path that will receive the extracted update package.
        Defaults to the directory that contains the running entry-point script
        (``sys.argv[0]``).
    service_name : str, optional
        Name of the OS service to restart after a successful update.
        Defaults to ``'HawkSoard'`` on Windows and ``'hawk-soard'`` on Linux.
    check_interval : int, optional
        Seconds between version-check cycles.  Defaults to 3600 (1 hour).
    insecure : bool, optional
        When ``True`` TLS certificate verification is skipped.  Should only
        be set in controlled lab environments.
    """

    def __init__(
        self,
        current_version,
        install_dir=None,
        service_name=None,
        check_interval=DEFAULT_CHECK_INTERVAL,
        insecure=False,
    ):
        super().__init__(daemon=True, name="hawk-soard-updater")
        self.current_version = current_version
        self.install_dir = install_dir or os.path.dirname(os.path.abspath(sys.argv[0]))
        self.service_name = service_name or (
            "HawkSoard" if os.name == "nt" else "hawk-soard"
        )
        self.check_interval = int(check_interval)
        self.insecure = bool(insecure)
        self._stop_event = threading.Event()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def stop(self):
        """Signal the thread to exit on the next wake-up."""
        self._stop_event.set()

    # ------------------------------------------------------------------
    # Thread body
    # ------------------------------------------------------------------

    def run(self):
        logger.info(
            "AutoUpgrader: started (version=%s interval=%ds install_dir=%s)"
            % (self.current_version, self.check_interval, self.install_dir)
        )
        # Wait for the service to fully settle before the first check.
        self._stop_event.wait(STARTUP_DELAY)
        while not self._stop_event.is_set():
            try:
                self._check_and_upgrade()
            except Exception as exc:
                logger.error("AutoUpgrader: unhandled error in update cycle: %s" % exc)
            self._stop_event.wait(self.check_interval)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _get(self, url, stream=False, timeout=60):
        """Perform an HTTP GET via requests, honouring the insecure flag."""
        import requests  # imported lazily so the module loads even without requests

        return requests.get(url, stream=stream, timeout=timeout, verify=not self.insecure)

    def _fetch_manifest(self):
        try:
            resp = self._get(VERSION_MANIFEST_URL, timeout=30)
            resp.raise_for_status()
            return resp.json()
        except Exception as exc:
            logger.warning("AutoUpgrader: failed to fetch manifest: %s" % exc)
            return None

    def _download(self, url, dest_path):
        resp = self._get(url, stream=True, timeout=180)
        resp.raise_for_status()
        with open(dest_path, "wb") as fh:
            for chunk in resp.iter_content(chunk_size=65536):
                fh.write(chunk)

    def _check_and_upgrade(self):
        logger.info("AutoUpgrader: checking %s" % VERSION_MANIFEST_URL)

        manifest = self._fetch_manifest()
        if not manifest:
            return

        remote_version = manifest.get("version", "").strip()
        if not remote_version:
            logger.warning("AutoUpgrader: manifest missing 'version' field, skipping")
            return

        if _version_tuple(remote_version) <= _version_tuple(self.current_version):
            logger.info(
                "AutoUpgrader: agent is up to date (local=%s remote=%s)"
                % (self.current_version, remote_version)
            )
            return

        logger.info(
            "AutoUpgrader: new version available %s -> %s"
            % (self.current_version, remote_version)
        )

        filename = manifest.get("filename", "").strip()
        expected_sha256 = manifest.get("sha256", "").strip().lower()
        if not filename:
            logger.error("AutoUpgrader: manifest missing 'filename', cannot upgrade")
            return

        download_url = "%s/%s" % (UPDATE_BASE_URL.rstrip("/"), filename)

        with tempfile.TemporaryDirectory(prefix="hawk-soard-upd-") as tmp:
            pkg_path = os.path.join(tmp, filename)

            logger.info("AutoUpgrader: downloading %s" % download_url)
            try:
                self._download(download_url, pkg_path)
            except Exception as exc:
                logger.error("AutoUpgrader: download failed: %s" % exc)
                return

            # Integrity check
            if expected_sha256:
                actual = _sha256_of_file(pkg_path)
                if actual != expected_sha256:
                    logger.error(
                        "AutoUpgrader: SHA-256 mismatch — expected %s got %s; "
                        "aborting update" % (expected_sha256, actual)
                    )
                    return
                logger.info("AutoUpgrader: SHA-256 verified OK")
            else:
                logger.warning(
                    "AutoUpgrader: no SHA-256 in manifest, skipping integrity check"
                )

            # Extract
            logger.info(
                "AutoUpgrader: extracting update to %s" % self.install_dir
            )
            try:
                _extract_zip(pkg_path, self.install_dir)
            except Exception as exc:
                logger.error("AutoUpgrader: extraction failed: %s" % exc)
                return

        # Update tracked version before restart so a restart-loop cannot
        # occur if the service comes back up at the same version.
        self.current_version = remote_version
        logger.info(
            "AutoUpgrader: update to %s applied; restarting service '%s'"
            % (remote_version, self.service_name)
        )

        if os.name == "nt":
            _restart_service_windows(self.service_name)
        else:
            _restart_service_linux(self.service_name)
