"""Linux force-upgrade: download .deb, hand off to linux_deb_upgrade.sh (independent process)."""

from __future__ import annotations

import getpass
import glob
import os
import shutil
import subprocess
import sys
import uuid
from pathlib import Path
from typing import Optional

import requests

from ws_package.log.writer import LogWriter
from ws_package.utils.error_codes import ErrorCode, ExitCode

logger = LogWriter.file(__name__)

TMP_DEB_GLOB = "/tmp/ws_force_upgrade_*.deb"
DEB_PACKAGE_NAME = "workstatus"
UPGRADE_SCRIPT_NAME = "linux_deb_upgrade.sh"
PERSISTENT_UPGRADE_SCRIPT_DIR = os.path.join(
    os.environ.get("TMPDIR", "/tmp"),
    "ws_force_upgrade",
)
DEB_AR_MAGIC = b"!<arch>\n"


def is_valid_deb_file(path: str) -> bool:
    """True if file looks like a Debian .deb (ar archive), not a raw binary."""
    try:
        with open(path, "rb") as f:
            header = f.read(8)
        if header == DEB_AR_MAGIC:
            return True
        logger.error(
            "[linux_upgrade] invalid .deb header %r in %s (API may have returned a binary, not .deb)",
            header,
            path,
        )
        return False
    except OSError as exc:
        logger.error("[linux_upgrade] cannot read %s: %s", path, exc)
        return False


def _upgrade_log_path() -> Path:
    return Path(os.path.expanduser("~")) / ".workstatus" / "logs" / "linux_force_upgrade.log"


def resolve_linux_deb_upgrade_script() -> Path:
    """Locate linux_deb_upgrade.sh next to frozen binary or under workstatus/."""
    candidates: list[Path] = []
    if getattr(sys, "frozen", False):
        meipass = getattr(sys, "_MEIPASS", None)
        if meipass:
            candidates.append(Path(meipass) / UPGRADE_SCRIPT_NAME)
        candidates.append(Path(sys.executable).resolve().parent / UPGRADE_SCRIPT_NAME)
    candidates.append(Path.home() / "Workstatus" / UPGRADE_SCRIPT_NAME)
    here = Path(__file__).resolve()
    # .../workstatus/ws_package/utils/update_linux.py -> .../workstatus/linux_deb_upgrade.sh
    candidates.append(here.parents[2] / UPGRADE_SCRIPT_NAME)
    candidates.append(here.parents[3] / "workstatus" / UPGRADE_SCRIPT_NAME)

    for path in candidates:
        if path.is_file():
            return path
    raise FileNotFoundError(
        f"{UPGRADE_SCRIPT_NAME} not found; checked: {[str(p) for p in candidates]}"
    )


def materialize_linux_upgrade_script() -> Path:
    """
    Return a path to linux_deb_upgrade.sh that still exists after the agent exits.

    In onefile builds the script lives under _MEIPASS; copy it to a stable temp
    directory so the detached bash process can run after Python exits.
    """
    source = resolve_linux_deb_upgrade_script()
    dest_dir = Path(PERSISTENT_UPGRADE_SCRIPT_DIR)
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest = dest_dir / UPGRADE_SCRIPT_NAME

    meipass = getattr(sys, "_MEIPASS", None)
    in_meipass = bool(
        meipass and str(source.resolve()).lower().startswith(str(Path(meipass).resolve()).lower())
    )
    if getattr(sys, "frozen", False) and in_meipass:
        shutil.copy2(source, dest)
        dest.chmod(dest.stat().st_mode | 0o111)
        logger.info(
            "[linux_upgrade] materialized upgrade script to persistent location: %s",
            dest,
        )
        return dest

    install_script = Path.home() / "Workstatus" / UPGRADE_SCRIPT_NAME
    if not install_script.is_file() or source.resolve() != install_script.resolve():
        try:
            install_script.parent.mkdir(parents=True, exist_ok=True)
            if not install_script.is_file() or os.path.getmtime(source) > os.path.getmtime(install_script):
                shutil.copy2(source, install_script)
                install_script.chmod(install_script.stat().st_mode | 0o111)
                logger.info("[linux_upgrade] copied upgrade script to: %s", install_script)
        except OSError as exc:
            logger.warning("[linux_upgrade] could not copy script to %s: %s", install_script, exc)

    if not dest.is_file() or source.resolve() != dest.resolve():
        try:
            if not dest.is_file() or os.path.getmtime(source) > os.path.getmtime(dest):
                shutil.copy2(source, dest)
                dest.chmod(dest.stat().st_mode | 0o111)
                logger.info("[linux_upgrade] copied upgrade script to: %s", dest)
                return dest
        except OSError as exc:
            logger.warning("[linux_upgrade] could not copy script to %s: %s", dest, exc)

    if install_script.is_file():
        return install_script
    return source


def ensure_linux_deb_upgrade_script_in_install_dir(install_dir: Path) -> bool:
    """Copy bundled linux_deb_upgrade.sh into ~/Workstatus if missing."""
    dest = install_dir / UPGRADE_SCRIPT_NAME
    if dest.is_file():
        return True
    try:
        source = resolve_linux_deb_upgrade_script()
    except FileNotFoundError:
        return False
    if source.resolve() == dest.resolve():
        return True
    try:
        install_dir.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, dest)
        dest.chmod(dest.stat().st_mode | 0o111)
        logger.info("[linux_upgrade] installed upgrade script at %s", dest)
        return True
    except OSError as exc:
        logger.warning("[linux_upgrade] could not install upgrade script at %s: %s", dest, exc)
        return False


class update_linux_build:
    """Download a .deb and spawn linux_deb_upgrade.sh as an independent process."""

    def __init__(self, version: str) -> None:
        self.updated_version = version
        self.deb_path: Optional[str] = None
        self._upgrade_id = uuid.uuid4().hex[:12]
        logger.info("[linux_upgrade] init target_version=%s upgrade_id=%s", version, self._upgrade_id)

    def _cleanup_old_temp_debs(self, keep_path: Optional[str] = None) -> None:
        keep_resolved = None
        if keep_path:
            try:
                keep_resolved = os.path.realpath(keep_path)
            except OSError:
                keep_resolved = keep_path
        for pattern in (TMP_DEB_GLOB,):
            for path in glob.glob(pattern):
                if not str(path).endswith(".deb"):
                    continue
                if keep_resolved:
                    try:
                        if os.path.realpath(path) == keep_resolved:
                            continue
                    except OSError:
                        pass
                try:
                    os.remove(path)
                    logger.info("[linux_upgrade] removed previous temp .deb: %s", path)
                except OSError as exc:
                    logger.warning("[linux_upgrade] could not remove %s: %s", path, exc)

    def _strip_profile_venv_line(self, username: str) -> None:
        """Remove legacy venv-based autostart lines from all shell rc files.

        The old installer injected a `nohup bash -c 'source …/activate …'` line
        into ~/.profile only.  Modern shells also source ~/.bashrc and ~/.zshrc,
        so clean all three to cover users who switched shells or use custom setups.
        """
        home_dir = os.path.expanduser("~")
        target_prefix = f"nohup bash -c 'source /home/{username}/Workstatus/venv/bin/activate"

        rc_files = [".profile", ".bashrc", ".zshrc"]
        for rc_name in rc_files:
            rc_path = os.path.join(home_dir, rc_name)
            if not os.path.exists(rc_path):
                continue
            try:
                with open(rc_path, "r", encoding="utf-8") as file:
                    lines = file.readlines()
                filtered_lines = [line for line in lines if target_prefix not in line]
                if len(filtered_lines) != len(lines):
                    with open(rc_path, "w", encoding="utf-8") as file:
                        file.writelines(filtered_lines)
                    logger.info("[linux_upgrade] removed legacy venv autostart line(s) from %s", rc_path)
            except OSError as exc:
                logger.warning("[linux_upgrade] could not clean %s: %s", rc_path, exc)

    def _ensure_upgrade_log_dir(self, log_file: Path) -> None:
        try:
            log_file.parent.mkdir(parents=True, exist_ok=True)
        except OSError as exc:
            logger.warning("[linux_upgrade] could not create log dir %s: %s", log_file.parent, exc)

    def _log_python_handoff(self, log_file: Path, script_path: Path, deb_path: str) -> None:
        from datetime import datetime, timezone

        ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%dT%H:%M:%S%z")
        line = (
            f"[python] {ts} upgrade_id={self._upgrade_id} "
            f"target_version={self.updated_version!r} deb={deb_path} "
            f"script={script_path}\n"
        )
        try:
            with open(log_file, "a", encoding="utf-8") as f:
                f.write(line)
        except OSError as exc:
            logger.warning("[linux_upgrade] could not append handoff log: %s", exc)

    async def swap_build(self) -> bool:
        if not self.deb_path or not os.path.isfile(self.deb_path):
            logger.error("[linux_upgrade] swap_build: missing .deb at %s", self.deb_path)
            return False

        self._strip_profile_venv_line(getpass.getuser())

        log_file = _upgrade_log_path()
        self._ensure_upgrade_log_dir(log_file)

        try:
            script_path = materialize_linux_upgrade_script()
        except FileNotFoundError as exc:
            logger.error("[linux_upgrade] %s", exc)
            return False

        if not os.access(script_path, os.X_OK):
            try:
                script_path.chmod(script_path.stat().st_mode | 0o111)
            except OSError as chmod_err:
                logger.warning("[linux_upgrade] could not chmod script: %s", chmod_err)

        self._log_python_handoff(log_file, script_path, self.deb_path)
        logger.info("[linux_upgrade] spawning independent process: %s", script_path)
        logger.info("[linux_upgrade] deb=%s upgrade_id=%s", self.deb_path, self._upgrade_id)
        logger.info("[linux_upgrade] detailed log: %s", log_file)

        subprocess.Popen(
            [
                "/bin/bash",
                str(script_path),
                self.deb_path,
                self._upgrade_id,
                str(self.updated_version),
            ],
            env=os.environ.copy(),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            start_new_session=True,
            close_fds=True,
        )
        logger.critical("%s", ErrorCode.UPGRADE_LINUX)
        sys.exit(ExitCode.OK)
        return True  # unreachable; satisfies type checkers

    async def download_file(self, url, max_retries=3):
        deb_name = f"ws_force_upgrade_{self._upgrade_id}.deb"
        new_build = os.path.join("/tmp", deb_name)
        self.deb_path = new_build
        self._cleanup_old_temp_debs(keep_path=new_build)

        log_file = _upgrade_log_path()
        self._ensure_upgrade_log_dir(log_file)
        from datetime import datetime, timezone

        ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%dT%H:%M:%S%z")
        try:
            with open(log_file, "a", encoding="utf-8") as f:
                f.write(
                    f"[python] {ts} download_start upgrade_id={self._upgrade_id} "
                    f"url={url} path={new_build}\n"
                )
        except OSError:
            pass

        logger.info("[linux_upgrade] download url=%s path=%s", url, new_build)
        logger.info("[linux_upgrade] detailed log: %s", log_file)
        logger.info("[linux_upgrade] expect .deb for package name %s", DEB_PACKAGE_NAME)

        for attempt in range(max_retries):
            try:
                response = requests.get(url, stream=True, timeout=300)
                if response.status_code != 200:
                    logger.error("[linux_upgrade] HTTP error: %s", response.status_code)
                    return False

                content_length = response.headers.get("content-length")
                expected_size = int(content_length) if content_length is not None else 0
                logger.info(
                    "[linux_upgrade] attempt %s/%s expect_bytes=%s",
                    attempt + 1,
                    max_retries,
                    expected_size,
                )

                with open(new_build, "wb") as out:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:
                            out.write(chunk)

                actual_size = os.path.getsize(new_build)
                logger.info(
                    "[linux_upgrade] wrote bytes=%s (declared content-length=%s)",
                    actual_size,
                    expected_size,
                )

                if expected_size > 0 and actual_size != expected_size:
                    logger.warning("[linux_upgrade] size mismatch; retrying")
                    if attempt < max_retries - 1:
                        try:
                            os.remove(new_build)
                        except OSError:
                            pass
                        continue
                    logger.error("[linux_upgrade] download failed after retries (size)")
                    os.remove(new_build)
                    return False

                if not is_valid_deb_file(new_build):
                    logger.error(
                        "[linux_upgrade] downloaded file is not a .deb — check "
                        "linux_deb_download_url_override in config.ini"
                    )
                    try:
                        os.remove(new_build)
                    except OSError:
                        pass
                    if attempt < max_retries - 1:
                        continue
                    return False

                logger.info("[linux_upgrade] download ok; handing off to %s", UPGRADE_SCRIPT_NAME)
                if await self.swap_build():
                    return True
                logger.error("[linux_upgrade] handoff failed after successful download")
                return False

            except Exception as e:
                logger.error("[linux_upgrade] download attempt %s failed: %s", attempt + 1, e, exc_info=True)
                if attempt < max_retries - 1:
                    try:
                        if os.path.isfile(new_build):
                            os.remove(new_build)
                    except OSError:
                        pass
                    continue
                if os.path.isfile(new_build):
                    try:
                        os.remove(new_build)
                    except OSError:
                        pass
                return False

        return False
