"""
Application update orchestration for online (cloud) and local git-based installs.
"""
from __future__ import annotations

import os
import platform
import subprocess
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from app.core.config import settings
from app.utils.logger import get_logger

logger = get_logger(__name__)

UPDATE_ADMIN_ROLES = frozenset({"super_admin", "admin"})


def app_root() -> Path:
    return Path(__file__).resolve().parents[2]


def read_version() -> str:
    vfile = app_root() / "version.txt"
    try:
        return vfile.read_text(encoding="utf-8").strip() or "0.0.0"
    except OSError:
        return "0.0.0"


def _git_available() -> bool:
    import shutil
    return shutil.which("git") is not None


def _is_windows() -> bool:
    return platform.system().lower() == "windows"


def update_script_path() -> Optional[Path]:
    deploy = app_root() / "deploy"
    if _is_windows():
        script = deploy / "auto_update.ps1"
    else:
        script = deploy / "auto_update.sh"
    return script if script.is_file() else None


def log_path() -> Path:
    return app_root() / "logs" / "auto_update.log"


def lock_path() -> Path:
    return app_root() / "logs" / "auto_update.lock"


def is_update_in_progress(max_age_minutes: int = 45) -> bool:
    lp = lock_path()
    if not lp.exists():
        return False
    try:
        age = datetime.now(timezone.utc).timestamp() - lp.stat().st_mtime
        if age > max_age_minutes * 60:
            return False
        return True
    except OSError:
        return False


def _parse_version(v: str) -> tuple:
    parts = []
    for piece in (v or "0").lstrip("v").split("."):
        try:
            parts.append(int(piece))
        except ValueError:
            parts.append(0)
    while len(parts) < 3:
        parts.append(0)
    return tuple(parts[:3])


def version_is_newer(latest: str, current: str) -> bool:
    return _parse_version(latest) > _parse_version(current)


def require_update_admin(role: Optional[str]) -> None:
    from fastapi import HTTPException
    if (role or "").lower() not in UPDATE_ADMIN_ROLES:
        raise HTTPException(
            status_code=403,
            detail="Only administrators can update the application.",
        )


def pending_commits(branch: str = "main") -> List[str]:
    if not _git_available():
        return []
    root = str(app_root())
    try:
        subprocess.run(
            ["git", "fetch", "origin", "--quiet"],
            cwd=root,
            capture_output=True,
            timeout=60,
        )
        out = subprocess.run(
            ["git", "log", f"HEAD..origin/{branch}", "--oneline", "--no-color"],
            cwd=root,
            capture_output=True,
            timeout=15,
        )
        return [
            line.strip()
            for line in out.stdout.decode("utf-8", errors="replace").splitlines()
            if line.strip()
        ]
    except Exception as exc:
        logger.warning("Could not list pending commits: %s", exc)
        return []


def current_git_commit() -> str:
    if not _git_available():
        return "unknown"
    try:
        out = subprocess.run(
            ["git", "log", "-1", "--pretty=format:%h - %s (%cr)"],
            cwd=str(app_root()),
            capture_output=True,
            timeout=5,
        )
        return out.stdout.decode("utf-8", errors="replace").strip() or "unknown"
    except Exception:
        return "unknown"


def check_for_updates() -> Dict[str, Any]:
    import requests

    current_version = read_version()
    branch = getattr(settings, "update_branch", None) or "main"
    repo = settings.github_repo
    token = settings.github_token

    result: Dict[str, Any] = {
        "current_version": current_version,
        "latest_version": None,
        "update_available": False,
        "update_reason": None,
        "release_name": None,
        "release_notes": None,
        "release_url": None,
        "published_at": None,
        "pending_commits": [],
        "git_commit": current_git_commit(),
        "deployment_mode": getattr(settings, "deployment_mode", "cloud"),
        "platform": platform.system(),
        "update_enabled": getattr(settings, "update_enabled", True),
        "can_trigger_update": bool(update_script_path()) and _git_available(),
        "in_progress": is_update_in_progress(),
        "error": None,
    }

    commits = pending_commits(branch)
    result["pending_commits"] = commits
    if commits:
        result["update_available"] = True
        result["update_reason"] = "commits"

    headers = {"Accept": "application/vnd.github+json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    try:
        resp = requests.get(
            f"https://api.github.com/repos/{repo}/releases/latest",
            headers=headers,
            timeout=12,
        )
        if resp.status_code == 404:
            if not result["update_available"]:
                result["error"] = (
                    "No GitHub releases found yet. Updates can still be applied from origin/main commits."
                )
            return result
        resp.raise_for_status()
        release = resp.json()
        latest = (release.get("tag_name") or "").lstrip("v")
        result["latest_version"] = latest
        result["release_name"] = release.get("name")
        result["release_notes"] = release.get("body")
        result["release_url"] = release.get("html_url")
        result["published_at"] = release.get("published_at")

        if latest and version_is_newer(latest, current_version):
            result["update_available"] = True
            if not result["update_reason"]:
                result["update_reason"] = "release"
    except Exception as exc:
        if not result["update_available"]:
            result["error"] = f"GitHub API: {exc}"

    return result


def read_log_tail(lines: int = 50) -> Dict[str, Any]:
    lp = log_path()
    base = {
        "current_version": read_version(),
        "git_commit": current_git_commit(),
        "has_logs": False,
        "last_success": None,
        "last_error": None,
        "recent_logs": [],
        "in_progress": is_update_in_progress(),
    }
    if not lp.exists():
        return {**base, "message": "No update logs yet."}
    try:
        content = lp.read_text(encoding="utf-8", errors="replace").splitlines()
        recent = [l.rstrip() for l in content[-lines:]]
        last_success = next(
            (l.strip() for l in reversed(content) if "[SUCCESS]" in l and "completed" in l.lower()),
            None,
        )
        last_error = next((l.strip() for l in reversed(content) if "[ERROR]" in l), None)
        return {
            **base,
            "has_logs": True,
            "last_success": last_success,
            "last_error": last_error,
            "recent_logs": recent,
        }
    except OSError as exc:
        return {**base, "message": f"Failed to read update log: {exc}"}


def trigger_update(triggered_by: str = "unknown") -> Dict[str, Any]:
    if not getattr(settings, "update_enabled", True):
        return {"success": False, "message": "Application updates are disabled on this server."}

    script = update_script_path()
    if not script:
        return {
            "success": False,
            "message": "Update script not found for this platform.",
        }

    if not _git_available():
        return {"success": False, "message": "Git is not installed — cannot pull updates."}

    if is_update_in_progress():
        return {
            "success": False,
            "message": "An update is already in progress. Check the log below.",
            "in_progress": True,
        }

    branch = getattr(settings, "update_branch", None) or "main"

    def _run():
        try:
            lp = lock_path()
            lp.parent.mkdir(parents=True, exist_ok=True)
            lp.write_text(
                f"started={datetime.now(timezone.utc).isoformat()}\nby={triggered_by}\n",
                encoding="utf-8",
            )
            root = str(app_root())
            if _is_windows():
                popen_kw: Dict[str, Any] = {"cwd": root}
                if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
                    popen_kw["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
                subprocess.Popen(
                    [
                        "powershell.exe",
                        "-ExecutionPolicy",
                        "Bypass",
                        "-File",
                        str(script),
                        "-Branch",
                        branch,
                    ],
                    **popen_kw,
                )
            else:
                subprocess.Popen(
                    ["bash", str(script), branch],
                    cwd=root,
                    start_new_session=True,
                )
            logger.info("Update triggered by %s via %s", triggered_by, script.name)
        except Exception as exc:
            logger.error("Failed to start update: %s", exc)
            try:
                lock_path().unlink(missing_ok=True)
            except OSError:
                pass

    threading.Thread(target=_run, daemon=True).start()
    return {
        "success": True,
        "message": "Update started. Pull, migrations, and restart may take 1–3 minutes.",
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "in_progress": True,
    }
