import logging
from typing import TYPE_CHECKING, Any

from mstrio import config
from mstrio.api import monitors
from mstrio.connection import Connection
from mstrio.server import Cluster
from mstrio.utils import helper
from mstrio.utils.version_helper import class_version_handler, meets_minimal_version

if TYPE_CHECKING:
    from mstrio.users_and_groups.user import User

logger = logging.getLogger(__name__)


@class_version_handler('11.2.0000')
class UserConnections:
    """Browse and manage active user connections on the environment. Use the
    `fetch()` method to fetch the latest active user connections. Filter the
    `user_connections` by using `filter_connections()` method.

    Attributes:
        connection: A Strategy One connection object
        user_connections: All active user connections on the environment
    """

    def __init__(self, connection: Connection):
        """Initialize the `UserConnections` object.

        Args:
            connection: Strategy One connection object returned
                by `connection.Connection()`
        """
        self.connection = connection
        self.user_connections: list[dict[str, Any]] = []

    def fetch(self) -> None:
        """Populate the `UserConnections` object by retrieving all active user
        connections on the environment."""
        self.user_connections = self.list_connections()

    def filter_connections(self, **filters) -> list[dict[str, Any]] | None:
        """Filter the user connections stored in the `UserConnections` object
        by specifying the `filters` keyword arguments.

        Args:
            **filters: Available filter parameters: ['id', 'parent_id',
                'username', 'user_full_name', 'project_index', 'project_id',
                'project_name', 'open_jobs_count', 'project_type',
                'date_connection_created', 'duration', 'session_id', 'client',
                'config_level']
        """
        filtered_connections = None
        if not self.user_connections:
            helper.exception_handler(
                "Populate the `UserConnections` object with `UserConnections.fetch` "
                "first.",
                Warning,
            )
        else:
            filtered_connections = helper.filter_list_of_dicts(
                self.user_connections, **filters
            )
        return filtered_connections

    def list_connections(
        self,
        nodes: str | list[str] | None = None,
        limit: int | None = None,
        **filters,
    ) -> list[dict[str, Any]]:
        """Get all active user connections. Optionally filter the connections
        by specifying the `filters` keyword arguments.

        Args:
            nodes: Node (server) names on which users will be disconnected.
            limit: limit the number of elements returned. If `None`, all objects
                are returned.
            **filters: Available filter parameters: ['id', 'parent_id',
                'username', 'user_full_name', 'project_index', 'project_id',
                'project_name', 'open_jobs_count', 'project_type',
                'date_connection_created', 'duration', 'session_id', 'client',
                'config_level', 'application_type', 'last_action']
        """
        all_nodes = Cluster(self.connection).list_nodes(to_dictionary=True)
        all_connections = []
        if nodes is None:
            nodes = [node['name'] for node in all_nodes if node['status'] == 'running']
        else:
            nodes = nodes if isinstance(nodes, list) else [nodes]

        msg = 'Error fetching chunk of active user connections.'
        for node in nodes:
            all_connections.extend(
                helper.fetch_objects_async(
                    self.connection,
                    monitors.get_user_connections,
                    monitors.get_user_connections_async,
                    dict_unpack_value="userConnections",
                    limit=limit,
                    chunk_size=1000,
                    error_msg=msg,
                    node_name=node,
                    filters=filters,
                )
            )
        return all_connections

    def disconnect_users(
        self,
        connection_ids: str | list[str] | None = None,
        users: list["User"] | list[str] | None = None,
        nodes: str | list[str] | None = None,
        force: bool = False,
        **filters,
    ) -> list[dict] | None:
        """Disconnect user connections by passing in users (objects) or
        connection_ids. Optionally disconnect users by specifying the `filters`
        keyword arguments.

        Args:
            connection_ids: chosen ids that can be retrieved with
                `list_connections()`
            users: List of User objects or usernames
            nodes: Node (server) names on which users will be disconnected
            force: if True, no additional prompt will be shown before
                disconnecting users
            **filters: Available filter parameters: ['id', 'parent_id',
                'username', 'user_full_name', 'project_index', 'project_id',
                'project_name', 'open_jobs_count', 'project_type',
                'date_connection_created', 'duration', 'session_id', 'client',
                'config_level']
        Returns:
            - list of statuses of disconnecting chosen connections with theirs
              ids and messages from the I-Server:
                status code 200 is when all connections were disconnected
                status code 207 is when some connections were disconnected
                status code 403 without error code is when no connections were
                disconnected
            - in case of error of nothing to disconnect it returns None


        """
        # import here to avoid circular imports
        from mstrio.users_and_groups.user import User

        if (
            self.connection
            and not connection_ids
            and not users
            and not filters
            and not force
        ):
            msg = (
                "You need to pass connection_ids or users or specify filters. To "
                "disconnect all connections use `disconnect_all_users()` method."
            )
            helper.exception_handler(msg)

        if connection_ids:
            # disconnect specific user connections without fetching connections
            return self.__disconnect_by_connection_id(connection_ids)

        # get all user connections to filter locally
        all_connections = self.list_connections(nodes, **filters)
        if users:  # filter user connections by user objects
            users = users if isinstance(users, list) else [users]
            usernames = []
            for user in users:
                if isinstance(user, User):
                    usernames.append(user.username)
                elif isinstance(user, str):
                    usernames.append(user)
                else:
                    helper.exception_handler(
                        "'user' param must be a list of User objects or usernames.",
                        exception_type=TypeError,
                    )

            all_connections = list(
                filter(lambda conn: conn['username'] in usernames, all_connections)
            )

        if all_connections:
            # extract connection ids and disconnect
            connection_ids = [conn['id'] for conn in all_connections]
            return self.__disconnect_by_connection_id(connection_ids)
        elif config.verbose:
            logger.info('No active user connections.')

    def disconnect_all_users(self, force: bool = False) -> list[dict] | None:
        """Disconnect all user connections.

        Args:
            force: if True, no additional prompt will be shown before
                disconnecting all users

        Returns:
            - list of statuses of disconnecting all connections with their ids
               and messages from the I-Server
            - in case of error it returns None
        """
        if not force:
            user_input = input(
                "Are you sure you want to disconnect all users from the I-Server? "
                "[Y/N]: "
            )
            if user_input != "Y":
                return None
            else:
                force = True

        return self.disconnect_users(force=force)

    def __disconnect_by_connection_id(
        self, connection_ids: str | list[str]
    ) -> list[dict] | None:
        """It disconnects connections which ids are provided in
        'connection_ids'. It prints information about executed operations.
        Returns list of statuses of for the given connection ids with the
        messages from the I-Server or `None` in case of an error.
        """
        connection_ids = (
            connection_ids if isinstance(connection_ids, list) else [connection_ids]
        )
        server_version = (
            helper.version_cut(self.connection.iserver_version)
            if self.connection.iserver_version
            else None
        )

        # use monitors.delete_user_connections
        # or monitors.delete_user_connection depending on the server version
        if meets_minimal_version(server_version, '11.3.1'):
            res = monitors.delete_user_connections(
                connection=self.connection, ids=connection_ids
            )
            if res.status_code in [200, 207] or (
                res.status_code == 403 and not res.json().get('code')
            ):
                return self._prepare_disconnect_by_id_message(
                    statuses=res.json()['deleteUserConnectionsStatus']
                )
            else:
                err_msg = f'Error disconnecting user sessions: {connection_ids}.'
                return helper.response_handler(
                    response=res, msg=err_msg, throw_error=False
                )
        else:
            statuses: list[dict[str, str | int]] = []
            for connection_id in connection_ids:
                response = monitors.delete_user_connection(
                    self.connection, connection_id, bulk=True
                )
                statuses.append({'id': connection_id, 'status': response.status_code})
            return self._prepare_disconnect_by_id_message(statuses=statuses)

    @staticmethod
    def _prepare_disconnect_by_id_message(statuses: list[dict]) -> list[dict]:
        succeeded = []
        failed = []

        for s in statuses:
            status = s['status']
            id = s['id']
            if status >= 200 and status < 300:
                succeeded.append(id)
            elif status >= 400:
                # in most cases there will be status code 403
                failed.append(id)

        if config.verbose:
            if succeeded:
                logger.info(
                    'User connections with ids below were successfully disconnected:'
                    '\n\t' + ',\n\t'.join(succeeded)
                )
            if failed:
                logger.warning(
                    'User connections with ids below were not disconnected:\n\t'
                    + ',\n\t'.join(failed)
                )

        return statuses
