"""Backup agents for the Backup integration."""

from __future__ import annotations

import abc
from collections.abc import AsyncIterator, Callable, Coroutine
from pathlib import Path
from typing import Any, Protocol

from propcache.api import cached_property

from homeassistant.core import HomeAssistant, callback

from .models import AgentBackup, BackupAgentError


class BackupAgentUnreachableError(BackupAgentError):
    """Raised when the agent can't reach its API."""

    error_code = "backup_agent_unreachable"
    _message = "The backup agent is unreachable."


class BackupAgent(abc.ABC):
    """Backup agent interface."""

    domain: str
    name: str
    unique_id: str

    @cached_property
    def agent_id(self) -> str:
        """Return the agent_id."""
        return f"{self.domain}.{self.unique_id}"

    @abc.abstractmethod
    async def async_download_backup(
        self,
        backup_id: str,
        **kwargs: Any,
    ) -> AsyncIterator[bytes]:
        """Download a backup file.

        Raises BackupNotFound if the backup does not exist.

        :param backup_id: The ID of the backup that was returned in async_list_backups.
        :return: An async iterator that yields bytes.
        """

    @abc.abstractmethod
    async def async_upload_backup(
        self,
        *,
        open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
        backup: AgentBackup,
        **kwargs: Any,
    ) -> None:
        """Upload a backup.

        :param open_stream: A function returning an async iterator that yields bytes.
        :param backup: Metadata about the backup that should be uploaded.
        """

    @abc.abstractmethod
    async def async_delete_backup(
        self,
        backup_id: str,
        **kwargs: Any,
    ) -> None:
        """Delete a backup file.

        Raises BackupNotFound if the backup does not exist.

        :param backup_id: The ID of the backup that was returned in async_list_backups.
        """

    @abc.abstractmethod
    async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
        """List backups."""

    @abc.abstractmethod
    async def async_get_backup(
        self,
        backup_id: str,
        **kwargs: Any,
    ) -> AgentBackup:
        """Return a backup.

        Raises BackupNotFound if the backup does not exist.
        """


class LocalBackupAgent(BackupAgent):
    """Local backup agent."""

    @abc.abstractmethod
    def get_backup_path(self, backup_id: str) -> Path:
        """Return the local path to an existing backup.

        The method should return the path to the backup file with the specified id.
        Raises BackupAgentError if the backup does not exist.
        """

    @abc.abstractmethod
    def get_new_backup_path(self, backup: AgentBackup) -> Path:
        """Return the local path to a new backup."""


class BackupAgentPlatformProtocol(Protocol):
    """Define the format of backup platforms which implement backup agents."""

    async def async_get_backup_agents(
        self,
        hass: HomeAssistant,
        **kwargs: Any,
    ) -> list[BackupAgent]:
        """Return a list of backup agents."""

    @callback
    def async_register_backup_agents_listener(
        self,
        hass: HomeAssistant,
        *,
        listener: Callable[[], None],
        **kwargs: Any,
    ) -> Callable[[], None]:
        """Register a listener to be called when agents are added or removed.

        :return: A function to unregister the listener.
        """