"""Filesystem-based EventService implementation."""

import glob
import json
import logging
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import AsyncGenerator
from uuid import UUID

from fastapi import Request

from openhands.agent_server.models import EventPage, EventSortOrder
from openhands.app_server.app_conversation.app_conversation_info_service import (
    AppConversationInfoService,
)
from openhands.app_server.errors import OpenHandsError
from openhands.app_server.event.event_service import EventService, EventServiceInjector
from openhands.app_server.event_callback.event_callback_models import EventKind
from openhands.app_server.services.injector import InjectorState
from openhands.sdk import Event

_logger = logging.getLogger(__name__)


@dataclass
class FilesystemEventService(EventService):
    """Filesystem-based implementation of EventService.

    Events are stored in files with the naming format:
    {conversation_id}/{YYYYMMDDHHMMSS}_{kind}_{id.hex}

    Uses an AppConversationInfoService to lookup conversations
    """

    app_conversation_info_service: AppConversationInfoService
    events_dir: Path

    def _ensure_events_dir(self, conversation_id: UUID | None = None) -> Path:
        """Ensure the events directory exists."""
        if conversation_id:
            events_path = self.events_dir / str(conversation_id)
        else:
            events_path = self.events_dir
        events_path.mkdir(parents=True, exist_ok=True)
        return events_path

    def _timestamp_to_str(self, timestamp: datetime | str) -> str:
        """Convert timestamp to YYYYMMDDHHMMSS format."""
        if isinstance(timestamp, str):
            # Parse ISO format timestamp string
            dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
            return dt.strftime('%Y%m%d%H%M%S')
        return timestamp.strftime('%Y%m%d%H%M%S')

    def _get_event_filename(self, conversation_id: UUID, event: Event) -> str:
        """Generate filename using YYYYMMDDHHMMSS_kind_id.hex format."""
        timestamp_str = self._timestamp_to_str(event.timestamp)
        kind = event.__class__.__name__
        # Handle both UUID objects and string UUIDs
        if isinstance(event.id, str):
            id_hex = event.id.replace('-', '')
        else:
            id_hex = event.id.hex
        return f'{timestamp_str}_{kind}_{id_hex}'

    def _save_event_to_file(self, conversation_id: UUID, event: Event) -> None:
        """Save an event to a file."""
        events_path = self._ensure_events_dir(conversation_id)
        filename = self._get_event_filename(conversation_id, event)
        filepath = events_path / filename

        with open(filepath, 'w') as f:
            # Use model_dump with mode='json' to handle UUID serialization
            data = event.model_dump(mode='json')
            f.write(json.dumps(data, indent=2))

    def _load_event_from_file(self, filepath: Path) -> Event | None:
        """Load an event from a file."""
        try:
            json_data = filepath.read_text()
            return Event.model_validate_json(json_data)
        except Exception:
            return None

    def _get_event_files_by_pattern(
        self, pattern: str, conversation_id: UUID | None = None
    ) -> list[Path]:
        """Get event files matching a glob pattern, sorted by timestamp."""
        if conversation_id:
            search_path = self.events_dir / str(conversation_id) / pattern
        else:
            search_path = self.events_dir / '*' / pattern

        files = glob.glob(str(search_path))
        return sorted([Path(f) for f in files])

    def _parse_filename(self, filename: str) -> dict[str, str] | None:
        """Parse filename to extract timestamp, kind, and event_id."""
        try:
            parts = filename.split('_')
            if len(parts) >= 3:
                timestamp_str = parts[0]
                kind = '_'.join(parts[1:-1])  # Handle kinds with underscores
                event_id = parts[-1]
                return {'timestamp': timestamp_str, 'kind': kind, 'event_id': event_id}
        except Exception:
            pass
        return None

    def _get_conversation_id(self, file: Path) -> UUID | None:
        try:
            return UUID(file.parent.name)
        except Exception:
            return None

    def _get_conversation_ids(self, files: list[Path]) -> set[UUID]:
        result = set()
        for file in files:
            conversation_id = self._get_conversation_id(file)
            if conversation_id:
                result.add(conversation_id)
        return result

    async def _filter_files_by_conversation(self, files: list[Path]) -> list[Path]:
        conversation_ids = list(self._get_conversation_ids(files))
        conversations = (
            await self.app_conversation_info_service.batch_get_app_conversation_info(
                conversation_ids
            )
        )
        permitted_conversation_ids = set()
        for conversation in conversations:
            if conversation:
                permitted_conversation_ids.add(conversation.id)
        result = [
            file
            for file in files
            if self._get_conversation_id(file) in permitted_conversation_ids
        ]
        return result

    def _filter_files_by_criteria(
        self,
        files: list[Path],
        conversation_id__eq: UUID | None = None,
        kind__eq: EventKind | None = None,
        timestamp__gte: datetime | None = None,
        timestamp__lt: datetime | None = None,
    ) -> list[Path]:
        """Filter files based on search criteria."""
        filtered_files = []

        for file_path in files:
            # Check conversation_id filter
            if conversation_id__eq:
                if str(conversation_id__eq) not in str(file_path):
                    continue

            # Parse filename for additional filtering
            filename_info = self._parse_filename(file_path.name)
            if not filename_info:
                continue

            # Check kind filter
            if kind__eq and filename_info['kind'] != kind__eq:
                continue

            # Check timestamp filters
            if timestamp__gte or timestamp__lt:
                try:
                    file_timestamp = datetime.strptime(
                        filename_info['timestamp'], '%Y%m%d%H%M%S'
                    )
                    if timestamp__gte and file_timestamp < timestamp__gte:
                        continue
                    if timestamp__lt and file_timestamp >= timestamp__lt:
                        continue
                except ValueError:
                    continue

            filtered_files.append(file_path)

        return filtered_files

    async def get_event(self, event_id: str) -> Event | None:
        """Get the event with the given id, or None if not found."""
        # Convert event_id to hex format (remove dashes) for filename matching
        if isinstance(event_id, str) and '-' in event_id:
            id_hex = event_id.replace('-', '')
        else:
            id_hex = event_id

        # Use glob pattern to find files ending with the event_id
        pattern = f'*_{id_hex}'
        files = self._get_event_files_by_pattern(pattern)

        if not files:
            return None

        # If there is no access to the conversation do not return the event
        file = files[0]
        conversation_id = self._get_conversation_id(file)
        if not conversation_id:
            return None
        conversation = (
            await self.app_conversation_info_service.get_app_conversation_info(
                conversation_id
            )
        )
        if not conversation:
            return None

        # Load and return the first matching event
        return self._load_event_from_file(file)

    async def search_events(
        self,
        conversation_id__eq: UUID | None = None,
        kind__eq: EventKind | None = None,
        timestamp__gte: datetime | None = None,
        timestamp__lt: datetime | None = None,
        sort_order: EventSortOrder = EventSortOrder.TIMESTAMP,
        page_id: str | None = None,
        limit: int = 100,
    ) -> EventPage:
        """Search for events matching the given filters."""
        # Build the search pattern
        pattern = '*'
        files = self._get_event_files_by_pattern(pattern, conversation_id__eq)

        files = await self._filter_files_by_conversation(files)

        files = self._filter_files_by_criteria(
            files, conversation_id__eq, kind__eq, timestamp__gte, timestamp__lt
        )

        files.sort(
            key=lambda f: f.name,
            reverse=(sort_order == EventSortOrder.TIMESTAMP_DESC),
        )

        # Handle pagination
        start_index = 0
        if page_id:
            for i, file_path in enumerate(files):
                if file_path.name == page_id:
                    start_index = i + 1
                    break

        # Collect items for this page
        page_files = files[start_index : start_index + limit]
        next_page_id = None
        if start_index + limit < len(files):
            next_page_id = files[start_index + limit].name

        # Load all events from files
        page_events = []
        for file_path in page_files:
            event = self._load_event_from_file(file_path)
            if event is not None:
                page_events.append(event)

        return EventPage(items=page_events, next_page_id=next_page_id)

    async def count_events(
        self,
        conversation_id__eq: UUID | None = None,
        kind__eq: EventKind | None = None,
        timestamp__gte: datetime | None = None,
        timestamp__lt: datetime | None = None,
        sort_order: EventSortOrder = EventSortOrder.TIMESTAMP,
    ) -> int:
        """Count events matching the given filters."""
        # Build the search pattern
        pattern = '*'
        files = self._get_event_files_by_pattern(pattern, conversation_id__eq)

        files = await self._filter_files_by_conversation(files)

        files = self._filter_files_by_criteria(
            files, conversation_id__eq, kind__eq, timestamp__gte, timestamp__lt
        )

        return len(files)

    async def save_event(self, conversation_id: UUID, event: Event):
        """Save an event. Internal method intended not be part of the REST api."""
        conversation = (
            await self.app_conversation_info_service.get_app_conversation_info(
                conversation_id
            )
        )
        if not conversation:
            # This is either an illegal state or somebody is trying to hack
            raise OpenHandsError('No such conversation: {conversaiont_id}')
        self._save_event_to_file(conversation_id, event)


class FilesystemEventServiceInjector(EventServiceInjector):
    async def inject(
        self, state: InjectorState, request: Request | None = None
    ) -> AsyncGenerator[EventService, None]:
        from openhands.app_server.config import (
            get_app_conversation_info_service,
            get_global_config,
        )

        async with get_app_conversation_info_service(
            state, request
        ) as app_conversation_info_service:
            persistence_dir = get_global_config().persistence_dir

            yield FilesystemEventService(
                app_conversation_info_service=app_conversation_info_service,
                events_dir=persistence_dir / 'v1' / 'events',
            )
