import os

from parallels.core.utils.json_utils import read_json, write_json


class StatusStorage(object):
    OPERATION_CONNECT = 'connect'
    OPERATION_SCAN_CONTENT = 'scan-content'

    STATUS_QUEUED = 'queued'
    STATUS_CANCELLED = 'cancelled'
    STATUS_IN_PROGRESS = 'in-progress'
    STATUS_SUCCESS = 'success'
    STATUS_FAILURE = 'failure'
    STATUS_WARNING = 'warning'

    ISSUE_ID_FAILED_TO_DETECT_DOCROOT = 'failed-to-detect-docroot'
    ISSUE_ID_FTPS_CONNECT_FAILED = 'ftps-connect-failed'
    ISSUE_ID_SECURE_CONNECT_FAILED = 'secure-connect-failed'
    ISSUE_ID_INSECURE_HTTP_DETECTED = 'insecure-http-detected'
    ISSUE_ID_INSECURE_HTTPS_DETECTED = 'insecure-https-detected'

    STORAGE_STATUS_TYPE_COMMON_STATUS = 'status'
    STORAGE_STATUS_TYPE_CHECK_STATUS = 'check-status'

    STORAGE_ISSUES_TYPE_COMMON_ISSUES = 'issues'
    STORAGE_ISSUES_TYPE_CHECK_ISSUES = 'check-issues'

    def __init__(self, filename, site_name):
        """
        :type filename: parallels.core.session_files.SessionFile
        """
        self._filename = filename
        self._site_name = site_name

    def is_operation_successful(self, operation):
        """
        :type operation: str | unicode
        :rtype: None
        """
        data = self._load()
        return (
            data.get(self._site_name, {}).get('operation', {}).get(operation, {}).get('status') == self.STATUS_SUCCESS
        )

    def reset_operations_status(self):
        data = self._load()
        if self._site_name in data and 'operation' in data[self._site_name]:
            del data[self._site_name]['operation']
        self._save(data)

    def set_operation_success(self, operation):
        """
        :type operation: str | unicode
        :rtype: None
        """
        data = self._load()
        self._init_operation_status(data, operation)
        data[self._site_name]['operation'][operation]['status'] = self.STATUS_SUCCESS
        self._save(data)

    def set_operation_failure(self, operation, message, issue_id=None):
        """
        :type operation: str | unicode
        :type message: str | unicode
        :type issue_id: str | unicode
        :rtype: None
        """
        data = self._load()
        self._init_operation_status(data, operation)
        data[self._site_name]['operation'][operation]['status'] = self.STATUS_FAILURE
        data[self._site_name]['operation'][operation]['message'] = message
        if issue_id is not None:
            data[self._site_name]['operation'][operation]['issue_id'] = issue_id

        self._save(data)

    def set_application_status(self, application_type, source_path, target_path, status, status_type=None):
        """
        :type application_type: str | unicode
        :type source_path: str | unicode
        :type target_path:
        :type status:
        :type status_type: str | unicode | None
        :rtype: None
        """
        if status_type is None:
            status_type = self.STORAGE_STATUS_TYPE_COMMON_STATUS

        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'applications' not in data[self._site_name]:
            data[self._site_name]['applications'] = []

        found_application = False
        for application_info in data[self._site_name]['applications']:
            if application_info['type'] == application_type and application_info['source_path'] == source_path:
                found_application = True
                application_info['target_path'] = target_path
                application_info[status_type] = status
                break

        if not found_application:
            data[self._site_name]['applications'].append({
                'type': application_type,
                'source_path': source_path,
                'target_path': target_path,
                status_type: status
            })
        self._save(data)

    def add_application_issue(self, application_type, source_path, issue, issue_type=None):
        """
        :type application_type: str | unicode
        :type source_path: str | unicode
        :type issue: parallels.core.reports.model.issue.Issue
        :type issue_type: str | unicode | None
        :rtype: None
        """
        self.add_application_issues(application_type, source_path, [issue], issues_type=issue_type)

    def add_application_issues(self, application_type, source_path, issues, issues_type=None):
        """
        :type application_type: str | unicode
        :type source_path: str | unicode
        :type issues: list[parallels.core.reports.model.issue.Issue]
        :type issues_type: str | unicode | None
        :rtype: None
        """
        if issues_type is None:
            issues_type = self.STORAGE_ISSUES_TYPE_COMMON_ISSUES

        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'applications' not in data[self._site_name]:
            data[self._site_name]['applications'] = []

        found_application = None
        for application_info in data[self._site_name]['applications']:
            if application_info['type'] == application_type and application_info['source_path'] == source_path:
                found_application = application_info
                break

        if found_application:
            if not found_application.get(issues_type):
                found_application[issues_type] = []
            found_application[issues_type].extend([self._issue_as_dict(issue) for issue in issues])

        self._save(data)

    def update_application_list(self, current_application_paths):
        """Update list of applications in status file according to provided list - throw away all which do not exist

        :type current_application_paths: dict[str | unicode, list[str | unicode]]
        :rtype: None
        """
        data = self._load()

        if self._site_name not in data:
            return

        if 'applications' not in data[self._site_name]:
            return

        current_applications_set = set()
        for application_type, paths in current_application_paths.items():
            for path in paths:
                current_applications_set.add((application_type, path))

        data[self._site_name]['applications'] = [
            application_info
            for application_info in data[self._site_name]['applications']
            if (application_info['type'], application_info['source_path']) in current_applications_set
        ]

        self._save(data)

    def set_files_status(self, source_path, target_path, status):
        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'files' not in data[self._site_name]:
            data[self._site_name]['files'] = []

        found_file = False
        for file_info in data[self._site_name]['files']:
            if file_info['source_path'] == source_path:
                found_file = True
                file_info['target_path'] = target_path
                file_info['status'] = status
                break

        if not found_file:
            data[self._site_name]['files'].append({
                'source_path': source_path,
                'target_path': target_path,
                'status': status
            })
        self._save(data)

    def update_files_list(self, root_path, files_tree):
        """Update list of folders in status file according to provided list - throw away all which do not exist

        :type root_path: str | unicode
        :type files_tree: list
        :rtype: None
        """
        def get_dirs_list(path, tree):
            dirs_list = []
            for item in tree:
                if isinstance(item, dict):  # directory
                    for subdir_name, subtree in item.items():
                        subdir_path = path + os.sep + subdir_name
                        dirs_list.append(subdir_path)
                        dirs_list.extend(get_dirs_list(subdir_path, subtree))

            return dirs_list

        data = self._load()

        if self._site_name not in data:
            return

        if 'files' not in data[self._site_name]:
            return

        dirs_list = set(get_dirs_list(root_path, files_tree))

        data[self._site_name]['files'] = [
            file_info
            for file_info in data[self._site_name]['files']
            if file_info['source_path'] in dirs_list
        ]

        self._save(data)

    def add_folder_issue(self, source_path, issue):
        """
        :type source_path: str | unicode
        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: None
        """
        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'files' not in data[self._site_name]:
            data[self._site_name]['files'] = []

        found_file = None
        for file_info in data[self._site_name]['files']:
            if file_info['source_path'] == source_path:
                found_file = file_info
                break

        if found_file:
            if not found_file.get('issues'):
                found_file['issues'] = []
            found_file['issues'].append(self._issue_as_dict(issue))

        self._save(data)

    def set_database_status(self, name, host, port, username, password, status):
        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'databases' not in data[self._site_name]:
            data[self._site_name]['databases'] = []

        found_database = False
        for database in data[self._site_name]['databases']:
            if database['name'] == name and database['host'] == host and database['port'] == port:
                found_database = True
                database['username'] = username
                database['password'] = password
                database['status'] = status

        if not found_database:
            data[self._site_name]['databases'].append({
                'name': name,
                'host': host,
                'port': port,
                'username': username,
                'password': password,
                'status': status
            })

        self._save(data)

    def add_database_issue(self, name, host, port, issue):
        """
        :type name: str | unicode
        :type host: str | unicode
        :type port: str | unicode
        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: None
        """
        data = self._load()

        if self._site_name not in data:
            data[self._site_name] = {}

        if 'databases' not in data[self._site_name]:
            data[self._site_name]['databases'] = []

        found_database = None
        for database in data[self._site_name]['databases']:
            if database['name'] == name and database['host'] == host and database['port'] == port:
                found_database = database
                break

        if found_database:
            if not found_database.get('issues'):
                found_database['issues'] = []
            found_database['issues'].append(self._issue_as_dict(issue))

        self._save(data)

    @staticmethod
    def _issue_as_dict(issue):
        """Convert issue object to dictionary, saving limited list of fields - which are actually needed for GUI

        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: dict
        """
        return dict(
            execution_date=issue.execution_date,
            severity=issue.severity,
            problem_text=issue.problem_text,
            details_text=issue.details_text
        )

    def _init_operation_status(self, data, operation):
        if self._site_name not in data:
            data[self._site_name] = {}

        if 'operation' not in data[self._site_name]:
            data[self._site_name]['operation'] = {}

        if operation not in data[self._site_name]['operation']:
            data[self._site_name]['operation'][operation] = {
                'status': None,
                'message': None
            }

    def _save(self, data):
        write_json(self._filename.path, data, pretty_print=True)

    def _load(self):
        data = read_json(self._filename.path)
        if data is not None:
            return data
        else:
            return {}
