import stat
import subprocess
import tempfile
import uuid
from pathlib import Path

import requests

from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)

# Desktop DMG from API must ship Workstatus-Desktop.app (not the Python agent bundle).
DESKTOP_APP_NAMES = (
    "Workstatus-Desktop.app",
    "workstatus-desktop.app",
)
AGENT_ONLY_APP_NAME = "Workstatus.app"
APPLICATIONS_DIR = Path("/Applications")


class MacOSUpdater:
    """
    macOS force-upgrade handoff (aligned with Windows NSIS flow):

    1) Download desktop .dmg from API download_link
    2) Spawn detached bash script
    3) Script stops processes, cleans Application Support/Workstatus + old apps
    4) Copies .app from DMG to /Applications and opens it
    5) Desktop app reinstalls Python agent / packages on launch
    """

    def __init__(self):
        self._upgrade_id = uuid.uuid4().hex[:12]
        # Must live outside Application Support/Workstatus — the upgrade script deletes that tree.
        self.primary_dmg = (
            Path(tempfile.gettempdir()) / f"ws_mac_force_upgrade_{self._upgrade_id}.dmg"
        )
        self.log_file = Path.home() / ".workstatus" / "logs" / "mac_force_upgrade.log"
        self.log_file.parent.mkdir(parents=True, exist_ok=True)

    async def download_file(self, url, max_retries=3):
        download_url = (url or "").strip()
        if not download_url:
            logger.error("[mac_upgrade] empty download URL")
            return False

        if ".dmg" not in download_url.lower():
            logger.warning("[mac_upgrade] URL does not look like DMG: %s", download_url)

        try:
            self.primary_dmg.unlink(missing_ok=True)
        except Exception as e:
            logger.warning("[mac_upgrade] could not remove stale dmg %s: %s", self.primary_dmg, e)

        dmg_path = self.primary_dmg
        logger.info("[mac_upgrade] Downloading DMG from %s to %s", download_url, dmg_path)

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

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

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

                actual_size = dmg_path.stat().st_size
                if expected_size > 0 and actual_size != expected_size:
                    logger.warning(
                        "[mac_upgrade] size mismatch expected=%s got=%s",
                        expected_size,
                        actual_size,
                    )
                    try:
                        dmg_path.unlink(missing_ok=True)
                    except Exception:
                        pass
                    if attempt < max_retries - 1:
                        continue
                    return False

                if not self._looks_like_dmg(dmg_path):
                    logger.error("[mac_upgrade] downloaded file is not a DMG: %s", dmg_path)
                    try:
                        dmg_path.unlink(missing_ok=True)
                    except Exception:
                        pass
                    if attempt < max_retries - 1:
                        continue
                    return False

                logger.info("[mac_upgrade] DMG downloaded (%s bytes): %s", actual_size, dmg_path)
                return self._spawn_upgrade_handoff(dmg_path)
            except Exception as e:
                logger.error(
                    "[mac_upgrade] download attempt %s failed: %s",
                    attempt + 1,
                    e,
                    exc_info=True,
                )
                try:
                    dmg_path.unlink(missing_ok=True)
                except Exception:
                    pass
                if attempt == max_retries - 1:
                    return False

        return False

    @staticmethod
    def _looks_like_dmg(path: Path) -> bool:
        try:
            return path.stat().st_size > 100 * 1024
        except Exception:
            return False

    def _spawn_upgrade_handoff(self, dmg_path: Path) -> bool:
        script_path = Path(tempfile.gettempdir()) / f"ws_mac_force_upgrade_{self._upgrade_id}.sh"
        home = Path.home()
        app_support_workstatus = home / "Library" / "Application Support" / "Workstatus"
        app_support_desktop = home / "Library" / "Application Support" / "Workstatus-Desktop"
        launchagent_plist = home / "Library" / "LaunchAgents" / "com.workstatus.engine.plist"
        legacy_agent_app = APPLICATIONS_DIR / "Workstatus.app"
        desktop_app_names = " ".join(DESKTOP_APP_NAMES)

        script = f"""#!/bin/bash
set -u

UPGRADE_ID="{self._upgrade_id}"
DMG_PATH="{dmg_path}"
LOG_FILE="{self.log_file}"
MOUNT_POINT="/Volumes/Workstatus_Upgrade_${{UPGRADE_ID}}"
APP_SUPPORT="{app_support_workstatus}"
APP_SUPPORT_DESKTOP="{app_support_desktop}"
PLIST="{launchagent_plist}"
LEGACY_APP="{legacy_agent_app}"
APPLICATIONS="{APPLICATIONS_DIR}"
DESKTOP_APP_NAMES="{desktop_app_names}"

log() {{
  local msg="$1"
  /bin/echo "[mac-upgrade] $(/bin/date '+%Y-%m-%d %H:%M:%S') $msg" >> "$LOG_FILE"
}}

stop_port_listeners() {{
  local port="$1"
  local proto="$2"
  local pids
  if [ "$proto" = "udp" ]; then
    pids="$(/usr/sbin/lsof -tiUDP:"$port" 2>/dev/null || true)"
  else
    pids="$(/usr/sbin/lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)"
  fi
  if [ -n "$pids" ]; then
    log "stopping $proto listeners on port $port: $pids"
    /bin/kill -9 $pids 2>/dev/null || true
  fi
}}

find_desktop_app_in_mount() {{
  local name found
  for name in $DESKTOP_APP_NAMES; do
    found="$(/usr/bin/find "$MOUNT_POINT" -maxdepth 5 -name "$name" -print -quit 2>/dev/null || true)"
    if [ -n "$found" ] && [ -d "$found" ]; then
      /bin/echo "$found"
      return 0
    fi
  done
  found="$(/usr/bin/find "$MOUNT_POINT" -maxdepth 5 -name '{AGENT_ONLY_APP_NAME}' -print -quit 2>/dev/null || true)"
  if [ -n "$found" ] && [ -d "$found" ]; then
    log "ERROR dmg contains only {AGENT_ONLY_APP_NAME} (python agent) — API must return Workstatus-Desktop DMG"
    return 1
  fi
  return 1
}}

remove_tree() {{
  local target="$1"
  local label="$2"
  if [ ! -e "$target" ]; then
    log "$label: already absent ($target)"
    return 0
  fi
  /bin/chmod -R u+w "$target" 2>/dev/null || true
  /bin/rm -rf "$target" 2>/dev/null || true
  if [ ! -e "$target" ]; then
    log "$label: removed $target"
    return 0
  fi
  log "ERROR $label: failed to remove $target"
  return 1
}}

log "===== mac dmg upgrade start upgrade_id=${{UPGRADE_ID}} dmg=$DMG_PATH ====="
/bin/sleep 2

log "stopping Workstatus processes"
/usr/bin/pkill -f "Workstatus-Desktop" >/dev/null 2>&1 || true
/usr/bin/pkill -f "workstatus-desktop" >/dev/null 2>&1 || true
/usr/bin/pkill -f "Workstatus.app/Contents/MacOS" >/dev/null 2>&1 || true
/usr/bin/pkill -f "Workstatus" >/dev/null 2>&1 || true
/bin/sleep 2

stop_port_listeners 12345 udp
stop_port_listeners 54321 tcp

log "unloading LaunchAgent (if any)"
/bin/launchctl bootout "gui/$(/usr/bin/id -u)/com.workstatus.engine" >/dev/null 2>&1 || true
/bin/rm -f "$PLIST" 2>/dev/null || true

log "removing Login Item (best effort)"
/usr/bin/osascript -e 'tell application "System Events" to delete every login item whose name is "Workstatus"' >/dev/null 2>&1 || true

log "cleaning Application Support/Workstatus (agent, DB, caches — desktop will reinstall Python)"
remove_tree "$APP_SUPPORT" "appdata" || true

log "cleaning Application Support/Workstatus-Desktop (Electron user data)"
remove_tree "$APP_SUPPORT_DESKTOP" "electron-userdata" || true

log "removing legacy /Applications/Workstatus.app if present"
remove_tree "$LEGACY_APP" "legacy-agent-app" || true

log "removing existing desktop apps in /Applications"
for name in $DESKTOP_APP_NAMES; do
  remove_tree "$APPLICATIONS/$name" "desktop-app" || true
done

if [ ! -f "$DMG_PATH" ]; then
  log "ERROR dmg not found at $DMG_PATH"
  exit 1
fi

/usr/bin/hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
if ! /usr/bin/hdiutil attach "$DMG_PATH" -nobrowse -mountpoint "$MOUNT_POINT" >> "$LOG_FILE" 2>&1; then
  log "ERROR failed to mount dmg"
  exit 1
fi

SRC_APP="$(find_desktop_app_in_mount || true)"
if [ -z "$SRC_APP" ] || [ ! -d "$SRC_APP" ]; then
  log "ERROR no Workstatus-Desktop.app in dmg — check API download_link points to desktop DMG"
  /usr/bin/hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
  exit 1
fi

APP_BASENAME="$(/usr/bin/basename "$SRC_APP")"
TARGET_APP="$APPLICATIONS/$APP_BASENAME"
log "installing desktop app $SRC_APP -> $TARGET_APP"

if ! /usr/bin/ditto "$SRC_APP" "$TARGET_APP" >> "$LOG_FILE" 2>&1; then
  log "ERROR failed to copy app bundle to Applications"
  /usr/bin/hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
  exit 1
fi

/usr/bin/xattr -dr com.apple.quarantine "$TARGET_APP" >/dev/null 2>&1 || true
/usr/bin/xattr -dr com.apple.provenance "$TARGET_APP" >/dev/null 2>&1 || true
/bin/chmod -R u+rwX "$TARGET_APP" >/dev/null 2>&1 || true

INSTALLED_VER="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$TARGET_APP/Contents/Info.plist" 2>/dev/null || /bin/echo unknown)"
log "installed desktop version: $INSTALLED_VER at $TARGET_APP"

/usr/bin/hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
/bin/rm -f "$DMG_PATH" >/dev/null 2>&1 || true

log "launching $TARGET_APP (desktop will install Python agent on first run)"
/usr/bin/open "$TARGET_APP" >/dev/null 2>&1 || true

log "===== mac dmg upgrade end upgrade_id=${{UPGRADE_ID}} ====="
"""

        try:
            script_path.write_text(script, encoding="utf-8")
            current_mode = script_path.stat().st_mode
            script_path.chmod(current_mode | stat.S_IXUSR)

            subprocess.Popen(
                ["/bin/bash", str(script_path)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                stdin=subprocess.DEVNULL,
                start_new_session=True,
                close_fds=True,
            )
            logger.info(
                "[mac_upgrade] spawned detached updater script=%s log=%s",
                script_path,
                self.log_file,
            )
            return True
        except Exception as e:
            logger.error(
                "[mac_upgrade] failed to spawn detached updater: %s",
                e,
                exc_info=True,
            )
            return False
