from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import secrets
import shutil
import subprocess
import time

from ..core.api_client import PassiveApiClient
from ..core.config import AgentConfig
from ..core.pairing import PairingCoordinator
from ..core.reconnect import ReconnectPolicy
from ..core.runtime import PassiveHeartbeatRuntime
from ..core.storage import JsonFileSecureStorage, SecureStorage
from ..core.update_model import (
    UpdateManifest,
    evaluate_release_for_upgrade,
    package_version_covers_target,
    release_install_allowed,
    release_payload_checksum,
    resolve_upgrade_current_version,
)
from ..core.version import AGENT_MVP_VERSION, LEGACY_AGENT_VERSION_ALIASES
from .active.audit_logger import CommandAuditLogger
from .active.capability_store import CapabilityStore
from .active.command_executor import CommandExecutor
from .active.command_models import ConfirmationCallback
from .active.confirmation_bridge import ConfirmationBridge
from .active.device_capability_scanner import (
    get_scan_scheduler,
    is_profile_stale,
    load_profile,
    to_heartbeat_summary,
)
from .active.device_scan_scheduler import get_device_scan_scheduler
from .active.heartbeat_extras_store import HeartbeatExtrasStore
from .active.pipeline import ActiveCommandPipeline
from .active.policy_engine import PolicyEngine
from .desktop_event_logger import DesktopAgentEventLogger
from .status_resolver import (
    RuntimeEvidenceStore,
    is_official_channel_update_eligible,
    is_policy_trusted,
    normalize_trust_for_policy,
    resolve_canonical_status,
)


@dataclass
class DesktopAgentMVP:
    """Passive desktop client MVP with tray/settings placeholders."""

    config: AgentConfig
    pairing: PairingCoordinator
    reconnect: ReconnectPolicy
    tray_ui_enabled: bool = True
    settings_ui_enabled: bool = True
    update_channel: str = "stable"
    event_logger: DesktopAgentEventLogger | None = None

    def __post_init__(self) -> None:
        self._runtime_evidence_store = RuntimeEvidenceStore(self._runtime_evidence_path())
        self._capability_store = CapabilityStore()
        self._policy_engine = PolicyEngine()
        self._heartbeat_extras_store = HeartbeatExtrasStore()
        self._device_capability_scan_scheduler = get_scan_scheduler()
        self._device_scan_scheduler = get_device_scan_scheduler(
            capability_scheduler=self._device_capability_scan_scheduler,
        )
        self._device_capability_scan_scheduler.on_apps_discovered = self._on_device_apps_discovered
        self._runtime = PassiveHeartbeatRuntime(
            pairing=self.pairing,
            reconnect=self.reconnect,
            app_version=self.config.app_version,
            device_type="desktop",
            heartbeat_interval_sec=self.config.heartbeat_interval_sec,
            nonce_factory=lambda: secrets.token_hex(16),
            status_callback=self._on_runtime_status,
            heartbeat_metadata_factory=self._build_heartbeat_metadata,
            heartbeat_response_handler=self._on_heartbeat_response,
        )
        self._command_executor = CommandExecutor(
            app_version=self.config.app_version,
            snapshot_provider=lambda: self._runtime.snapshot(),
            logs_path_provider=lambda: str(self.event_logger.log_path) if self.event_logger else "",
            update_check_fn=lambda: self.upgrade_in_place(dry_run=True),
            cloud_fetch_fn=self._cloud_fetch_bounded,
            cloud_summarize_fn=self._cloud_summarize_content,
            cloud_generate_fn=self._cloud_generate_content,
        )
        self._command_audit_logger = CommandAuditLogger()
        self._confirmation_bridge = ConfirmationBridge()
        self._active_pipeline = ActiveCommandPipeline(
            pairing=self.pairing,
            policy_engine=self._policy_engine,
            capability_store=self._capability_store,
            trust_resolver=self._resolve_trust_state,
            app_version=self.config.app_version,
            command_executor=self._command_executor,
            snapshot_provider=lambda: self._runtime.snapshot(),
            audit_logger=self._command_audit_logger,
            confirmation_callback=self._confirmation_bridge.request,
        )
        self._runtime.set_active_pipeline(self._active_pipeline)
        if not self._runtime_evidence_store.path.exists():
            self._runtime_evidence_store.save_snapshot(self._runtime.snapshot())
        self._log_event("agent_initialized", update_channel=self.update_channel)

    def _schedule_startup_capability_scan(self) -> None:
        self._maybe_schedule_device_capability_scan(reason="agent_start", force=True)
        if self.is_paired() and is_policy_trusted(self._resolve_trust_state()):
            self._device_scan_scheduler.start()

    def _on_device_apps_discovered(self, apps: list[dict[str, object]]) -> None:
        if not apps:
            return
        self._command_audit_logger.log_device_apps_discovered(apps=apps)
        self._log_event(
            "device_apps_discovered",
            count=len(apps),
            names=[str(item.get("name") or "")[:80] for item in apps[:5]],
        )

    def _maybe_schedule_device_capability_scan(self, *, reason: str = "", force: bool = False) -> bool:
        if not self.is_paired():
            return False
        if not is_policy_trusted(self._resolve_trust_state()):
            return False
        return self._device_capability_scan_scheduler.schedule_if_needed(force=force, reason=reason)

    def pair_with_token(self, pair_token: str) -> str:
        self._log_event("pair_attempt")
        if hasattr(self.pairing.storage, "set_ephemeral_pair_token"):
            self.pairing.storage.set_ephemeral_pair_token(pair_token)
        result = self.pairing.pair_device(
            pair_token=pair_token,
            app_version=self.config.app_version,
            device_type="desktop",
        )
        self._runtime_evidence_store.save_snapshot(self._runtime.snapshot())
        self._log_event("pair_result", status=result.status)
        return result.status

    def unpair_device(self, *, reason: str = "user_request") -> dict[str, object]:
        """Clear local pairing credentials so the device can be re-paired."""
        state = self.pairing.storage.load_state()
        had_state = state is not None
        previous_device_id = str(getattr(state, "device_id", "") or "") if state is not None else ""
        self.pairing.storage.clear_paired_state()
        self._runtime.snapshot().status = "WaitingForPairing"
        self._runtime.snapshot().last_error = ""
        self._runtime.snapshot().failure_count = 0
        self._runtime.snapshot().last_heartbeat_at = ""
        self._runtime_evidence_store.save_snapshot(self._runtime.snapshot())
        self._log_event("unpair_device", reason=reason, had_state=had_state, device_id=previous_device_id)
        return {
            "ok": True,
            "had_state": had_state,
            "device_id": previous_device_id,
            "reason": reason,
            "storage_path": str(getattr(self.pairing.storage, "file_path", "")),
        }

    def heartbeat_cycle(self, nonce: str) -> dict[str, object]:
        return self.pairing.build_signed_heartbeat(
            nonce=nonce,
            app_version=self.config.app_version,
            device_type="desktop",
        )

    def run_background_loop(self, *, iterations: int | None = None) -> str:
        self._log_event("heartbeat_loop_start", iterations=iterations)
        self._runtime.run_loop(iterations=iterations)
        self._runtime_evidence_store.save_snapshot(self._runtime.snapshot())
        self._log_event("heartbeat_loop_end", status=self.current_status())
        return self._canonical_status()["runtime_state"]

    def current_status(self) -> str:
        return str(self._canonical_status().get("runtime_state") or "Offline")

    def is_paired(self) -> bool:
        return self.pairing.storage.load_state() is not None

    def tray_view_model(self) -> dict[str, object]:
        canonical = self._canonical_status()
        runtime_state = str(canonical.get("runtime_state") or "Offline")
        trust_state = str(canonical.get("trust_state") or "Unpaired")
        device_state = str(canonical.get("device_state") or "Unknown")
        return {
            "enabled": self.tray_ui_enabled,
            "status": runtime_state,
            "runtime_state": runtime_state,
            "connectivity": runtime_state,
            "paired": bool(canonical.get("paired", False)),
            "trust_state": trust_state,
            "device_state": device_state,
            "last_heartbeat_at": str(canonical.get("last_heartbeat_at") or ""),
            "heartbeat_age_sec": canonical.get("heartbeat_age_sec"),
            "trust_badge": trust_state,
            "pairing_timestamp": self._pairing_timestamp_iso(),
            "active_controlled_candidate": True,
            "status_source_version": canonical.get("status_source_version"),
            "actions": [
                "open_iuve_connect",
                "open_logs",
                "restart_agent",
                "show_status",
                "voice_push_to_talk",
                "voice_manual_mic",
                "voice_cancel",
            ],
            "menu_labels": {
                "open_iuve_connect": "Open IuVe Connect",
                "open_logs": "Open Logs",
                "restart_agent": "Restart Agent",
                "show_status": "Local Status",
                "voice_push_to_talk": "Voice: Push-to-Talk",
                "voice_manual_mic": "Voice: Manual Mic",
                "voice_cancel": "Voice: Cancel Session",
            },
            "voice_presence": self._voice_presence_tray_block(),
        }

    def _voice_presence_tray_block(self) -> dict[str, object]:
        try:
            from iuve_connect_agents.desktop.voice.voice_presence_service import (
                VoicePresenceService,
                voice_presence_enabled,
                voice_wake_word_enabled,
            )

            service = VoicePresenceService()
            settings = service.settings_block()
            return {
                "enabled": voice_presence_enabled(),
                "wake_word_enabled": voice_wake_word_enabled(),
                "push_to_talk_key": settings.get("push_to_talk_key"),
                "state": service.snapshot().state.value,
                "input_modes": settings.get("input_modes"),
            }
        except Exception:
            return {
                "enabled": False,
                "wake_word_enabled": False,
                "push_to_talk_key": "Ctrl+Shift+Space",
                "state": "disabled",
                "input_modes": ["push_to_talk", "manual_mic"],
            }

    def settings_screen_placeholder(self) -> dict[str, object]:
        return {
            "enabled": self.settings_ui_enabled,
            "sections": [
                "Connection",
                "Pairing",
                "Heartbeat",
                "Storage",
                "Notifications",
                "Recovery",
                "Voice Presence",
                "Advanced",
            ],
            "active_controlled_candidate": True,
            "execution_surface": "allowlisted_commands_only",
        }

    def update_channel_placeholder(self) -> dict[str, object]:
        return {
            "enabled": True,
            "channel": self.update_channel,
            "auto_update": False,
            "message": "Use the 'upgrade' command for in-place package update.",
        }

    def upgrade_in_place(self, *, dry_run: bool = False) -> dict[str, object]:
        state = self.pairing.storage.load_state()
        if state is None:
            return {
                "ok": False,
                "status": "blocked",
                "reason": "device_not_paired",
                "dry_run": bool(dry_run),
            }

        current_version = self._upgrade_current_version()

        try:
            release = self.pairing.api_client.get_desktop_release_latest(
                platform="linux",
                auth_token=state.device_auth_token,
            )
        except Exception as exc:
            return {
                "ok": False,
                "status": "failed",
                "reason": "release_lookup_failed",
                "error": str(exc),
                "dry_run": bool(dry_run),
            }

        target_version = str(release.get("version") or "").strip()
        installed_pkg = self._installed_package_version()
        release_checksum = release_payload_checksum(release)
        if package_version_covers_target(installed_pkg, target_version):
            return {
                "ok": True,
                "status": "up_to_date",
                "reason": "already_up_to_date",
                "current_version": installed_pkg,
                "target_version": target_version,
                "dry_run": bool(dry_run),
                "available_version": target_version,
                "release_size_bytes": int(release.get("size_bytes") or 0),
                "release_checksum": release_checksum,
                "would_upgrade": False,
            }

        decision = evaluate_release_for_upgrade(
            release_payload=release,
            current_version=current_version,
            update_channel=self.update_channel,
        )
        if not decision.ok:
            return {
                "ok": False,
                "status": "blocked",
                "reason": decision.reason,
                "current_version": current_version,
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
                "available_version": decision.target_version,
                "release_size_bytes": int(release.get("size_bytes") or 0),
                "release_checksum": release_checksum,
                "would_upgrade": False,
            }
        if not decision.update_available:
            return {
                "ok": True,
                "status": "up_to_date",
                "reason": decision.reason,
                "current_version": current_version,
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
                "available_version": decision.target_version,
                "release_size_bytes": int(release.get("size_bytes") or 0),
                "release_checksum": release_checksum,
                "would_upgrade": False,
            }

        manifest = UpdateManifest(
            version=str(release.get("version") or ""),
            channel=str(release.get("channel") or ""),
            signature=str(release.get("signature_file") or ""),
            url=str(release.get("download_url") or ""),
            signed=bool(release.get("signed", False)),
            signature_available=bool(release.get("signature_available", False)),
            signature_valid=bool(release.get("signature_valid", False)),
            signature_algorithm=str(release.get("signature_algorithm") or ""),
            public_key_id=str(release.get("public_key_id") or ""),
        )
        if not release_install_allowed(
            manifest,
            trusted_official_channel=is_official_channel_update_eligible(self._resolve_trust_state()),
        )[0]:
            return {
                "ok": False,
                "status": "blocked",
                "reason": "release_signature_policy_failed",
                "current_version": current_version,
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
                "available_version": decision.target_version,
                "release_size_bytes": int(release.get("size_bytes") or 0),
                "release_checksum": release_checksum,
                "would_upgrade": False,
            }

        if dry_run:
            return {
                "ok": True,
                "status": "dry_run",
                "reason": "eligible",
                "dry_run": True,
                "current_version": current_version,
                "available_version": decision.target_version,
                "target_version": decision.target_version,
                "release_size_bytes": int(release.get("size_bytes") or 0),
                "release_checksum": release_checksum,
                "would_upgrade": True,
                "would_use_command": "dpkg -i <downloaded.deb>",
            }

        filename = str(release.get("filename") or f"iuve-connect-desktop-agent_{decision.target_version}_all.deb")
        cache_dir = Path.home() / ".cache" / "iuve-connect-agent" / "updates"
        package_path = cache_dir / filename

        try:
            download_meta = self.pairing.api_client.download_desktop_release(
                platform="linux",
                auth_token=state.device_auth_token,
                destination_path=package_path,
            )
        except Exception as exc:
            return {
                "ok": False,
                "status": "failed",
                "reason": "release_download_failed",
                "error": str(exc),
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
            }
        header_checksum = str(download_meta.get("checksum") or "").strip().lower()
        local_checksum = self._sha256_file(package_path)
        if release_checksum != local_checksum or (header_checksum and header_checksum != local_checksum):
            try:
                package_path.unlink(missing_ok=True)
            except Exception:
                pass
            return {
                "ok": False,
                "status": "blocked",
                "reason": "checksum_mismatch",
                "current_version": current_version,
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
            }

        if os.geteuid() != 0:
            install_result = self._install_deb_with_privilege(package_path)
            exit_code = int(install_result.get("exit_code") or 1)
            install_cmd = install_result.get("install_command")
            install_stderr = str(install_result.get("stderr") or "").strip()
            install_reason = str(install_result.get("reason") or "dpkg_install_failed")
            if exit_code != 0:
                if install_reason == "installed":
                    install_reason = "install_failed"
                if package_version_covers_target(installed_pkg, decision.target_version):
                    return {
                        "ok": True,
                        "status": "up_to_date",
                        "reason": "already_up_to_date",
                        "current_version": installed_pkg,
                        "target_version": decision.target_version,
                        "package_path": str(package_path),
                        "dry_run": bool(dry_run),
                        "pairing_state_intact": self._pairing_state_matches(state),
                        "install_stderr": install_stderr,
                        "would_upgrade": False,
                    }
                pairing_intact = self._pairing_state_matches(state)
                return {
                    "ok": False,
                    "status": "failed" if install_reason == "install_failed" else "blocked",
                    "reason": install_reason,
                    "exit_code": exit_code,
                    "current_version": current_version,
                    "target_version": decision.target_version,
                    "package_path": str(package_path),
                    "dry_run": bool(dry_run),
                    "pairing_state_intact": pairing_intact,
                    "install_command": install_cmd,
                    "install_stderr": install_stderr,
                    "error": install_stderr,
                }
            return {
                "ok": True,
                "status": "updated_in_place",
                "reason": "installed",
                "previous_version": current_version,
                "target_version": decision.target_version,
                "package_path": str(package_path),
                "preserved_config_path": str(getattr(self.pairing.storage, "file_path", "")),
                "dry_run": bool(dry_run),
                "pairing_state_intact": self._pairing_state_matches(state),
                "install_command": install_cmd,
            }

        dpkg_path = shutil.which("dpkg")
        if not dpkg_path:
            return {
                "ok": False,
                "status": "blocked",
                "reason": "dpkg_not_found",
                "current_version": current_version,
                "target_version": decision.target_version,
                "dry_run": bool(dry_run),
            }

        exit_code = os.spawnv(os.P_WAIT, dpkg_path, [dpkg_path, "-i", str(package_path)])
        if exit_code != 0:
            pairing_intact = self._pairing_state_matches(state)
            installed_pkg = self._installed_package_version()
            if package_version_covers_target(installed_pkg, decision.target_version):
                return {
                    "ok": True,
                    "status": "up_to_date",
                    "reason": "already_up_to_date",
                    "current_version": installed_pkg,
                    "target_version": decision.target_version,
                    "package_path": str(package_path),
                    "dry_run": bool(dry_run),
                    "pairing_state_intact": pairing_intact,
                    "would_upgrade": False,
                }
            return {
                "ok": False,
                "status": "failed",
                "reason": "dpkg_install_failed",
                "exit_code": int(exit_code),
                "current_version": current_version,
                "target_version": decision.target_version,
                "package_path": str(package_path),
                "dry_run": bool(dry_run),
                "pairing_state_intact": pairing_intact,
            }

        return {
            "ok": True,
            "status": "updated_in_place",
            "reason": "installed",
            "previous_version": current_version,
            "target_version": decision.target_version,
            "package_path": str(package_path),
            "preserved_config_path": str(getattr(self.pairing.storage, "file_path", "")),
            "dry_run": bool(dry_run),
            "pairing_state_intact": self._pairing_state_matches(state),
        }

    @staticmethod
    def _installed_package_version() -> str:
        proc = subprocess.run(
            ["dpkg-query", "-W", "-f=${Version}", "iuve-connect-desktop-agent"],
            check=False,
            capture_output=True,
            text=True,
        )
        if proc.returncode != 0:
            return ""
        return (proc.stdout or "").strip()

    def _upgrade_current_version(self) -> str:
        return resolve_upgrade_current_version(
            config_version=self.config.app_version,
            package_version=self._installed_package_version(),
            runtime_version=AGENT_MVP_VERSION,
        )

    def _install_deb_with_privilege(self, package_path: Path) -> dict[str, object]:
        """Install a downloaded .deb via pkexec or sudo dpkg (avoid apt repo side-effects)."""
        path = str(package_path)
        attempts: list[list[str]] = []
        pkexec = shutil.which("pkexec")
        dpkg = shutil.which("dpkg")
        sudo = shutil.which("sudo")
        if pkexec and dpkg:
            attempts.append([pkexec, dpkg, "-i", path])
        if sudo and dpkg:
            attempts.append([sudo, dpkg, "-i", path])

        if not attempts:
            return {
                "exit_code": 1,
                "reason": "privilege_installer_unavailable",
                "install_command": None,
                "stderr": "",
            }

        last_exit = 1
        last_cmd: list[str] | None = None
        last_stderr = ""
        for command in attempts:
            last_cmd = command
            try:
                completed = subprocess.run(
                    command,
                    check=False,
                    capture_output=True,
                    text=True,
                )
                last_exit = int(completed.returncode)
                last_stderr = str(completed.stderr or completed.stdout or "").strip()
            except OSError as exc:
                last_exit = 1
                last_stderr = str(exc)
                continue
            if last_exit == 0:
                return {
                    "exit_code": 0,
                    "reason": "installed",
                    "install_command": command,
                    "stderr": "",
                }
        return {
            "exit_code": last_exit,
            "reason": "install_failed" if last_exit != 0 else "installed",
            "install_command": last_cmd,
            "stderr": last_stderr,
        }

    def onboarding_wizard_model(self) -> dict[str, object]:
        paired = self.is_paired()
        return {
            "enabled": True,
            "steps": [
                {"id": 1, "title": "Welcome", "status": "done"},
                {"id": 2, "title": "Backend URL", "status": "done"},
                {"id": 3, "title": "Pair Token", "status": "done" if paired else "pending"},
                {"id": 4, "title": "Trust Confirmation", "status": "done" if paired else "pending"},
                {"id": 5, "title": "Passive Layer Explanation", "status": "done"},
                {"id": 6, "title": "Connected", "status": "done" if paired else "pending"},
            ],
            "active_controlled_indicator": "Allowlisted command channel only after explicit consent and pairing.",
        }

    def recovery_guidance_model(self) -> dict[str, object]:
        canonical = self._canonical_status()
        error_text = str(canonical.get("last_error") or "").strip().lower()
        connectivity = str(canonical.get("runtime_state") or "Offline")
        return {
            "enabled": True,
            "status": connectivity,
            "scenarios": {
                "corrupted_state_recovery": [
                    "Stop desktop service",
                    "Remove corrupted state file",
                    "Run pair command with fresh token",
                ],
                "revoked_token_recovery": [
                    "Generate new pair token in IuVe Connect",
                    "Run pair command again",
                    "Confirm device trust returns to trusted",
                ],
                "reconnect_guidance": [
                    "Check network reachability",
                    "Restart desktop service",
                    "Verify heartbeat in status output",
                ],
            },
            "suggested_action": self._recovery_action_for_error(error_text),
            "active_controlled_candidate": True,
        }

    def notifications_model(self) -> dict[str, object]:
        return {
            "enabled": True,
            "events": [
                "paired_successfully",
                "connection_restored",
                "revoked",
                "degraded_heartbeat",
            ],
            "delivery": "local_desktop_notification_placeholder",
            "active_controlled_candidate": True,
        }

    def status_details(self) -> dict[str, object]:
        canonical = self._canonical_status()
        runtime_state = str(canonical.get("runtime_state") or "Offline")
        trust_state = str(canonical.get("trust_state") or "Unpaired")
        device_state = str(canonical.get("device_state") or "Unknown")
        heartbeat_age_sec = canonical.get("heartbeat_age_sec")
        return {
            "status": runtime_state,
            "runtime_state": runtime_state,
            "connectivity": runtime_state,
            "paired": bool(canonical.get("paired", False)),
            "trust_state": trust_state,
            "device_state": device_state,
            "failure_count": int(canonical.get("failure_count") or 0),
            "last_error": str(canonical.get("last_error") or ""),
            "last_heartbeat_at": str(canonical.get("last_heartbeat_at") or ""),
            "heartbeat_age_sec": heartbeat_age_sec,
            "pairing_timestamp": self._pairing_timestamp_iso(),
            "trust_badge": trust_state,
            "reconnect_pulse": "active" if runtime_state == "Degraded" else "idle",
            "storage_path": str(getattr(self.pairing.storage, "file_path", "memory")),
            "backend_base_url": self.config.backend_base_url,
            "app_version": self.config.app_version,
            "update_channel": self.update_channel,
            "tray_ui_enabled": self.tray_ui_enabled,
            "settings_ui_enabled": self.settings_ui_enabled,
            "logs_path": str(self.event_logger.log_path) if self.event_logger else "",
            "error": "",
            "status_source_version": canonical.get("status_source_version"),
            "computed_at": canonical.get("computed_at"),
            "active_controlled_candidate": True,
        }

    def _connectivity_state(self) -> str:
        snapshot = self._runtime.snapshot()
        status = str(snapshot.status or "").strip().lower()
        error_text = str(snapshot.last_error or "").strip().lower()
        if "revoked" in error_text:
            return "Revoked"
        if not self.is_paired():
            return "Pending Trust"
        if status == "connected":
            return "Connected"
        if status == "reconnecting":
            return "Reconnecting"
        if status == "offline":
            return "Offline"
        if status == "error":
            return "Reconnecting"
        return "Offline"

    def _pairing_timestamp_iso(self) -> str:
        file_path = getattr(self.pairing.storage, "file_path", None)
        if file_path is None:
            return ""
        path = Path(file_path)
        if not path.exists():
            return ""
        ts = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
        return ts.isoformat()

    def _trust_badge(self, connectivity: str) -> str:
        badges = {
            "Connected": "Trusted",
            "Reconnecting": "Recovery",
            "Offline": "Offline",
            "Revoked": "Revoked",
            "Pending Trust": "Pending",
        }
        return badges.get(connectivity, "Unknown")

    def _recovery_action_for_error(self, error_text: str) -> str:
        lowered = str(error_text or "").strip().lower()
        if "revoked" in lowered:
            return "safe_repair_required"
        if "invalid_device_auth" in lowered:
            return "re_pair_required"
        if "storage_error" in lowered or "json" in lowered:
            return "state_recovery_required"
        if "backend" in lowered or "unreachable" in lowered:
            return "network_or_backend_reconnect"
        return "monitor"

    def _build_heartbeat_metadata(self) -> dict[str, str]:
        from ..core.metadata import collect_safe_metadata

        metadata = collect_safe_metadata(
            app_version=self.config.app_version,
            device_type="desktop",
        )
        store = self._capability_store.action_session_store
        count = store.active_session_count()
        metadata["active_sessions_count"] = str(count)
        metadata["active_sessions_digest"] = store.active_sessions_digest()
        profile = load_profile()
        if is_profile_stale(profile):
            self._maybe_schedule_device_capability_scan(reason="heartbeat_stale")
        elif profile is not None:
            from .active.device_capability_scanner import refresh_profile_light, save_profile

            profile = refresh_profile_light(profile)
            save_profile(profile)
        if self.is_paired() and is_policy_trusted(self._resolve_trust_state()):
            self._device_scan_scheduler.run_task(
                "diagnostics_snapshot",
                force=False,
                reason="heartbeat_diagnostics_snapshot",
            )
        metadata.update(to_heartbeat_summary(profile))
        try:
            from .active.agent_activity_state import read_agent_activity_heartbeat_fields

            metadata.update(read_agent_activity_heartbeat_fields())
        except Exception:
            pass
        try:
            from .active.capability_resolver import build_capability_snapshot
            from .active.orchestra_runtime_sync import fetch_capability_graph, post_capability_snapshot
            from iuve_connect_agents.shared.ui_capability_grants import UI_CAPABILITY_KEYS

            manager = getattr(self._capability_store, "manager", None)
            grants: dict[str, bool] = {}
            if manager is not None and hasattr(manager, "is_enabled"):
                grants = {key: bool(manager.is_enabled(key)) for key in UI_CAPABILITY_KEYS}
            snapshot = build_capability_snapshot(
                trust_state=self._resolve_trust_state(),
                grants=grants,
                app_version=self.config.app_version,
            )
            post_capability_snapshot(snapshot)
            fetch_capability_graph()
        except Exception:
            pass
        return metadata

    def _on_heartbeat_response(self, response: dict[str, object]) -> None:
        if not isinstance(response, dict):
            return
        if response.get("suggested_actions") is not None or response.get("active_session_sync") is not None:
            self._heartbeat_extras_store.save_response(response)

    def _on_runtime_status(self, value: str) -> None:
        self._runtime_evidence_store.save_snapshot(self._runtime.snapshot())
        self._log_event("runtime_status", status=value)

    def _canonical_status(self, *, force_refresh: bool = False) -> dict[str, object]:
        now = time.monotonic()
        cached = getattr(self, "_canonical_status_cache", None)
        cached_at = float(getattr(self, "_canonical_status_cached_at", 0.0) or 0.0)
        if not force_refresh and isinstance(cached, dict) and (now - cached_at) < 5.0:
            return dict(cached)

        backend_fields = self._backend_status_fields()
        resolved = resolve_canonical_status(
            runtime_snapshot=self._runtime.snapshot(),
            runtime_evidence=self._runtime_evidence_store.load(),
            legacy_fields=self._legacy_status_fields(),
            backend_fields=backend_fields,
            paired=self.is_paired(),
            backend_url=self.config.backend_base_url,
            app_version=self.config.app_version,
            update_channel=self.update_channel,
            logs_path=str(self.event_logger.log_path) if self.event_logger else "",
            storage_path=str(getattr(self.pairing.storage, "file_path", "memory")),
        ).to_dict()
        self._canonical_status_cache = dict(resolved)
        self._canonical_status_cached_at = now
        return dict(resolved)

    def _device_auth_token(self) -> str:
        state = self.pairing.storage.load_state()
        if state is None:
            return ""
        return str(getattr(state, "device_auth_token", "") or "").strip()

    def _cloud_fetch_bounded(self, url: str) -> dict[str, object]:
        auth_token = self._device_auth_token()
        if not auth_token:
            return {"ok": False, "error": "device_not_paired"}
        try:
            payload = self.pairing.api_client.request_web_fetch_bounded(auth_token=auth_token, url=url)
            evidence = dict(payload.get("evidence") or {})
            return {
                "ok": bool(payload.get("ok")),
                "url": str(evidence.get("url") or url),
                "text": str(evidence.get("text") or evidence.get("text_preview") or ""),
                "text_length": int(evidence.get("text_length") or 0),
                "content_hash": str(evidence.get("content_hash") or ""),
                "truncated": bool(evidence.get("truncated", False)),
                "error": str(payload.get("error") or ""),
            }
        except Exception as exc:
            return {"ok": False, "error": str(exc) or "cloud_fetch_failed"}

    def _cloud_summarize_content(self, content: str, goal_summary: str) -> dict[str, object]:
        auth_token = self._device_auth_token()
        if not auth_token:
            return {"ok": False, "error": "device_not_paired"}
        try:
            payload = self.pairing.api_client.request_summarize_content(
                auth_token=auth_token,
                content=content,
                goal_summary=goal_summary,
            )
            evidence = dict(payload.get("evidence") or {})
            summary = str(evidence.get("summary") or evidence.get("summary_preview") or "")
            return {
                "ok": bool(payload.get("ok")),
                "summary": summary,
                "mode": str(evidence.get("mode") or "cloud"),
                "error": str(payload.get("error") or ""),
            }
        except Exception as exc:
            return {"ok": False, "error": str(exc) or "cloud_summarize_failed"}

    def _cloud_generate_content(self, content_type: str, topic: str, goal_summary: str) -> dict[str, object]:
        auth_token = self._device_auth_token()
        if not auth_token:
            return {"ok": False, "error": "device_not_paired"}
        try:
            payload = self.pairing.api_client.request_generate_content(
                auth_token=auth_token,
                content_type=content_type,
                topic=topic,
                goal_summary=goal_summary,
            )
            evidence = dict(payload.get("evidence") or {})
            content = str(
                evidence.get("content")
                or evidence.get("generated_content")
                or evidence.get("generated_text")
                or evidence.get("text")
                or ""
            )
            return {
                "ok": bool(payload.get("ok")),
                "content": content,
                "generated_content": content,
                "mode": str(evidence.get("mode") or "cloud"),
                "error": str(payload.get("error") or ""),
            }
        except Exception as exc:
            return {"ok": False, "error": str(exc) or "cloud_generate_failed"}

    def _legacy_status_fields(self) -> dict[str, object]:
        # Legacy compatibility bucket (lowest priority evidence).
        candidate_paths: list[Path] = [
            self._runtime_evidence_path().with_name("desktop-status.json"),
            self._runtime_evidence_path().with_name("runtime-status.json"),
        ]
        for path in candidate_paths:
            if not path.exists():
                continue
            try:
                data = json.loads(path.read_text(encoding="utf-8"))
                if isinstance(data, dict):
                    return data
            except Exception:
                continue
        return {}

    def _backend_status_fields(self) -> dict[str, object]:
        now = time.monotonic()
        cached = getattr(self, "_backend_status_cache", None)
        cached_at = float(getattr(self, "_backend_status_cached_at", 0.0) or 0.0)
        if isinstance(cached, dict) and (now - cached_at) < 30.0:
            return dict(cached)

        state = self.pairing.storage.load_state()
        if state is None:
            self._backend_status_cache = {}
            self._backend_status_cached_at = now
            return {}
        auth_token = str(getattr(state, "device_auth_token", "") or "").strip()
        if not auth_token:
            self._backend_status_cache = {}
            self._backend_status_cached_at = now
            return {}
        try:
            profile = self.pairing.api_client.get_agent_profile(auth_token=auth_token)
        except Exception:
            self._backend_status_cache = {}
            self._backend_status_cached_at = now
            return {}
        if not isinstance(profile, dict):
            self._backend_status_cache = {}
            self._backend_status_cached_at = now
            return {}
        payload = {
            "status": str(profile.get("status") or "").strip(),
            "trust_state": str(profile.get("trust_state") or "").strip(),
            "last_heartbeat_at": str(profile.get("last_seen_at") or profile.get("last_heartbeat_at") or "").strip(),
        }
        self._backend_status_cache = dict(payload)
        self._backend_status_cached_at = now
        return dict(payload)

    def _resolve_trust_state(self) -> str:
        """Policy trust — same canonical source as console UI (not cloud-only)."""
        canonical = self._canonical_status()
        return normalize_trust_for_policy(str(canonical.get("trust_state") or ""))

    def register_command_confirmation_callback(
        self,
        callback: ConfirmationCallback | None,
    ) -> None:
        """Override confirmation handler (tests); default is ConfirmationBridge."""
        if callback is None:
            self._active_pipeline.confirmation_callback = self._confirmation_bridge.request
        else:
            self._active_pipeline.confirmation_callback = callback

    def confirmation_bridge(self) -> ConfirmationBridge:
        return self._confirmation_bridge

    def read_command_audit_recent(self, n: int = 50) -> list[dict[str, object]]:
        return self._command_audit_logger.read_recent(n)

    def _runtime_evidence_path(self) -> Path:
        storage_file = getattr(self.pairing.storage, "file_path", None)
        if storage_file is None:
            return Path.home() / ".config" / "iuve-connect-agent" / "desktop-runtime-evidence.json"
        return Path(storage_file).expanduser().resolve().with_name("desktop-runtime-evidence.json")

    def _log_event(self, event: str, **fields: object) -> None:
        if self.event_logger is not None:
            self.event_logger.log(event, fields)

    @staticmethod
    def _sha256_file(path: Path) -> str:
        h = hashlib.sha256()
        with open(path, "rb") as handle:
            for chunk in iter(lambda: handle.read(1024 * 1024), b""):
                h.update(chunk)
        return h.hexdigest()

    def _pairing_state_matches(self, previous_state: object) -> bool:
        if previous_state is None:
            return False
        current = self.pairing.storage.load_state()
        if current is None:
            return False
        return (
            str(getattr(current, "device_id", "")) == str(getattr(previous_state, "device_id", ""))
            and str(getattr(current, "device_auth_token", "")) == str(getattr(previous_state, "device_auth_token", ""))
            and str(getattr(current, "heartbeat_secret", "")) == str(getattr(previous_state, "heartbeat_secret", ""))
        )

    @classmethod
    def create_default(
        cls,
        config: AgentConfig,
        storage: SecureStorage | None = None,
        *,
        update_channel: str | None = None,
        tray_ui_enabled: bool = True,
        settings_ui_enabled: bool = True,
        log_path: str | Path | None = None,
        schedule_capability_scan: bool = True,
    ) -> "DesktopAgentMVP":
        secure_storage = storage or JsonFileSecureStorage(_default_desktop_state_path())
        pairing = PairingCoordinator(
            api_client=PassiveApiClient(base_url=config.backend_base_url),
            storage=secure_storage,
        )
        reconnect = ReconnectPolicy(
            initial_sec=config.reconnect_initial_sec,
            max_sec=config.reconnect_max_sec,
            multiplier=config.reconnect_multiplier,
        )
        logger = DesktopAgentEventLogger(log_path or _default_desktop_log_path())
        agent = cls(
            config=config,
            pairing=pairing,
            reconnect=reconnect,
            tray_ui_enabled=tray_ui_enabled,
            settings_ui_enabled=settings_ui_enabled,
            update_channel=update_channel or config.update_channel,
            event_logger=logger,
        )
        if schedule_capability_scan:
            agent._schedule_startup_capability_scan()
        return agent

    @classmethod
    def from_config_file(
        cls,
        config_path: str | Path,
        *,
        schedule_capability_scan: bool = True,
    ) -> "DesktopAgentMVP":
        path = Path(config_path)
        data = json.loads(path.read_text(encoding="utf-8"))
        config_block = dict(data.get("agent", data))
        config_block.setdefault("device_type", "desktop")
        app_version = str(config_block.get("app_version") or "").strip()
        if app_version in LEGACY_AGENT_VERSION_ALIASES:
            config_block["app_version"] = AGENT_MVP_VERSION
            if isinstance(data.get("agent"), dict):
                data["agent"] = dict(data.get("agent") or {})
                data["agent"]["app_version"] = AGENT_MVP_VERSION
            else:
                data["app_version"] = AGENT_MVP_VERSION
            path.write_text(json.dumps(data, ensure_ascii=True, indent=2, sort_keys=True), encoding="utf-8")
        config = AgentConfig.from_dict(config_block)

        if "voice_presence" not in data:
            data["voice_presence"] = {
                "enabled": False,
                "wake_word_enabled": False,
                "push_to_talk_key": "Ctrl+Shift+Space",
                "locale": "",
                "input_modes": ["push_to_talk", "manual_mic"],
            }
            path.write_text(json.dumps(data, ensure_ascii=True, indent=2, sort_keys=True), encoding="utf-8")

        storage_path = data.get("storage_path") or _default_desktop_state_path()
        log_path = data.get("log_path") or _default_desktop_log_path()
        tray_ui_enabled = bool(data.get("tray_ui_enabled", True))
        settings_ui_enabled = bool(data.get("settings_ui_enabled", True))
        update_channel = str(data.get("update_channel", config.update_channel))

        return cls.create_default(
            config,
            storage=JsonFileSecureStorage(storage_path),
            update_channel=update_channel,
            tray_ui_enabled=tray_ui_enabled,
            settings_ui_enabled=settings_ui_enabled,
            log_path=log_path,
            schedule_capability_scan=schedule_capability_scan,
        )


def _default_desktop_state_path() -> Path:
    return Path.home() / ".config" / "iuve-connect-agent" / "desktop-state.json"


def _default_desktop_log_path() -> Path:
    return Path.home() / ".config" / "iuve-connect-agent" / "desktop-agent.log"
