"""Support to manage a ais tts."""
from http import HTTPStatus
import logging
import uuid

import voluptuous as vol

from homeassistant.components import http, websocket_api
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json

from .const import DOMAIN

ATTR_NAME = "name"

_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA)
EVENT = "aistts_list_updated"
EVENT_PLAY_ITEM = "aistts_play_item"
ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str})
PERSISTENCE = ".dom/.aistts.json"

SERVICE_ADD_ITEM = "add_item"
SERVICE_PLAY_ITEM = "play_item"
SERVICE_COMPLETE_ITEM = "complete_item"

SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): vol.Any(None, cv.string)})

WS_TYPE_AISTTS_ITEMS = "aistts/items"
WS_TYPE_AISTTS_ADD_ITEM = "aistts/items/add"
WS_TYPE_AISTTS_UPDATE_ITEM = "aistts/items/update"
WS_TYPE_AISTTS_CLEAR_ITEMS = "aistts/items/clear"
WS_TYPE_AISTTS_REORDER_ITEMS = "aistts/items/reorder"

SCHEMA_WEBSOCKET_ITEMS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {vol.Required("type"): WS_TYPE_AISTTS_ITEMS}
)

SCHEMA_WEBSOCKET_ADD_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_AISTTS_ADD_ITEM,
        vol.Required("name"): str,
        vol.Required("pitch"): str,
        vol.Required("rate"): str,
        vol.Required("language"): str,
        vol.Required("voice"): str,
    }
)

SCHEMA_WEBSOCKET_UPDATE_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_AISTTS_UPDATE_ITEM,
        vol.Required("item_id"): str,
        vol.Optional("name"): str,
        vol.Optional("complete"): bool,
    }
)

SCHEMA_WEBSOCKET_CLEAR_ITEMS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {vol.Required("type"): WS_TYPE_AISTTS_CLEAR_ITEMS}
)

SCHEMA_WEBSOCKET_REORDER_ITEMS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
    {
        vol.Required("type"): WS_TYPE_AISTTS_REORDER_ITEMS,
        vol.Required("item_ids"): [str],
    }
)


async def async_setup(hass, config):
    """Initialize the AIS TTS"""

    if DOMAIN not in config:
        return True

    async def add_item_service(call):
        """Add an item with `name`."""
        data = hass.data[DOMAIN]
        name = call.data.get(ATTR_NAME)
        pitch = all.data.get("pitch")
        rate = all.data.get("rate")
        language = all.data.get("language")
        voice = all.data.get("voice")
        if name is not None:
            data.async_add(name, pitch, rate, language, voice)

    async def complete_item_service(call):
        """Mark the item provided via `name` as completed."""
        data = hass.data[DOMAIN]
        name = call.data.get(ATTR_NAME)
        if name is None:
            return
        try:
            item = [item for item in data.items if item["name"] == name][0]
        except IndexError:
            _LOGGER.error("Removing of item failed: %s cannot be found", name)
        else:
            data.async_update(item["id"], {"name": name, "complete": True})

    async def change_auto_mode(call):
        """Change automation selected in auto mode."""
        entity_id = call.data.get("entity_id")
        change_to = call.data.get("change_to")
        automations = {
            state.entity_id: state.name
            for state in hass.states.async_all()
            if state.entity_id.startswith("automation")
            and not state.entity_id.startswith("automation.ais_")
        }
        for key, value in automations.items():
            await hass.services.async_call(
                "automation", "turn_off", {ATTR_ENTITY_ID: key}
            )
        if change_to == "on":
            await hass.services.async_call(
                "automation", "turn_on", {ATTR_ENTITY_ID: entity_id}
            )

    async def stop_talking(call):
        """Change automation selected in auto mode."""
        await hass.services.async_call(
            "ais_ai_service",
            "say_it",
            {
                "text": "",
            },
        )

    async def play_item_service(call):
        """Play the item"""
        item_no = 0
        if "item" in call.data:
            item_no = call.data.get("item")
        if item_no == 0:
            hass.bus.async_fire(EVENT_PLAY_ITEM)
        else:
            hass.data[DOMAIN].async_play_item(item_no)

    data = hass.data[DOMAIN] = AisTtsData(hass)
    await data.async_load()

    hass.services.async_register(
        DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA
    )
    hass.services.async_register(DOMAIN, SERVICE_PLAY_ITEM, play_item_service)
    hass.services.async_register(
        DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, schema=SERVICE_ITEM_SCHEMA
    )
    hass.services.async_register(DOMAIN, "change_auto_mode", change_auto_mode)
    hass.services.async_register(DOMAIN, "stop_talking", stop_talking)

    hass.http.register_view(AisTtsView)
    hass.http.register_view(CreateAisTtsItemView)
    hass.http.register_view(UpdateAisTtsItemView)
    hass.http.register_view(ClearCompletedItemsView)

    hass.components.frontend.async_register_built_in_panel(
        "aistts",
        require_admin=False,
        sidebar_title="TTS Ręczny",
        sidebar_icon="mdi:gesture-tap-button",
        update=True,
    )

    hass.components.frontend.async_register_built_in_panel(
        "aisttsauto",
        require_admin=False,
        sidebar_title="TTS Automatyczny",
        sidebar_icon="mdi:electric-switch",
        update=True,
    )

    # ais_tts - remove all panels
    if "lovelace-dom" in hass.data.get(hass.components.frontend.DATA_PANELS, {}):
        hass.components.frontend.async_remove_panel("lovelace-dom")
    if "aisaudio" in hass.data.get(hass.components.frontend.DATA_PANELS, {}):
        hass.components.frontend.async_remove_panel("aisaudio")
    if "map" in hass.data.get(hass.components.frontend.DATA_PANELS, {}):
        hass.components.frontend.async_remove_panel("map")
    if "history" in hass.data.get(hass.components.frontend.DATA_PANELS, {}):
        hass.components.frontend.async_remove_panel("history")
    if "logbook" in hass.data.get(hass.components.frontend.DATA_PANELS, {}):
        hass.components.frontend.async_remove_panel("logbook")

    hass.components.websocket_api.async_register_command(
        WS_TYPE_AISTTS_ITEMS, websocket_handle_items, SCHEMA_WEBSOCKET_ITEMS
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_AISTTS_ADD_ITEM, websocket_handle_add, SCHEMA_WEBSOCKET_ADD_ITEM
    )
    hass.components.websocket_api.async_register_command(
        WS_TYPE_AISTTS_UPDATE_ITEM,
        websocket_handle_update,
        SCHEMA_WEBSOCKET_UPDATE_ITEM,
    )

    hass.components.websocket_api.async_register_command(
        WS_TYPE_AISTTS_CLEAR_ITEMS,
        websocket_handle_clear,
        SCHEMA_WEBSOCKET_CLEAR_ITEMS,
    )

    hass.components.websocket_api.async_register_command(
        WS_TYPE_AISTTS_REORDER_ITEMS,
        websocket_handle_reorder,
        SCHEMA_WEBSOCKET_REORDER_ITEMS,
    )

    return True


class AisTtsData:
    """Class to hold ais tts data."""

    def __init__(self, hass):
        """Initialize the ais tts."""
        self.hass = hass
        self.items = []

    @callback
    def async_add(self, name, pitch, rate, language, voice):
        """Add a ais tts item."""
        item = {
            "name": name,
            "pitch": pitch,
            "rate": rate,
            "language": language,
            "voice": voice,
            "id": uuid.uuid4().hex,
            "complete": False,
        }
        self.items.append(item)
        self.hass.async_add_job(self.save)
        return item

    @callback
    def async_update(self, item_id, info):
        """Update a ais tts item."""
        item = next((itm for itm in self.items if itm["id"] == item_id), None)

        if item is None:
            raise KeyError

        info = ITEM_UPDATE_SCHEMA(info)
        item.update(info)
        self.hass.async_add_job(self.save)
        return item

    @callback
    def async_clear_completed(self):
        """Clear completed items."""
        self.items = [itm for itm in self.items if not itm["complete"]]
        self.hass.async_add_job(self.save)

    @callback
    def async_play_item(self, item_no):
        """Clear completed items."""
        try:
            item = self.items[item_no - 1]
            self.hass.async_add_job(
                self.hass.services.async_call(
                    "ais_ai_service",
                    "say_it",
                    {
                        "text": item["name"],
                        "pitch": item["pitch"],
                        "rate": item["rate"],
                        "language": item["language"],
                        "voice": item["voice"],
                    },
                )
            )
        except Exception as e:
            _LOGGER.error("async_play_item: " + str(e))

    @callback
    def async_reorder(self, item_ids):
        """Reorder items."""
        # The array for sorted items.
        new_items = []
        all_items_mapping = {item["id"]: item for item in self.items}
        # Append items by the order of passed in array.
        for item_id in item_ids:
            if item_id not in all_items_mapping:
                raise KeyError
            new_items.append(all_items_mapping[item_id])
            # Remove the item from mapping after it's appended in the result array.
            del all_items_mapping[item_id]
        # Append the rest of the items
        for key in all_items_mapping:
            # All the unchecked items must be passed in the item_ids array,
            # so all items left in the mapping should be checked items.
            if all_items_mapping[key]["complete"] is False:
                raise vol.Invalid(
                    "The item ids array doesn't contain all the unchecked shopping list items."
                )
            new_items.append(all_items_mapping[key])
        self.items = new_items
        self.hass.async_add_executor_job(self.save)

    async def async_load(self):
        """Load items."""

        def load():
            """Load the items synchronously."""
            return load_json(self.hass.config.path(PERSISTENCE), default=[])

        self.items = await self.hass.async_add_executor_job(load)

    def save(self):
        """Save the items."""
        save_json(self.hass.config.path(PERSISTENCE), self.items)


class AisTtsView(http.HomeAssistantView):
    """View to retrieve ais tts content."""

    url = "/api/aistts"
    name = "api:aistts"

    @callback
    def get(self, request):
        """Retrieve ais tts items."""
        return self.json(request.app["hass"].data[DOMAIN].items)


class UpdateAisTtsItemView(http.HomeAssistantView):
    """View to retrieve ais tts content."""

    url = "/api/aistts/item/{item_id}"
    name = "api:aistts:item:id"

    async def post(self, request, item_id):
        """Update a ais tts item."""
        data = await request.json()

        try:
            item = request.app["hass"].data[DOMAIN].async_update(item_id, data)
            request.app["hass"].bus.async_fire(EVENT)
            return self.json(item)
        except KeyError:
            return self.json_message("Item not found", HTTPStatus.NOT_FOUND)
        except vol.Invalid:
            return self.json_message("Item not found", HTTPStatus.BAD_REQUEST)


class CreateAisTtsItemView(http.HomeAssistantView):
    """View to retrieve ais tts content."""

    url = "/api/aistts/item"
    name = "api:aistts:item"

    @RequestDataValidator(vol.Schema({vol.Required("name"): str}))
    async def post(self, request, data):
        """Create a new ais tts item."""
        item = request.app["hass"].data[DOMAIN].async_add(data["name"])
        request.app["hass"].bus.async_fire(EVENT)
        return self.json(item)


class ClearCompletedItemsView(http.HomeAssistantView):
    """View to retrieve ais tts content."""

    url = "/api/aistts/clear_completed"
    name = "api:aistts:clear_completed"

    @callback
    def post(self, request):
        """Retrieve if API is running."""
        hass = request.app["hass"]
        hass.data[DOMAIN].async_clear_completed()
        hass.bus.async_fire(EVENT)
        return self.json_message("Cleared completed items.")


@callback
def websocket_handle_items(hass, connection, msg):
    """Handle get ais tts items."""
    connection.send_message(
        websocket_api.result_message(msg["id"], hass.data[DOMAIN].items)
    )


@callback
def websocket_handle_add(hass, connection, msg):
    """Handle add item to ais tts."""
    item = hass.data[DOMAIN].async_add(
        msg["name"], msg["pitch"], msg["rate"], msg["language"], msg["voice"]
    )
    hass.bus.async_fire(EVENT, {"action": "add", "item": item})
    connection.send_message(websocket_api.result_message(msg["id"], item))


@websocket_api.async_response
async def websocket_handle_update(hass, connection, msg):
    """Handle update ais tts item."""
    msg_id = msg.pop("id")
    item_id = msg.pop("item_id")
    msg.pop("type")
    data = msg

    try:
        item = hass.data[DOMAIN].async_update(item_id, data)
        hass.bus.async_fire(EVENT, {"action": "update", "item": item})
        connection.send_message(websocket_api.result_message(msg_id, item))
    except KeyError:
        connection.send_message(
            websocket_api.error_message(msg_id, "item_not_found", "Item not found")
        )


@callback
def websocket_handle_clear(hass, connection, msg):
    """Handle clearing ais tts items."""
    hass.data[DOMAIN].async_clear_completed()
    hass.bus.async_fire(EVENT, {"action": "clear"})
    connection.send_message(websocket_api.result_message(msg["id"]))


def websocket_handle_reorder(hass, connection, msg):
    """Handle reordering shopping_list items."""
    msg_id = msg.pop("id")
    try:
        hass.data[DOMAIN].async_reorder(msg.pop("item_ids"))
        hass.bus.async_fire(EVENT, {"action": "reorder"})
        connection.send_result(msg_id)
    except KeyError:
        connection.send_error(
            msg_id,
            websocket_api.const.ERR_NOT_FOUND,
            "One or more item id(s) not found.",
        )
    except vol.Invalid as err:
        connection.send_error(msg_id, websocket_api.const.ERR_INVALID_FORMAT, f"{err}")
