"""Anki Connect API client."""

import logging
from typing import Any, Dict, List, Optional

import requests

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT_SECONDS = 10


class AnkiConnectError(Exception):
    """Exception raised for Anki Connect errors."""


class AnkiClient:
    """Client for interacting with Anki via AnkiConnect."""

    def __init__(self, host: str = "localhost", port: int = 8765):
        """Initialize Anki Connect client.

        Args:
            host: Anki Connect server host (default: localhost)
            port: Anki Connect server port (default: 8765)
        """
        self.url = f"http://{host}:{port}"
        self.version = 6

    def _request(self, action: str, **params) -> Dict[str, Any]:
        """Send a request to Anki Connect.

        Args:
            action: The action to perform
            **params: Parameters for the action

        Returns:
            The response from Anki Connect

        Raises:
            AnkiConnectError: If the request fails or returns an error
        """
        payload = {"action": action, "version": self.version, "params": params}

        try:
            response = requests.post(
                self.url,
                json=payload,
                timeout=REQUEST_TIMEOUT_SECONDS,
            )
            response.raise_for_status()
        except requests.exceptions.ConnectionError as e:
            raise AnkiConnectError(
                f"Could not connect to Anki Connect at {self.url}. Is Anki running?"
            ) from e
        except requests.exceptions.RequestException as e:
            raise AnkiConnectError(f"Request failed: {e}") from e

        result = response.json()

        if result.get("error") is not None:
            raise AnkiConnectError(f"Anki Connect error: {result['error']}")

        return result.get("result")

    def get_all_note_ids(self) -> List[int]:
        """Get all note IDs from Anki.

        Returns:
            List of note IDs
        """
        logger.debug("Fetching all note IDs from Anki")
        note_ids = self._request("findNotes", query="")
        logger.debug("Found %s notes", len(note_ids))

        # Check if note_ids is a list of integers
        if not isinstance(note_ids, list) or not all(
            isinstance(nid, int) for nid in note_ids
        ):
            raise AnkiConnectError("Unexpected response format for note IDs")

        return note_ids

    def get_notes_info(self, note_ids: List[int]) -> List[Dict[str, Any]]:
        """Get detailed information about notes.

        Args:
            note_ids: List of note IDs

        Returns:
            List of note info dictionaries
        """
        if not note_ids:
            return []

        logger.info("Fetching info for %s notes", len(note_ids))
        notes_info = self._request("notesInfo", notes=note_ids)

        # Check if notes_info is a list of dictionaries
        if not isinstance(notes_info, list) or not all(
            isinstance(info, dict) for info in notes_info
        ):
            raise AnkiConnectError("Unexpected response format for notes info")

        return notes_info

    def get_model_field_names(self, model_name: str) -> List[str]:
        """Get field names for a note type (model).

        Args:
            model_name: Name of the note type

        Returns:
            List of field names
        """
        logger.debug("Fetching field names for model: %s", model_name)
        models = self._request("modelNames")

        if model_name not in models:
            raise AnkiConnectError(f"Model '{model_name}' not found")

        field_names = self._request("modelFieldNames", modelName=model_name)

        # Check if field_names is a list of strings
        if not isinstance(field_names, list) or not all(
            isinstance(name, str) for name in field_names
        ):
            raise AnkiConnectError("Unexpected response format for model field names")

        return field_names

    def add_note(self, note_data: Dict[str, Any]) -> int:
        """Add a new note to Anki.

        Args:
            note_data: Note data with keys: 'deckName', 'modelName', 'fields', 'tags'

        Returns:
            The ID of the created note
        """
        logger.info("Adding note to deck: %s", note_data.get("deckName"))
        note_id = self._request("addNote", note=note_data)
        logger.debug("Created note with ID: %s", note_id)

        # Check if note_id is an integer
        if not isinstance(note_id, int):
            raise AnkiConnectError("Unexpected response format for added note ID")

        return note_id

    def update_note(
        self, note_id: int, fields: Dict[str, str], tags: List[str]
    ) -> None:
        """Update an existing note.

        Args:
            note_id: ID of the note to update
            fields: Dictionary of field_name -> field_value
            tags: List of tags
        """
        note = {"id": note_id, "fields": fields, "tags": tags}

        self._request("updateNote", note=note)
        logger.debug("Updated note %s", note_id)

    def get_card_ids_for_note(self, note_id: int) -> List[int]:
        """Get card IDs for a given note ID.

        Args:
            note_id: ID of the note

        Returns:
            List of card IDs
        """
        card_ids = self._request("findCards", query=f"nid:{note_id}")

        if not isinstance(card_ids, list) or not all(
            isinstance(cid, int) for cid in card_ids
        ):
            raise AnkiConnectError("Unexpected response format for card IDs")

        return card_ids

    def get_cards_info(self, card_ids: List[int]) -> List[Dict[str, Any]]:
        """Get detailed information about cards.

        Args:
            card_ids: List of card IDs

        Returns:
            List of card info dictionaries
        """
        if not card_ids:
            return []

        cards_info = self._request("cardsInfo", cards=card_ids)

        if not isinstance(cards_info, list) or not all(
            isinstance(info, dict) for info in cards_info
        ):
            raise AnkiConnectError("Unexpected response format for cards info")

        return cards_info

    def get_note_deck(self, note_id: int) -> Optional[str]:
        """Get the deck name for a note based on its cards.

        Args:
            note_id: ID of the note

        Returns:
            Deck name or None if not found
        """
        card_ids = self.get_card_ids_for_note(note_id)
        if not card_ids:
            return None

        cards_info = self.get_cards_info(card_ids)
        if not cards_info:
            return None

        return cards_info[0].get("deckName")

    def move_note_to_deck(self, note_id: int, deck_name: str) -> None:
        """Move all cards of a note to a different deck.

        Args:
            note_id: ID of the note
            deck_name: Target deck name
        """
        card_ids = self.get_card_ids_for_note(note_id)
        if not card_ids:
            logger.warning("No cards found for note %s", note_id)
            return

        self._request("changeDeck", cards=card_ids, deck=deck_name)
        logger.debug("Moved note %s to deck %s", note_id, deck_name)

    def note_exists(self, note_id: int) -> bool:
        """Check if a note exists.

        Args:
            note_id: ID of the note to check

        Returns:
            True if note exists, False otherwise
        """
        try:
            result = self._request("notesInfo", notes=[note_id])

            if len(result) == 0:
                return False

            return len(result) > 0
        except AnkiConnectError:
            return False

    def get_decks(self) -> Dict[str, Dict[str, Any]]:
        """Get all deck names and info.

        Returns:
            Dictionary of deck name -> deck info
        """
        logger.debug("Fetching deck list")
        decks = self._request("deckNames")

        # Check if decks is a list of strings
        if not isinstance(decks, list) or not all(
            isinstance(deck, str) for deck in decks
        ):
            raise AnkiConnectError("Unexpected response format for deck names")

        return {d: {} for d in decks}
