from __future__ import annotations

from .api_client import BackendAuthRequiredError, PassiveApiClient
from .heartbeat import build_heartbeat_payload
from .metadata import ALLOWED_METADATA_FIELDS, collect_safe_metadata
from .models import AgentState, PairingOutcome
from .profile_avatar import avatar_payload_changed, build_avatar_upload_payload
from .signer import sign_payload
from .storage import SecureStorage
from .version import HEARTBEAT_PROTOCOL_VERSION


class PairingCoordinator:
    def __init__(self, *, api_client: PassiveApiClient, storage: SecureStorage) -> None:
        self.api_client = api_client
        self.storage = storage
        self._last_avatar_sha256: str | None = None

    def _attach_profile_avatar_metadata(self, metadata: dict[str, str]) -> dict[str, str]:
        payload = build_avatar_upload_payload()
        if payload is None:
            return metadata
        merged = dict(metadata)
        merged["avatar_base64"] = str(payload.get("avatar_base64") or "")
        merged["avatar_sha256"] = str(payload.get("avatar_sha256") or "")
        self._last_avatar_sha256 = merged["avatar_sha256"] or None
        return merged

    def pair_device(
        self,
        *,
        pair_token: str,
        app_version: str,
        device_type: str,
        register_metadata: dict[str, str] | None = None,
    ) -> PairingOutcome:
        cleaned_token = pair_token.strip()
        if not cleaned_token:
            return PairingOutcome(ok=False, status="invalid_token", detail="Pair token is empty")

        try:
            auth = self.api_client.authenticate_pair_token(
                cleaned_token,
                device_name="IuVe Agent",
                device_type=device_type,
                platform="unknown",
                device_identifier="",
            )
            raw_metadata = register_metadata or collect_safe_metadata(app_version=app_version, device_type=device_type)
            raw_metadata = self._attach_profile_avatar_metadata(raw_metadata)
            metadata = {
                key: str(raw_metadata.get(key, ""))
                for key in ALLOWED_METADATA_FIELDS
            }
            registration_body = dict(metadata)
            avatar_base64 = str(raw_metadata.get("avatar_base64") or "").strip()
            if avatar_base64:
                registration_body["avatar_base64"] = avatar_base64
            registration = self.api_client.register_device(
                auth_token=auth.get("bootstrap_access_token", ""),
                pair_token_id=auth.get("pair_token_id", ""),
                metadata=registration_body,
            )
        except BackendAuthRequiredError:
            self.storage.clear_pair_token()
            return PairingOutcome(
                ok=False,
                status="backend_auth_gap",
                detail="Backend requires user/API auth for bootstrap endpoints. No bypass applied.",
            )
        finally:
            # Pair token is one-time and must never be persisted.
            self.storage.clear_pair_token()

        if not registration.get("device_id") or not registration.get("device_auth_token") or not registration.get("heartbeat_secret"):
            return PairingOutcome(
                ok=False,
                status="pairing_contract_error",
                detail="Backend pairing contract did not return required device credentials",
            )

        state = AgentState(
            device_id=registration["device_id"],
            device_auth_token=registration["device_auth_token"],
            heartbeat_secret=registration["heartbeat_secret"],
        )
        self.storage.save_state(state)
        return PairingOutcome(
            ok=True,
            status="paired",
            detail="Device paired in passive-only mode",
            device_id=state.device_id,
        )

    def build_signed_heartbeat(
        self,
        *,
        nonce: str,
        app_version: str,
        device_type: str,
        heartbeat_metadata: dict[str, str] | None = None,
    ) -> dict[str, object]:
        state = self.storage.load_state()
        if state is None:
            raise RuntimeError("No paired state found")

        raw_metadata = heartbeat_metadata or collect_safe_metadata(app_version=app_version, device_type=device_type)
        # Keep heartbeat payload bounded and passive-safe.
        heartbeat_metadata_clean = {
            key: value
            for key, value in raw_metadata.items()
            if key
            in {
                "os_name",
                "os_version",
                "app_version",
                "hostname",
                "timezone",
                "platform",
                "hostname_hash",
                "server_role_label",
                "uptime_class",
                "active_sessions_digest",
                "active_sessions_count",
                "agent_activity",
                "activity_label",
            }
        }

        payload = build_heartbeat_payload(
            device_id=state.device_id,
            metadata=heartbeat_metadata_clean,
            protocol_version=HEARTBEAT_PROTOCOL_VERSION,
            nonce=nonce,
        )
        signature = sign_payload(state.heartbeat_secret, payload)
        return {
            "payload": payload,
            "signature": signature,
        }

    def send_heartbeat_once(
        self,
        *,
        nonce: str,
        app_version: str,
        device_type: str,
        heartbeat_metadata: dict[str, str] | None = None,
    ) -> dict[str, object]:
        state = self.storage.load_state()
        if state is None:
            raise RuntimeError("No paired state found")

        heartbeat = self.build_signed_heartbeat(
            nonce=nonce,
            app_version=app_version,
            device_type=device_type,
            heartbeat_metadata=heartbeat_metadata,
        )
        try:
            return self.api_client.submit_heartbeat(
                auth_token=state.device_auth_token,
                signed_payload=heartbeat,
            )
        except RuntimeError as exc:
            # Compatibility fallback for older backends that only accept base passive metadata keys.
            if "invalid_metadata_key" not in str(exc):
                raise
            fallback_heartbeat = self.build_signed_heartbeat(
                nonce=nonce,
                app_version=app_version,
                device_type=device_type,
                heartbeat_metadata=collect_safe_metadata(app_version=app_version, device_type=device_type),
            )
            return self.api_client.submit_heartbeat(
                auth_token=state.device_auth_token,
                signed_payload=fallback_heartbeat,
            )

    def maybe_sync_device_avatar(self) -> bool:
        """Upload local profile avatar when it changes after pairing/reconnect."""
        state = self.storage.load_state()
        if state is None:
            return False
        if not avatar_payload_changed(self._last_avatar_sha256):
            return False
        payload = build_avatar_upload_payload()
        if payload is None:
            return False
        try:
            self.api_client.upload_device_avatar(
                auth_token=state.device_auth_token,
                avatar_base64=str(payload.get("avatar_base64") or ""),
                avatar_sha256=str(payload.get("avatar_sha256") or "") or None,
            )
            self._last_avatar_sha256 = str(payload.get("avatar_sha256") or "") or None
            return True
        except Exception:
            return False

    def poll_next_command(self) -> dict[str, object]:
        state = self.storage.load_state()
        if state is None:
            raise RuntimeError("No paired state found")
        return self.api_client.fetch_next_command(auth_token=state.device_auth_token)

    def submit_command_result(
        self,
        *,
        command_id: str,
        success: bool,
        result: dict[str, object] | None = None,
        error_message: str | None = None,
    ) -> dict[str, object]:
        state = self.storage.load_state()
        if state is None:
            raise RuntimeError("No paired state found")
        return self.api_client.submit_command_result(
            auth_token=state.device_auth_token,
            command_id=command_id,
            success=success,
            result=result,
            error_message=error_message,
        )


# Backward-compatible alias for modules that still import PairingManager (DEF-001).
PairingManager = PairingCoordinator
