"""The imap integration."""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING

from aioimaplib import IMAP4_SSL, AioImapException, Response
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import (
    HomeAssistant,
    ServiceCall,
    ServiceResponse,
    SupportsResponse,
    callback,
)
from homeassistant.exceptions import (
    ConfigEntryAuthFailed,
    ConfigEntryError,
    ConfigEntryNotReady,
    ServiceValidationError,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .const import CONF_ENABLE_PUSH, DOMAIN
from .coordinator import (
    ImapMessage,
    ImapPollingDataUpdateCoordinator,
    ImapPushDataUpdateCoordinator,
    connect_to_server,
)
from .errors import InvalidAuth, InvalidFolder

PLATFORMS: list[Platform] = [Platform.SENSOR]

CONF_ENTRY = "entry"
CONF_SEEN = "seen"
CONF_UID = "uid"
CONF_TARGET_FOLDER = "target_folder"

_LOGGER = logging.getLogger(__name__)


CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

_SERVICE_UID_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_ENTRY): cv.string,
        vol.Required(CONF_UID): cv.string,
    }
)

SERVICE_SEEN_SCHEMA = _SERVICE_UID_SCHEMA
SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend(
    {
        vol.Optional(CONF_SEEN): cv.boolean,
        vol.Required(CONF_TARGET_FOLDER): cv.string,
    }
)
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA


async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL:
    """Get IMAP client and connect."""
    if hass.data[DOMAIN].get(entry_id) is None:
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="invalid_entry",
        )
    entry = hass.config_entries.async_get_entry(entry_id)
    if TYPE_CHECKING:
        assert entry is not None
    try:
        client = await connect_to_server(entry.data)
    except InvalidAuth as exc:
        raise ServiceValidationError(
            translation_domain=DOMAIN, translation_key="invalid_auth"
        ) from exc
    except InvalidFolder as exc:
        raise ServiceValidationError(
            translation_domain=DOMAIN, translation_key="invalid_folder"
        ) from exc
    except (TimeoutError, AioImapException) as exc:
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="imap_server_fail",
            translation_placeholders={"error": str(exc)},
        ) from exc
    return client


@callback
def raise_on_error(response: Response, translation_key: str) -> None:
    """Get error message from response."""
    if response.result != "OK":
        error: str = response.lines[0].decode("utf-8")
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key=translation_key,
            translation_placeholders={"error": error},
        )


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up imap services."""

    async def async_seen(call: ServiceCall) -> None:
        """Process mark as seen service call."""
        entry_id: str = call.data[CONF_ENTRY]
        uid: str = call.data[CONF_UID]
        _LOGGER.debug(
            "Mark message %s as seen. Entry: %s",
            uid,
            entry_id,
        )
        client = await async_get_imap_client(hass, entry_id)
        try:
            response = await client.store(uid, "+FLAGS (\\Seen)")
        except (TimeoutError, AioImapException) as exc:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="imap_server_fail",
                translation_placeholders={"error": str(exc)},
            ) from exc
        raise_on_error(response, "seen_failed")
        await client.close()

    hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA)

    async def async_move(call: ServiceCall) -> None:
        """Process move email service call."""
        entry_id: str = call.data[CONF_ENTRY]
        uid: str = call.data[CONF_UID]
        seen = bool(call.data.get(CONF_SEEN))
        target_folder: str = call.data[CONF_TARGET_FOLDER]
        _LOGGER.debug(
            "Move message %s to folder %s. Mark as seen: %s. Entry: %s",
            uid,
            target_folder,
            seen,
            entry_id,
        )
        client = await async_get_imap_client(hass, entry_id)
        try:
            if seen:
                response = await client.store(uid, "+FLAGS (\\Seen)")
                raise_on_error(response, "seen_failed")
            response = await client.copy(uid, target_folder)
            raise_on_error(response, "copy_failed")
            response = await client.store(uid, "+FLAGS (\\Deleted)")
            raise_on_error(response, "delete_failed")
            response = await asyncio.wait_for(
                client.protocol.expunge(uid, by_uid=True), client.timeout
            )
            raise_on_error(response, "expunge_failed")
        except (TimeoutError, AioImapException) as exc:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="imap_server_fail",
                translation_placeholders={"error": str(exc)},
            ) from exc
        await client.close()

    hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA)

    async def async_delete(call: ServiceCall) -> None:
        """Process deleting email service call."""
        entry_id: str = call.data[CONF_ENTRY]
        uid: str = call.data[CONF_UID]
        _LOGGER.debug(
            "Delete message %s. Entry: %s",
            uid,
            entry_id,
        )
        client = await async_get_imap_client(hass, entry_id)
        try:
            response = await client.store(uid, "+FLAGS (\\Deleted)")
            raise_on_error(response, "delete_failed")
            response = await asyncio.wait_for(
                client.protocol.expunge(uid, by_uid=True), client.timeout
            )
            raise_on_error(response, "expunge_failed")
        except (TimeoutError, AioImapException) as exc:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="imap_server_fail",
                translation_placeholders={"error": str(exc)},
            ) from exc
        await client.close()

    hass.services.async_register(DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA)

    async def async_fetch(call: ServiceCall) -> ServiceResponse:
        """Process fetch email service and return content."""
        entry_id: str = call.data[CONF_ENTRY]
        uid: str = call.data[CONF_UID]
        _LOGGER.debug(
            "Fetch text for message %s. Entry: %s",
            uid,
            entry_id,
        )
        client = await async_get_imap_client(hass, entry_id)
        try:
            response = await client.fetch(uid, "BODY.PEEK[]")
        except (TimeoutError, AioImapException) as exc:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="imap_server_fail",
                translation_placeholders={"error": str(exc)},
            ) from exc
        raise_on_error(response, "fetch_failed")
        message = ImapMessage(response.lines[1])
        await client.close()
        return {
            "text": message.text,
            "sender": message.sender,
            "subject": message.subject,
            "uid": uid,
        }

    hass.services.async_register(
        DOMAIN,
        "fetch",
        async_fetch,
        SERVICE_FETCH_TEXT_SCHEMA,
        supports_response=SupportsResponse.ONLY,
    )

    return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up imap from a config entry."""
    try:
        imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data))
    except InvalidAuth as err:
        raise ConfigEntryAuthFailed from err
    except InvalidFolder as err:
        raise ConfigEntryError("Selected mailbox folder is invalid.") from err
    except (TimeoutError, AioImapException) as err:
        raise ConfigEntryNotReady from err

    coordinator_class: type[
        ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator
    ]
    enable_push: bool = entry.data.get(CONF_ENABLE_PUSH, True)
    if enable_push and imap_client.has_capability("IDLE"):
        coordinator_class = ImapPushDataUpdateCoordinator
    else:
        coordinator_class = ImapPollingDataUpdateCoordinator

    coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = (
        coordinator_class(hass, imap_client, entry)
    )
    await coordinator.async_config_entry_first_refresh()

    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

    entry.async_on_unload(
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown)
    )

    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

    return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
        coordinator: (
            ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator
        ) = hass.data[DOMAIN].pop(entry.entry_id)
        await coordinator.shutdown()
    return unload_ok
