import asyncio

from typing import Optional

from shimoku.api.resources.role import (
    user_delete_role,
    user_get_role,
    user_get_roles,
    user_create_role,
)
from shimoku.api.resources.business import Business
from shimoku.api.resources.app import App
from shimoku.api.resources.report import Report
from shimoku.api.resources.universe import Universe
from shimoku.utils import create_normalized_name
from shimoku.exceptions import WorkspaceError

import logging
from shimoku.execution_logger import log_error, ClassWithLogging

logger = logging.getLogger(__name__)


class WorkspacesLayer(ClassWithLogging):
    """
    This class is used to interact with the API at the Workspace layer
    """

    _module_logger = logger
    _use_info_logging = True

    def __init__(
        self,
        universe: Universe,
    ):
        self._universe = universe
        self._get_for_roles = self._get_business_with_warning

    async def _check_similar_name(self, name: str):
        """
        Check if there is a workspace with a similar name
        :param name: name of the workspace
        """
        all_business = await self._universe.get_businesses()
        normalized_name = create_normalized_name(name)
        if normalized_name in [
            create_normalized_name(business["name"]) for business in all_business
        ]:
            log_error(
                logger,
                "There exists a workspace with a very similar name, please choose another name",
                WorkspaceError,
            )

    async def _get_business_with_warning(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> Optional[Business]:
        """
        Get a workspace with a warning if it does not exist
        :param name: name of the workspace
        :param uuid: uuid of the workspace
        """
        business: Business = await self._universe.get_business(uuid=uuid, name=name)
        if not business:
            logger.warning(f"Workspace ({name if name else uuid}) not found")
        return business

    async def create_workspace(
        self, name: str, create_default_roles: bool = True, theme: Optional[dict] = None
    ) -> dict:
        """Create a new workspace and the necessary roles if specified
        :param name: Name of the workspace
        :param create_default_roles: Create the default roles for the workspace
        :param theme: Theme of the workspace
        :return: workspace data
        """
        await self._check_similar_name(name)

        business = await self._universe.create_business(name=name, theme=theme)
        logger.info(f"Created workspace ({str(business)})")

        if create_default_roles:
            create_roles_tasks = []

            for role_permission_resource in [
                "DATA",
                "DATA_EXECUTION",
                "USER_MANAGEMENT",
                "BUSINESS_INFO",
            ]:
                create_roles_tasks.append(
                    business.create_role(
                        role="business_read",
                        resource=role_permission_resource,
                    )
                )

            await asyncio.gather(*create_roles_tasks)
            logger.info(f"Created default roles for workspace ({str(business)})")

        return business.cascade_to_dict()

    async def update_workspace(
        self,
        uuid: Optional[str] = None,
        name: Optional[str] = None,
        new_name: Optional[str] = None,
        theme: Optional[dict] = None,
    ) -> bool:
        """Update a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param new_name: New name of the workspace
        :param theme: New theme of the workspace
        :return: True if the workspace was updated
        """
        params = {}

        if new_name is not None:
            await self._check_similar_name(new_name)
            params["new_name"] = new_name

        if isinstance(theme, dict):
            params["theme"] = theme

        return await self._universe.update_business(uuid=uuid, name=name, **params)

    async def delete_workspace(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> bool:
        """Delete a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: True if the workspace was deleted
        """
        return await self._universe.delete_business(uuid=uuid, name=name)

    async def get_workspace(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> dict:
        """Get a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: Workspace data
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return {}
        return business.cascade_to_dict()

    async def get_workspace_menu_paths(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> list[dict]:
        """Get the menu paths of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: list of apps
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return []
        return [app.cascade_to_dict() for app in await business.get_apps()]

    async def get_workspace_menu_path_ids(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> list[dict]:
        """Get the menu path ids of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: list of app ids
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return []
        return [app["id"] for app in await business.get_apps()]

    async def get_workspace_boards(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> list[dict]:
        """Get the boards of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: list of dashboards
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return []
        return [
            dashboard.cascade_to_dict() for dashboard in await business.get_dashboards()
        ]

    async def delete_all_workspace_menu_paths(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ):
        """Delete all menu paths of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return
        apps = await business.get_apps()
        await asyncio.gather(*[business.delete_app(uuid=app["id"]) for app in apps])

    async def delete_all_workspace_boards(
        self,
        uuid: Optional[str] = None,
        name: Optional[str] = None,
        force: bool = False,
    ):
        """Delete all boards of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param force: Flag to force delete the boards
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return
        dashboards = await business.get_dashboards()
        if force:
            await asyncio.gather(
                *[dashboard.remove_all_apps() for dashboard in dashboards]
            )
        await asyncio.gather(
            *[
                business.delete_dashboard(uuid=dashboard["id"])
                for dashboard in dashboards
            ]
        )

    async def change_boards_order(
        self,
        boards: list[str],
        uuid: Optional[str] = None,
        name: Optional[str] = None,
    ):
        """Change the order of the boards of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param boards: list of dashboard names
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return
        await asyncio.gather(
            *[
                business.update_dashboard(name=d_name, order=i + 1)
                for i, d_name in enumerate(boards)
            ]
        )

    @staticmethod
    async def _change_sub_paths_order(
        business: Business, menu_path: str, sub_paths: list[str]
    ):
        """
        Change the order of the sub paths of a menu path
        :param business: Business object
        :param menu_path: Menu path name
        :param sub_paths: list of sub paths
        """
        app: App = await business.get_app(name=menu_path, create_if_not_exists=False)
        if not app:
            logger.warning(f"Menu path ({menu_path}) not found")
            return
        reports: list[Report] = await app.get_reports()
        sub_paths = [create_normalized_name(sub_path) for sub_path in sub_paths]
        non_referenced_reports = sorted(
            [
                report
                for report in reports
                if report["path"] is not None
                and create_normalized_name(report["path"]) not in sub_paths
            ],
            key=lambda x: x["pathOrder"] if x["pathOrder"] is not None else 0,
        )
        for report in non_referenced_reports:
            sub_path = report["path"]
            if sub_path not in sub_paths:
                sub_paths.append(sub_path)

        tasks = []
        for i, sub_path in enumerate(sub_paths):
            _reports = [
                report
                for report in reports
                if (
                    report["path"] is not None
                    and create_normalized_name(report["path"])
                    == create_normalized_name(sub_path)
                )
            ]
            for report in _reports:
                tasks.append(app.update_report(uuid=report["id"], pathOrder=i))

        logger.info(f"Updating {len(reports)} components from menu path ({str(app)})")

        await asyncio.gather(*tasks)

    async def change_menu_order(
        self, menu_order: list, uuid: Optional[str] = None, name: Optional[str] = None
    ):
        """Change the order of the menu of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param menu_order: list of menu names
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return
        tasks = []
        for i, menu_option in enumerate(menu_order):
            menu_name = menu_option
            if isinstance(menu_option, tuple):
                menu_name, sub_paths = menu_option
                tasks.append(
                    self._change_sub_paths_order(
                        business=business, menu_path=menu_name, sub_paths=sub_paths
                    )
                )
            tasks.append(business.update_app(name=menu_name, order=i))

        await asyncio.gather(*tasks)

    # Accounts management
    async def invite_user(
        self, email: str, uuid: Optional[str] = None, name: Optional[str] = None,
        roles: Optional[list[str]] = None, send_email: bool = True
    ) -> Optional[dict]:
        """Invite a user to the workspace
        :param email: Email of the user
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param roles: Roles of the user
        :param send_email: Send the invitation email
        :return: Invitation data
        """
        if not roles:
            roles = []
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return None
        return (await business.invite_user(email, roles, send_email)).cascade_to_dict()

    async def get_pending_invitations(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> list[dict]:
        """Get the pending invitations of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: list of invitations
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return []
        return [
            invitation.cascade_to_dict()
            for invitation in await business.get_pending_invitations()
        ]

    async def delete_pending_invitation(
        self,
        email: str,
        uuid: Optional[str] = None,
        name: Optional[str] = None,
    ) -> bool:
        """Delete a pending invitation of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param email: Email of the user
        :return: True if the invitation was deleted
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return False

        if await business.delete_pending_invitation(email=email):
            logger.info(f"Successfully deleted pending invitation ({email})")
            return True
        else:
            logger.warning(f"Failed to delete pending invitation ({email})")
            return False

    async def get_all_workspace_users(
        self, uuid: Optional[str] = None, name: Optional[str] = None
    ) -> list[dict]:
        """Get the users of a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :return: list of users
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return []
        return [user.cascade_to_dict() for user in await business.get_users()]

    async def remove_user_from_workspace(
        self,
        email: str,
        uuid: Optional[str] = None,
        name: Optional[str] = None,
    ) -> bool:
        """Remove a user from a workspace
        :param name: Name of the workspace
        :param uuid: UUID of the workspace
        :param email: Email of the user
        :return: True if the user was removed
        """
        business = await self._get_business_with_warning(uuid=uuid, name=name)
        if not business:
            return False

        if await business.delete_user(email=email):
            logger.info(f"Successfully removed user ({email} from workspace")
        else:
            logger.warning(f"Failed to remove user ({email}) from workspace")

    # Role management
    get_role = user_get_role
    get_roles = user_get_roles
    create_role = user_create_role
    delete_role = user_delete_role
