"""Graceful departure protocol for leaving a cluster.

Steps:
  1. Stop sync scheduler
  2. Final delta sync (push pending changes)
  3. Snapshot: copy CRDB data → SQLite (if applicable)
  4. Deregister from Raft cluster
  5. Revoke node certificate
  6. Transition to Solo state
"""
from __future__ import annotations

import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import httpx

from greymatter_core.http_client import AsyncRetryClient, RetryConfig

logger = logging.getLogger(__name__)

_HTTP_TIMEOUT = 30.0


@dataclass
class LeaveResult:
    """Result of the leave protocol."""
    steps_completed: list[str] = field(default_factory=list)
    rows_synced: int = 0
    error: str = ""

    @property
    def success(self) -> bool:
        return not self.error


class LeaveProtocol:
    """Orchestrates graceful departure from a cluster."""

    def __init__(
        self,
        local_db: Any,
        hub_url: str = "",
        node_id: str = "",
        state_machine: Any = None,
        scheduler: Any = None,
        hub_db: Any = None,
        cert_dir: str | Path | None = None,
    ):
        self.local_db = local_db
        self.hub_url = hub_url
        self.node_id = node_id
        self.state_machine = state_machine
        self.scheduler = scheduler
        self.hub_db = hub_db
        self.cert_dir = Path(cert_dir or os.environ.get(
            "GM_CERT_DIR",
            str(Path.home() / ".greymatter" / "certs"),
        ))
        self.result = LeaveResult()

    async def execute(self) -> LeaveResult:
        """Run the leave protocol. Returns LeaveResult."""
        steps = [
            ("stop_sync", self._stop_sync),
            ("final_sync", self._final_sync),
            ("snapshot", self._snapshot),
            ("deregister", self._deregister),
            ("revoke_cert", self._revoke_cert),
            ("transition_solo", self._transition_solo),
        ]

        for name, handler in steps:
            try:
                logger.info("Leave step: %s", name)
                await handler()
                self.result.steps_completed.append(name)
            except Exception as e:
                self.result.error = f"{name}: {e}"
                logger.error("Leave failed at step %s: %s", name, e)
                # Still try to transition to Solo on failure
                if name != "transition_solo":
                    try:
                        await self._transition_solo()
                        self.result.steps_completed.append("transition_solo")
                    except Exception:
                        pass
                break

        return self.result

    async def _stop_sync(self) -> None:
        """Stop the background sync scheduler."""
        if self.scheduler and self.scheduler.is_running:
            await self.scheduler.stop()

    async def _final_sync(self) -> None:
        """Push any pending changes before leaving.

        Runs a delta sync from local → hub to ensure the hub has all
        our latest changes before we depart.
        """
        if not self.hub_db:
            logger.info("Final sync: no hub_db provided, skipping")
            return

        from greymatter_core.sync.delta import delta_sync
        result = delta_sync(self.local_db, self.hub_db)
        self.result.rows_synced += result.rows_pushed
        logger.info(
            "Final sync pushed %d rows (%d conflicts resolved)",
            result.rows_pushed,
            result.conflicts_resolved,
        )

    async def _snapshot(self) -> None:
        """Snapshot remote data to local SQLite.

        Runs a full sync from hub → local so the departing node retains
        a complete copy of cluster data for offline use.
        """
        if not self.hub_db:
            logger.info("Snapshot: no hub_db provided, skipping")
            return

        from greymatter_core.sync.full import full_sync
        result = full_sync(self.hub_db, self.local_db)
        self.result.rows_synced += result.rows_pulled
        logger.info("Snapshot pulled %d rows from hub", result.rows_pulled)

    async def _deregister(self) -> None:
        """Deregister from Overseer Raft cluster via hub API."""
        if not self.hub_url:
            logger.info("Deregister: no hub_url, skipping")
            return

        async with AsyncRetryClient(timeout=_HTTP_TIMEOUT) as client:
            resp = await client.post(
                f"{self.hub_url}/cluster/deregister",
                json={"node_id": self.node_id},
            )
            if resp.status_code != 200:
                raise ConnectionError(
                    f"Deregister failed: {resp.status_code} {resp.text}"
                )
        logger.info("Deregistered node %s from cluster", self.node_id)

    async def _revoke_cert(self) -> None:
        """Revoke this node's TLS certificate.

        Reads the local node cert and asks the hub to revoke it,
        or revokes directly if CA key is available locally.
        """
        node_cert_path = self.cert_dir / "node.crt"
        ca_cert_path = self.cert_dir / "ca.crt"

        if not node_cert_path.exists():
            logger.info("Revoke cert: no node cert found, skipping")
            return

        node_cert_pem = node_cert_path.read_bytes()
        ca_cert_pem = ca_cert_path.read_bytes() if ca_cert_path.exists() else b""

        # Try hub-side revocation via API
        if self.hub_url:
            async with AsyncRetryClient(timeout=_HTTP_TIMEOUT) as client:
                resp = await client.post(
                    f"{self.hub_url}/cluster/revoke-cert",
                    json={
                        "node_id": self.node_id,
                        "node_cert": node_cert_pem.decode(),
                    },
                )
                if resp.status_code == 200:
                    logger.info("Certificate revoked via hub for node %s", self.node_id)
                    return
                logger.warning(
                    "Hub revocation returned %d, falling back to local",
                    resp.status_code,
                )

        # Fallback: local revocation if CA key is available
        ca_key_path = self.cert_dir / "ca.key"
        if ca_key_path.exists() and ca_cert_pem:
            from greymatter_core.cluster.certs import revoke_cert
            revoke_cert(
                node_cert_pem,
                ca_cert_pem,
                ca_key_path.read_bytes(),
            )
            logger.info("Certificate revoked locally for node %s", self.node_id)
        else:
            logger.warning("Cannot revoke cert: no CA key available")

    async def _transition_solo(self) -> None:
        """Transition state machine back to Solo."""
        if self.state_machine:
            from greymatter_core.cluster.state import NodeState
            if self.state_machine.state == NodeState.LEAVING:
                self.state_machine.transition(NodeState.SOLO)
