# mypy: allow-untyped-defs

import uuid
import time
import os
import json
import re

from threading import Timer

from .test_loader import AUTOMATIC, MANUAL
from ..data.session import Session, PENDING, PAUSED, RUNNING, ABORTED, COMPLETED
from ..utils.user_agent_parser import parse_user_agent
from .event_dispatcher import STATUS_EVENT, RESUME_EVENT
from ..data.exceptions.not_found_exception import NotFoundException
from ..data.exceptions.invalid_data_exception import InvalidDataException
from ..utils.deserializer import deserialize_session

DEFAULT_TEST_TYPES = [AUTOMATIC, MANUAL]
DEFAULT_TEST_PATHS = ["/"]
DEFAULT_TEST_AUTOMATIC_TIMEOUT = 60000
DEFAULT_TEST_MANUAL_TIMEOUT = 300000


class SessionsManager:
    def initialize(self,
                   test_loader,
                   event_dispatcher,
                   tests_manager,
                   results_directory,
                   results_manager,
                   configuration):
        self._test_loader = test_loader
        self._sessions = {}
        self._expiration_timeout = None
        self._event_dispatcher = event_dispatcher
        self._tests_manager = tests_manager
        self._results_directory = results_directory
        self._results_manager = results_manager
        self._configuration = configuration

    def create_session(
        self,
        tests=None,
        test_types=None,
        timeouts=None,
        reference_tokens=None,
        user_agent=None,
        labels=None,
        expiration_date=None,
        type=None
    ):
        if tests is None:
            tests = {}
        if timeouts is None:
            timeouts = {}
        if reference_tokens is None:
            reference_tokens = []
        if user_agent is None:
            user_agent = ""
        if labels is None:
            labels = []

        if "include" not in tests:
            tests["include"] = DEFAULT_TEST_PATHS
        if "exclude" not in tests:
            tests["exclude"] = []
        if "automatic" not in timeouts:
            timeouts["automatic"] = self._configuration["timeouts"]["automatic"]
        if "manual" not in timeouts:
            timeouts["manual"] = self._configuration["timeouts"]["manual"]
        if test_types is None:
            test_types = DEFAULT_TEST_TYPES

        for test_type in test_types:
            if test_type != "automatic" and test_type != "manual":
                raise InvalidDataException(f"Unknown type '{test_type}'")

        token = str(uuid.uuid1())
        pending_tests = self._test_loader.get_tests(
            test_types,
            include_list=tests["include"],
            exclude_list=tests["exclude"],
            reference_tokens=reference_tokens)

        browser = parse_user_agent(user_agent)

        test_files_count = self._tests_manager.calculate_test_files_count(
            pending_tests
        )

        test_state = {}
        for api in test_files_count:
            test_state[api] = {
                "pass": 0,
                "fail": 0,
                "timeout": 0,
                "not_run": 0,
                "total": test_files_count[api],
                "complete": 0}

        date_created = int(time.time() * 1000)

        session = Session(
            token=token,
            tests=tests,
            user_agent=user_agent,
            browser=browser,
            test_types=test_types,
            timeouts=timeouts,
            pending_tests=pending_tests,
            running_tests={},
            test_state=test_state,
            status=PENDING,
            reference_tokens=reference_tokens,
            labels=labels,
            type=type,
            expiration_date=expiration_date,
            date_created=date_created
        )

        self._push_to_cache(session)
        if expiration_date is not None:
            self._set_expiration_timer()

        return session

    def read_session(self, token):
        if token is None:
            return None
        session = self._read_from_cache(token)
        if session is None or session.test_state is None:
            print("loading session from file system")
            session = self.load_session(token)
        if session is not None:
            self._push_to_cache(session)
        return session

    def read_sessions(self, index=None, count=None):
        if index is None:
            index = 0
        if count is None:
            count = 10
        self.load_all_sessions_info()
        sessions = []
        for it_index, token in enumerate(self._sessions):
            if it_index < index:
                continue
            if len(sessions) == count:
                break
            sessions.append(token)
        return sessions

    def read_session_status(self, token):
        if token is None:
            return None
        session = self._read_from_cache(token)
        if session is None:
            session = self.load_session_info(token)
            if session is None:
                return None
        if session.test_state is None:
            session = self.load_session(token)
        if session is not None:
            self._push_to_cache(session)
        return session

    def read_public_sessions(self):
        self.load_all_sessions_info()
        session_tokens = []
        for token in self._sessions:
            session = self._sessions[token]
            if not session.is_public:
                continue
            session_tokens.append(token)

        return session_tokens

    def update_session(self, session):
        self._push_to_cache(session)

    def update_session_configuration(
        self, token, tests, test_types, timeouts, reference_tokens, type
    ):
        session = self.read_session(token)
        if session is None:
            raise NotFoundException("Could not find session")
        if session.status != PENDING:
            return

        if tests is not None:
            if "include" not in tests:
                tests["include"] = session.tests["include"]
            if "exclude" not in tests:
                tests["exclude"] = session.tests["exclude"]
            if reference_tokens is None:
                reference_tokens = session.reference_tokens
            if test_types is None:
                test_types = session.test_types

            pending_tests = self._test_loader.get_tests(
                include_list=tests["include"],
                exclude_list=tests["exclude"],
                reference_tokens=reference_tokens,
                test_types=test_types
            )
            session.pending_tests = pending_tests
            session.tests = tests
            test_files_count = self._tests_manager.calculate_test_files_count(
                pending_tests)
            test_state = {}
            for api in test_files_count:
                test_state[api] = {
                    "pass": 0,
                    "fail": 0,
                    "timeout": 0,
                    "not_run": 0,
                    "total": test_files_count[api],
                    "complete": 0,
                }
            session.test_state = test_state

        if test_types is not None:
            session.test_types = test_types
        if timeouts is not None:
            if AUTOMATIC not in timeouts:
                timeouts[AUTOMATIC] = session.timeouts[AUTOMATIC]
            if MANUAL not in timeouts:
                timeouts[MANUAL] = session.timeouts[MANUAL]
            session.timeouts = timeouts
        if reference_tokens is not None:
            session.reference_tokens = reference_tokens
        if type is not None:
            session.type = type

        self._push_to_cache(session)
        return session

    def update_labels(self, token, labels):
        if token is None or labels is None:
            return
        session = self.read_session(token)
        if session is None:
            return
        if session.is_public:
            return
        session.labels = labels
        self._push_to_cache(session)

    def delete_session(self, token):
        session = self.read_session(token)
        if session is None:
            return
        if session.is_public is True:
            return
        del self._sessions[token]

    def add_session(self, session):
        if session is None:
            return
        self._push_to_cache(session)

    def load_all_sessions(self):
        if not os.path.isdir(self._results_directory):
            return
        tokens = os.listdir(self._results_directory)
        for token in tokens:
            self.load_session(token)

    def load_all_sessions_info(self):
        if not os.path.isdir(self._results_directory):
            return
        tokens = os.listdir(self._results_directory)
        for token in tokens:
            if token in self._sessions:
                continue
            self.load_session_info(token)

    def load_session(self, token):
        session = self.load_session_info(token)
        if session is None:
            return None

        if session.test_state is None:
            results = self._results_manager.load_results(token)
            test_state = self._results_manager.parse_test_state(results)
            session.test_state = test_state
            self._results_manager.create_info_file(session)

        self._push_to_cache(session)
        return session

    def load_session_info(self, token):
        result_directory = os.path.join(self._results_directory, token)
        if not os.path.isdir(result_directory):
            return None
        info_file = os.path.join(result_directory, "info.json")
        if not os.path.isfile(info_file):
            return None

        info_data = None
        with open(info_file) as file:
            info_data = file.read()
        parsed_info_data = json.loads(info_data)

        session = deserialize_session(parsed_info_data)
        self._push_to_cache(session)
        return session

    def _push_to_cache(self, session):
        self._sessions[session.token] = session

    def _read_from_cache(self, token):
        if token not in self._sessions:
            return None
        return self._sessions[token]

    def _set_expiration_timer(self):
        expiring_sessions = self._read_expiring_sessions()
        if len(expiring_sessions) == 0:
            return

        next_session = expiring_sessions[0]
        for session in expiring_sessions:
            if next_session.expiration_date > session.expiration_date:
                next_session = session

        if self._expiration_timeout is not None:
            self._expiration_timeout.cancel()

        timeout = next_session.expiration_date / 1000 - time.time()
        if timeout < 0:
            timeout = 0

        def handle_timeout(self):
            self._delete_expired_sessions()
            self._set_expiration_timer()

        self._expiration_timeout = Timer(timeout, handle_timeout, [self])
        self._expiration_timeout.start()

    def _delete_expired_sessions(self):
        expiring_sessions = self._read_expiring_sessions()
        now = int(time.time() * 1000)

        for session in expiring_sessions:
            if session.expiration_date < now:
                self.delete_session(session.token)

    def _read_expiring_sessions(self):
        expiring_sessions = []
        for token in self._sessions:
            session = self._sessions[token]
            if session.expiration_date is None:
                continue
            expiring_sessions.append(session)
        return expiring_sessions

    def start_session(self, token):
        session = self.read_session(token)

        if session is None:
            return

        if session.status != PENDING and session.status != PAUSED:
            return

        if session.status == PENDING:
            session.date_started = int(time.time() * 1000)
            session.expiration_date = None

        session.status = RUNNING
        self.update_session(session)

        self._event_dispatcher.dispatch_event(
            token,
            event_type=STATUS_EVENT,
            data=session.status
        )

    def pause_session(self, token):
        session = self.read_session(token)
        if session.status != RUNNING:
            return
        session.status = PAUSED
        self.update_session(session)
        self._event_dispatcher.dispatch_event(
            token,
            event_type=STATUS_EVENT,
            data=session.status
        )
        self._results_manager.persist_session(session)

    def stop_session(self, token):
        session = self.read_session(token)
        if session.status == ABORTED or session.status == COMPLETED:
            return
        session.status = ABORTED
        session.date_finished = int(time.time() * 1000)
        self.update_session(session)
        self._event_dispatcher.dispatch_event(
            token,
            event_type=STATUS_EVENT,
            data=session.status
        )

    def resume_session(self, token, resume_token):
        session = self.read_session(token)
        if session.status != PENDING:
            return
        self._event_dispatcher.dispatch_event(
            token,
            event_type=RESUME_EVENT,
            data=resume_token
        )
        self.delete_session(token)

    def complete_session(self, token):
        session = self.read_session(token)
        if session.status == COMPLETED or session.status == ABORTED:
            return
        session.status = COMPLETED
        session.date_finished = int(time.time() * 1000)
        self.update_session(session)
        self._event_dispatcher.dispatch_event(
            token,
            event_type=STATUS_EVENT,
            data=session.status
        )

    def test_in_session(self, test, session):
        return self._test_list_contains_test(test, session.pending_tests) \
            or self._test_list_contains_test(test, session.running_tests)

    def is_test_complete(self, test, session):
        return not self._test_list_contains_test(test, session.pending_tests) \
            and not self._test_list_contains_test(test, session.running_tests)

    def is_test_running(self, test, session):
        return self._test_list_contains_test(test, session.running_tests)

    def _test_list_contains_test(self, test, test_list):
        for api in list(test_list.keys()):
            if test in test_list[api]:
                return True
        return False

    def is_api_complete(self, api, session):
        return api not in session.pending_tests \
            and api not in session.running_tests

    def get_test_path_with_query(self, test, session):
        query_string = ""
        include_list = session.tests["include"]
        for include_test in include_list:
            split = include_test.split("?")
            query = ""
            if len(split) > 1:
                include_test = split[0]
                query = split[1]
            pattern = re.compile("^" + include_test)
            if pattern.match(test) is not None:
                query_string += query + "&"
        return f"{test}?{query_string}"

    def find_token(self, fragment):
        if len(fragment) < 8:
            return None
        tokens = []
        for token in self._sessions:
            if token.startswith(fragment):
                tokens.append(token)
        if len(tokens) != 1:
            return None
        return tokens[0]

    def get_total_sessions(self):
        return len(self._sessions)
