"""Preferences sync mixin for gRPC service."""

from __future__ import annotations

import hashlib
import json
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Protocol, Self, cast

from noteflow.infrastructure.logging import get_logger
from noteflow.infrastructure.persistence.repositories.preferences_repo import (
    PreferenceWithMetadata,
)

from ..proto import noteflow_pb2
from .errors import abort_database_required, abort_failed_precondition

if TYPE_CHECKING:
    from noteflow.domain.ports.repositories import PreferencesRepository
    from noteflow.domain.ports.unit_of_work import UnitOfWork

from ._types import GrpcContext

logger = get_logger(__name__)

# Entity type names for error messages
_ENTITY_PREFERENCES = "Preferences"


class PreferencesRepositoryProvider(Protocol):
    """Repository provider protocol for preferences operations."""

    @property
    def supports_preferences(self) -> bool: ...

    @property
    def preferences(self) -> PreferencesRepository: ...

    async def commit(self) -> None: ...

    async def __aenter__(self) -> Self: ...

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: object,
    ) -> None: ...


class PreferencesServicer(Protocol):
    """Protocol for hosts that support preferences operations."""

    def create_repository_provider(self) -> PreferencesRepositoryProvider | UnitOfWork: ...

    async def decode_and_validate_prefs(
        self,
        request: noteflow_pb2.SetPreferencesRequest,
        context: GrpcContext,
    ) -> dict[str, object]: ...

    async def apply_preferences(
        self,
        repo: PreferencesRepositoryProvider,
        request: noteflow_pb2.SetPreferencesRequest,
        current_prefs: list[PreferenceWithMetadata],
        decoded_prefs: dict[str, object],
    ) -> None: ...


def compute_etag(preferences: dict[str, str], updated_at: float) -> str:
    """Compute ETag from preferences and timestamp.

    Args:
        preferences: Dictionary of preference key-value pairs.
        updated_at: Unix timestamp of last update.

    Returns:
        MD5 hash as hex string for ETag.
    """
    content = json.dumps(preferences, sort_keys=True) + str(updated_at)
    return hashlib.md5(content.encode()).hexdigest()


def _prefs_to_dict_with_timestamp(
    prefs_with_meta: list[PreferenceWithMetadata],
) -> tuple[dict[str, str], float]:
    """Convert preference metadata list to dict with max timestamp.

    Args:
        prefs_with_meta: List of preference objects with key, value, updated_at.

    Returns:
        Tuple of (preferences dict with JSON-encoded values, max updated_at timestamp).
    """
    preferences: dict[str, str] = {}
    max_updated_at = 0.0

    for pref in prefs_with_meta:
        preferences[pref.key] = json.dumps(pref.value)
        pref_ts = pref.updated_at.timestamp()
        if pref_ts > max_updated_at:
            max_updated_at = pref_ts

    return preferences, max_updated_at


def _build_conflict_response(
    current_dict: dict[str, str],
    server_max_updated: float,
    current_etag: str,
) -> noteflow_pb2.SetPreferencesResponse:
    """Build conflict response for ETag mismatch."""
    return noteflow_pb2.SetPreferencesResponse(
        success=False,
        conflict=True,
        server_preferences=current_dict,
        server_updated_at=server_max_updated,
        etag=current_etag,
        conflict_message="Server preferences have been modified",
    )


class PreferencesMixin:
    """Mixin providing preferences sync functionality.

    Requires host to implement PreferencesServicer protocol.
    Preferences require database persistence.
    """

    async def GetPreferences(
        self: PreferencesServicer,
        request: noteflow_pb2.GetPreferencesRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.GetPreferencesResponse:
        """Get all preferences with sync metadata."""
        async with self.create_repository_provider() as repo:
            if not repo.supports_preferences:
                await abort_database_required(context, _ENTITY_PREFERENCES)

            keys_seq = cast(Sequence[str], request.keys)
            keys = list(keys_seq) if keys_seq else None
            prefs_with_meta = await repo.preferences.get_all_with_metadata(keys)
            preferences, max_updated_at = _prefs_to_dict_with_timestamp(prefs_with_meta)
            etag = compute_etag(preferences, max_updated_at)

            return noteflow_pb2.GetPreferencesResponse(
                preferences=preferences,
                updated_at=max_updated_at,
                etag=etag,
            )

    async def SetPreferences(
        self: PreferencesServicer,
        request: noteflow_pb2.SetPreferencesRequest,
        context: GrpcContext,
    ) -> noteflow_pb2.SetPreferencesResponse:
        """Set preferences with optimistic concurrency control."""
        async with self.create_repository_provider() as repo:
            if not repo.supports_preferences:
                await abort_database_required(context, _ENTITY_PREFERENCES)

            current_prefs = await repo.preferences.get_all_with_metadata()
            current_dict, server_max_updated = _prefs_to_dict_with_timestamp(current_prefs)
            current_etag = compute_etag(current_dict, server_max_updated)

            if request.if_match and request.if_match != current_etag:
                logger.warning(
                    "Preferences sync conflict: client etag=%s, server etag=%s",
                    request.if_match,
                    current_etag,
                )
                return _build_conflict_response(current_dict, server_max_updated, current_etag)

            decoded_prefs = await self.decode_and_validate_prefs(request, context)
            await self.apply_preferences(repo, request, current_prefs, decoded_prefs)
            await repo.commit()

            updated_prefs = await repo.preferences.get_all_with_metadata()
            updated_dict, new_max_updated = _prefs_to_dict_with_timestamp(updated_prefs)
            new_etag = compute_etag(updated_dict, new_max_updated)

            logger.info("Preferences synced: %d keys, merge=%s", len(decoded_prefs), request.merge)

            return noteflow_pb2.SetPreferencesResponse(
                success=True,
                conflict=False,
                server_preferences=updated_dict,
                server_updated_at=new_max_updated,
                etag=new_etag,
            )

    async def decode_and_validate_prefs(
        self: PreferencesServicer,
        request: noteflow_pb2.SetPreferencesRequest,
        context: GrpcContext,
    ) -> dict[str, object]:
        """Decode and validate JSON preferences from request."""
        decoded_prefs: dict[str, object] = {}
        prefs_map = cast(Mapping[str, str], request.preferences)
        for key, value_json in prefs_map.items():
            try:
                decoded_prefs[key] = json.loads(value_json)
            except json.JSONDecodeError as e:
                await abort_failed_precondition(context, f"Invalid JSON for preference '{key}': {e}")
        return decoded_prefs

    async def apply_preferences(
        self: PreferencesServicer,
        repo: PreferencesRepositoryProvider,
        request: noteflow_pb2.SetPreferencesRequest,
        current_prefs: list[PreferenceWithMetadata],
        decoded_prefs: dict[str, object],
    ) -> None:
        """Apply preferences based on merge mode."""
        if not request.merge:
            existing_keys = {p.key for p in current_prefs}
            keys_to_delete = existing_keys - set(decoded_prefs.keys())
            for key in keys_to_delete:
                await repo.preferences.delete(key)
        await repo.preferences.set_bulk(decoded_prefs)
