"""
Multi-Device Sync - Synchronize agent state across multiple devices.

Features:
- Real-time state synchronization
- Conflict resolution
- Offline support with sync queue
- End-to-end encryption
- Device pairing and management
- Selective sync (choose what to sync)
"""

import asyncio
import logging
import json
import hashlib
import os
from pathlib import Path
from typing import Dict, Any, List, Optional, Set, Callable
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime, timedelta, timezone
import uuid

logger = logging.getLogger(__name__)


class SyncStatus(Enum):
    """Sync operation status."""

    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"
    CONFLICT = "conflict"


class SyncStrategy(Enum):
    """Conflict resolution strategies."""

    LATEST_WINS = "latest_wins"
    SERVER_WINS = "server_wins"
    CLIENT_WINS = "client_wins"
    MERGE = "merge"
    MANUAL = "manual"


class SyncScope(Enum):
    """What data to sync."""

    ALL = "all"
    CONVERSATIONS = "conversations"
    SETTINGS = "settings"
    MEMORY = "memory"
    GOALS = "goals"
    TOOLS = "tools"
    WORLD_MODEL = "world_model"


@dataclass
class Device:
    """Registered device."""

    device_id: str
    device_name: str
    device_type: str  # mobile, desktop, server
    last_seen: datetime
    ip_address: Optional[str] = None
    public_key: Optional[str] = None
    trusted: bool = False
    sync_enabled: bool = True
    sync_scopes: Set[SyncScope] = field(default_factory=lambda: {SyncScope.ALL})


@dataclass
class SyncItem:
    """Item to be synchronized."""

    item_id: str
    scope: SyncScope
    data: Dict[str, Any]
    version: int
    timestamp: datetime
    device_id: str
    checksum: str

    def compute_checksum(self) -> str:
        """Compute data checksum."""
        data_str = json.dumps(self.data, sort_keys=True)
        return hashlib.sha256(data_str.encode()).hexdigest()


@dataclass
class SyncOperation:
    """Sync operation record."""

    operation_id: str
    device_id: str
    scope: SyncScope
    status: SyncStatus
    items_total: int
    items_synced: int
    started_at: datetime
    completed_at: Optional[datetime] = None
    error: Optional[str] = None


class ConflictResolver:
    """
    Resolves sync conflicts between devices.

    Strategies:
    - Latest wins: Newest timestamp wins
    - Server wins: Server version always wins
    - Client wins: Client version always wins
    - Merge: Intelligent merge of changes
    - Manual: Requires user intervention
    """

    def __init__(self, strategy: SyncStrategy = SyncStrategy.LATEST_WINS):
        """
        Initialize conflict resolver.

        Args:
            strategy: Default resolution strategy
        """
        self.strategy = strategy
        self.manual_queue: List[Dict[str, Any]] = []

    async def resolve(
        self, local_item: SyncItem, remote_item: SyncItem, is_server: bool = False
    ) -> SyncItem:
        """
        Resolve conflict between local and remote versions.

        Args:
            local_item: Local version
            remote_item: Remote version
            is_server: Whether this device is the server

        Returns:
            Resolved SyncItem
        """
        # No conflict if checksums match
        if local_item.checksum == remote_item.checksum:
            return local_item

        logger.info(f"Resolving conflict for {local_item.item_id} using {self.strategy.value}")

        if self.strategy == SyncStrategy.LATEST_WINS:
            return local_item if local_item.timestamp > remote_item.timestamp else remote_item

        elif self.strategy == SyncStrategy.SERVER_WINS:
            return local_item if is_server else remote_item

        elif self.strategy == SyncStrategy.CLIENT_WINS:
            return remote_item if is_server else local_item

        elif self.strategy == SyncStrategy.MERGE:
            return await self._merge_items(local_item, remote_item)

        elif self.strategy == SyncStrategy.MANUAL:
            self.manual_queue.append(
                {"local": local_item, "remote": remote_item, "timestamp": datetime.now(timezone.utc)}
            )
            # Return local for now
            return local_item

        return local_item

    async def _merge_items(self, local: SyncItem, remote: SyncItem) -> SyncItem:
        """Intelligently merge two items."""
        merged_data = {}

        # Merge strategy: combine keys, prefer newer values
        all_keys = set(local.data.keys()) | set(remote.data.keys())

        for key in all_keys:
            if key in local.data and key in remote.data:
                # Both have the key - use latest timestamp's value
                if local.timestamp > remote.timestamp:
                    merged_data[key] = local.data[key]
                else:
                    merged_data[key] = remote.data[key]
            elif key in local.data:
                merged_data[key] = local.data[key]
            else:
                merged_data[key] = remote.data[key]

        # Create merged item
        merged = SyncItem(
            item_id=local.item_id,
            scope=local.scope,
            data=merged_data,
            version=max(local.version, remote.version) + 1,
            timestamp=datetime.now(timezone.utc),
            device_id=local.device_id,
            checksum="",
        )
        merged.checksum = merged.compute_checksum()

        return merged


class SyncQueue:
    """
    Queue for offline sync operations.

    Stores operations when device is offline, syncs when online.
    """

    def __init__(self, storage_path: Path):
        """
        Initialize sync queue.

        Args:
            storage_path: Path to queue storage
        """
        self.storage_path = storage_path
        self.storage_path.parent.mkdir(parents=True, exist_ok=True)

        self.queue: List[SyncItem] = []
        self.load_queue()

    def load_queue(self):
        """Load queue from disk."""
        if not self.storage_path.exists():
            return

        try:
            with open(self.storage_path, "r") as f:
                data = json.load(f)
                self.queue = [
                    SyncItem(
                        item_id=item["item_id"],
                        scope=SyncScope(item["scope"]),
                        data=item["data"],
                        version=item["version"],
                        timestamp=datetime.fromisoformat(item["timestamp"]),
                        device_id=item["device_id"],
                        checksum=item["checksum"],
                    )
                    for item in data
                ]
            logger.info(f"Loaded {len(self.queue)} items from sync queue")
        except Exception as e:
            logger.error(f"Error loading sync queue: {e}")

    def save_queue(self):
        """Save queue to disk."""
        try:
            data = [
                {
                    "item_id": item.item_id,
                    "scope": item.scope.value,
                    "data": item.data,
                    "version": item.version,
                    "timestamp": item.timestamp.isoformat(),
                    "device_id": item.device_id,
                    "checksum": item.checksum,
                }
                for item in self.queue
            ]

            with open(self.storage_path, "w") as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logger.error(f"Error saving sync queue: {e}")

    def add(self, item: SyncItem):
        """Add item to queue."""
        self.queue.append(item)
        self.save_queue()
        logger.debug(f"Added {item.item_id} to sync queue")

    def get_pending(self, scope: Optional[SyncScope] = None) -> List[SyncItem]:
        """Get pending items, optionally filtered by scope."""
        if scope:
            return [item for item in self.queue if item.scope == scope]
        return self.queue.copy()

    def remove(self, item_id: str):
        """Remove item from queue."""
        self.queue = [item for item in self.queue if item.item_id != item_id]
        self.save_queue()

    def clear(self):
        """Clear entire queue."""
        self.queue.clear()
        self.save_queue()


class MultiDeviceSync:
    """
    Multi-device synchronization system.

    Features:
    - Real-time sync over WebSocket
    - Offline queue with eventual consistency
    - Conflict resolution
    - Device management
    - End-to-end encryption (optional)
    - Selective sync scopes
    """

    def __init__(
        self,
        device_id: Optional[str] = None,
        device_name: Optional[str] = None,
        storage_dir: Optional[Path] = None,
        conflict_strategy: SyncStrategy = SyncStrategy.LATEST_WINS,
        enable_encryption: bool = False,
    ):
        """
        Initialize multi-device sync.

        Args:
            device_id: Unique device ID (auto-generated if None)
            device_name: Human-readable device name
            storage_dir: Directory for sync data
            conflict_strategy: How to resolve conflicts
            enable_encryption: Enable E2E encryption
        """
        self.device_id = device_id or str(uuid.uuid4())
        self.device_name = device_name or f"Device-{self.device_id[:8]}"
        self.storage_dir = storage_dir or Path(os.environ.get("_SABLE_DATA_DIR", "data")) / "sync"
        self.storage_dir.mkdir(parents=True, exist_ok=True)

        self.conflict_resolver = ConflictResolver(conflict_strategy)
        self.sync_queue = SyncQueue(self.storage_dir / "queue.json")

        self.devices: Dict[str, Device] = {}
        self.sync_operations: Dict[str, SyncOperation] = {}

        self.is_online = False
        self.is_server = False

        self.enable_encryption = enable_encryption

        # Callbacks
        self.on_sync_complete: Optional[Callable] = None
        self.on_conflict: Optional[Callable] = None

        self._load_devices()

        logger.info(f"Initialized sync for device: {self.device_name} ({self.device_id})")

    def _load_devices(self):
        """Load registered devices."""
        devices_file = self.storage_dir / "devices.json"

        if not devices_file.exists():
            return

        try:
            with open(devices_file, "r") as f:
                data = json.load(f)
                for dev_data in data:
                    device = Device(
                        device_id=dev_data["device_id"],
                        device_name=dev_data["device_name"],
                        device_type=dev_data["device_type"],
                        last_seen=datetime.fromisoformat(dev_data["last_seen"]),
                        ip_address=dev_data.get("ip_address"),
                        public_key=dev_data.get("public_key"),
                        trusted=dev_data.get("trusted", False),
                        sync_enabled=dev_data.get("sync_enabled", True),
                        sync_scopes={SyncScope(s) for s in dev_data.get("sync_scopes", ["all"])},
                    )
                    self.devices[device.device_id] = device

            logger.info(f"Loaded {len(self.devices)} registered devices")
        except Exception as e:
            logger.error(f"Error loading devices: {e}")

    def _save_devices(self):
        """Save registered devices."""
        devices_file = self.storage_dir / "devices.json"

        try:
            data = [
                {
                    "device_id": dev.device_id,
                    "device_name": dev.device_name,
                    "device_type": dev.device_type,
                    "last_seen": dev.last_seen.isoformat(),
                    "ip_address": dev.ip_address,
                    "public_key": dev.public_key,
                    "trusted": dev.trusted,
                    "sync_enabled": dev.sync_enabled,
                    "sync_scopes": [s.value for s in dev.sync_scopes],
                }
                for dev in self.devices.values()
            ]

            with open(devices_file, "w") as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logger.error(f"Error saving devices: {e}")

    async def register_device(
        self, device_name: str, device_type: str, public_key: Optional[str] = None
    ) -> str:
        """
        Register a new device for syncing.

        Args:
            device_name: Device name
            device_type: Device type (mobile, desktop, server)
            public_key: Optional public key for encryption

        Returns:
            Device ID
        """
        device_id = str(uuid.uuid4())

        device = Device(
            device_id=device_id,
            device_name=device_name,
            device_type=device_type,
            last_seen=datetime.now(timezone.utc),
            public_key=public_key,
            trusted=False,  # Requires manual approval
            sync_enabled=True,
        )

        self.devices[device_id] = device
        self._save_devices()

        logger.info(f"Registered device: {device_name} ({device_id})")
        return device_id

    async def trust_device(self, device_id: str):
        """Trust a device for syncing."""
        if device_id in self.devices:
            self.devices[device_id].trusted = True
            self._save_devices()
            logger.info(f"Trusted device: {device_id}")

    async def untrust_device(self, device_id: str):
        """Untrust a device."""
        if device_id in self.devices:
            self.devices[device_id].trusted = False
            self._save_devices()
            logger.info(f"Untrusted device: {device_id}")

    async def sync_item(
        self, scope: SyncScope, item_id: str, data: Dict[str, Any], version: int = 1
    ) -> bool:
        """
        Sync a single item.

        Args:
            scope: Sync scope
            item_id: Item ID
            data: Item data
            version: Version number

        Returns:
            True if synced successfully
        """
        item = SyncItem(
            item_id=item_id,
            scope=scope,
            data=data,
            version=version,
            timestamp=datetime.now(timezone.utc),
            device_id=self.device_id,
            checksum="",
        )
        item.checksum = item.compute_checksum()

        if self.is_online:
            # Sync immediately
            return await self._sync_item_now(item)
        else:
            # Add to queue
            self.sync_queue.add(item)
            logger.info(f"Added {item_id} to offline queue")
            return True

    async def _sync_item_now(self, item: SyncItem) -> bool:
        """Sync item immediately (requires online connection)."""
        # In real implementation, this would send over WebSocket/HTTP
        # For now, simulate sync

        logger.info(f"Syncing {item.item_id} ({item.scope.value})")

        # Simulate network delay
        await asyncio.sleep(0.1)

        # Check for conflicts
        # (In real implementation, server would check and return conflicts)

        return True

    async def sync_all(self, scope: Optional[SyncScope] = None) -> SyncOperation:
        """
        Sync all pending items.

        Args:
            scope: Optional scope filter

        Returns:
            SyncOperation with results
        """
        operation_id = str(uuid.uuid4())

        # Get pending items
        pending = self.sync_queue.get_pending(scope)

        operation = SyncOperation(
            operation_id=operation_id,
            device_id=self.device_id,
            scope=scope or SyncScope.ALL,
            status=SyncStatus.IN_PROGRESS,
            items_total=len(pending),
            items_synced=0,
            started_at=datetime.now(timezone.utc),
        )

        self.sync_operations[operation_id] = operation

        logger.info(f"Starting sync operation: {operation_id} ({len(pending)} items)")

        # Sync each item
        for item in pending:
            try:
                success = await self._sync_item_now(item)

                if success:
                    operation.items_synced += 1
                    self.sync_queue.remove(item.item_id)

            except Exception as e:
                logger.error(f"Error syncing {item.item_id}: {e}")
                operation.error = str(e)

        # Complete operation
        operation.status = (
            SyncStatus.COMPLETED
            if operation.items_synced == operation.items_total
            else SyncStatus.FAILED
        )
        operation.completed_at = datetime.now(timezone.utc)

        logger.info(
            f"Sync operation complete: {operation.items_synced}/{operation.items_total} synced"
        )

        if self.on_sync_complete:
            await self.on_sync_complete(operation)

        return operation

    async def start_real_time_sync(self, server_url: str):
        """
        Start real-time sync over WebSocket.

        Args:
            server_url: WebSocket server URL
        """
        import websockets

        logger.info(f"Connecting to sync server: {server_url}")

        try:
            async with websockets.connect(server_url) as websocket:
                self.is_online = True

                # Send device info
                await websocket.send(
                    json.dumps(
                        {
                            "type": "register",
                            "device_id": self.device_id,
                            "device_name": self.device_name,
                        }
                    )
                )

                # Sync pending queue
                await self.sync_all()

                # Listen for updates
                async for message in websocket:
                    data = json.loads(message)
                    await self._handle_sync_message(data)

        except Exception as e:
            logger.error(f"Real-time sync error: {e}")
            self.is_online = False

    async def _handle_sync_message(self, message: Dict[str, Any]):
        """Handle incoming sync message."""
        msg_type = message.get("type")

        if msg_type == "sync_item":
            # Remote device synced an item
            item_data = message["item"]
            remote_item = SyncItem(
                item_id=item_data["item_id"],
                scope=SyncScope(item_data["scope"]),
                data=item_data["data"],
                version=item_data["version"],
                timestamp=datetime.fromisoformat(item_data["timestamp"]),
                device_id=item_data["device_id"],
                checksum=item_data["checksum"],
            )

            # Check for local version
            # (In real implementation, would load from local storage)

            # For now, just accept remote version
            logger.info(f"Received sync item: {remote_item.item_id}")

        elif msg_type == "conflict":
            # Conflict detected
            logger.warning("Sync conflict detected")

            if self.on_conflict:
                await self.on_conflict(message)

    def get_sync_status(self) -> Dict[str, Any]:
        """Get current sync status."""
        return {
            "device_id": self.device_id,
            "device_name": self.device_name,
            "is_online": self.is_online,
            "pending_items": len(self.sync_queue.queue),
            "registered_devices": len(self.devices),
            "trusted_devices": sum(1 for d in self.devices.values() if d.trusted),
            "recent_operations": len(
                [
                    op
                    for op in self.sync_operations.values()
                    if op.started_at > datetime.now(timezone.utc) - timedelta(hours=1)
                ]
            ),
        }


# Example usage
async def main():
    """Example multi-device sync usage."""

    print("=" * 60)
    print("Multi-Device Sync Example")
    print("=" * 60)

    # Initialize sync on Device 1
    print("\n📱 Initializing Device 1...")
    device1 = MultiDeviceSync(device_name="Desktop", conflict_strategy=SyncStrategy.LATEST_WINS)
    print(f"  Device ID: {device1.device_id}")

    # Initialize sync on Device 2
    print("\n📱 Initializing Device 2...")
    device2 = MultiDeviceSync(device_name="Mobile", conflict_strategy=SyncStrategy.LATEST_WINS)
    print(f"  Device ID: {device2.device_id}")

    # Register devices with each other
    print("\n🔗 Registering devices...")
    await device1.register_device("Mobile", "mobile")
    await device2.register_device("Desktop", "desktop")
    await device1.trust_device(device2.device_id)
    await device2.trust_device(device1.device_id)
    print("  ✅ Devices registered and trusted")

    # Sync some data from Device 1
    print("\n📤 Syncing from Device 1...")
    await device1.sync_item(
        scope=SyncScope.SETTINGS,
        item_id="user_preferences",
        data={"theme": "dark", "language": "en", "notifications": True},
        version=1,
    )
    print("  ✅ Settings synced")

    # Sync conversation from Device 2
    print("\n📤 Syncing from Device 2...")
    await device2.sync_item(
        scope=SyncScope.CONVERSATIONS,
        item_id="conv_001",
        data={
            "messages": [
                {"role": "user", "content": "Hello!"},
                {"role": "assistant", "content": "Hi there!"},
            ]
        },
        version=1,
    )
    print("  ✅ Conversation synced")

    # Get sync status
    print("\n📊 Sync Status:")
    status1 = device1.get_sync_status()
    print(
        f"  Device 1: {status1['pending_items']} pending, {status1['trusted_devices']} trusted devices"
    )

    status2 = device2.get_sync_status()
    print(
        f"  Device 2: {status2['pending_items']} pending, {status2['trusted_devices']} trusted devices"
    )

    print("\n✅ Multi-device sync example complete!")
    print("\n💡 In production:")
    print("  • Connect to WebSocket server for real-time sync")
    print("  • Enable E2E encryption for security")
    print("  • Use selective sync scopes to save bandwidth")
    print("  • Handle conflicts automatically or manually")


if __name__ == "__main__":
    asyncio.run(main())
