import ntpath
import posixpath

from parallels.core import messages
from parallels.core.application.base import ApplicationModel, ApplicationInstance
from parallels.core.application.utils import split_by_start_end, replace_line_bounds, iter_dir_tree_full_file_paths
from parallels.core.utils.common import cached
from parallels.core.utils.common.ip import resolve_all_safe
from parallels.core.utils.database_utils import split_mssql_host_parts_str, join_mssql_hostname_and_instance
from parallels.core.utils.line_processor import LineProcessor, ChangedLineInfo, ReplaceResults


class AspNetModel(ApplicationModel):
    @property
    def name(self):
        return 'ASP.NET application'

    def get_instance(self, server, base_path):
        """Retrieve instance of ASP.NET application

        :type server: parallels.core.connections.server.Server
        :type base_path: str
        :rtype: parallels.core.application.asp_net.AspNetInstance
        """
        return AspNetInstance(server, base_path)

    def get_supported_database_types(self):
        return [self.DATABASE_TYPE_MSSQL]

    def _search_file(self, server, file_path, file_name):
        if self.is_asp_net_config_file(server, file_path, file_name):
            return self.get_instance(server, server.get_base_path(file_path))
        else:
            return None

    @classmethod
    def is_asp_net_config_file(cls, server, file_path, file_name):
        if not file_name.lower().endswith('.config'):
            return False
        file_contents = cls._safe_get_file_contents(server, file_path)
        if file_contents is None:
            return False
        file_contents_lower = file_contents.lower()
        if 'connectionstring' not in file_contents_lower and 'sqlserver' not in file_contents_lower:
            return False

        return True

    def _search_directory(self, server, directory_path, directory_name, directory_tree):
        return None


class AspNetInstance(ApplicationInstance):
    CONNECTION_STRING_MARKERS = ['connectionstring', 'sqlserver']
    DATABASE_HOSTNAME_MARKERS = ["Data Source", "Server", "Address", "Addr", "Network Address"]
    DATABASE_NAME_MARKERS = ['database', 'initial catalog']
    DATABASE_USER_NAME_MARKERS = ['uid', 'user id']

    @property
    def description(self):
        return messages.APPLICATION_DESCRIPTION_ASPNET.format(base_path=self.base_path)

    @property
    @cached
    def config_file_paths(self):
        """Get list of all paths to ".config" files of application

        :rtype: list[str | unicode]
        """
        file_paths = []

        with self.server.runner() as runner:
            for file_path in iter_dir_tree_full_file_paths(
                self.server, runner.get_dir_tree(self.base_path), self.base_path
            ):
                file_name = (ntpath if self.server.is_windows() else posixpath).basename(file_path)
                if AspNetModel.is_asp_net_config_file(self.server, file_path, file_name):
                    file_paths.append(file_path)

        return file_paths

    def fix_database_hostname(self, database_hostname_mapping):
        """Replace hostname in database connection string of application configuration files

        :type database_hostname_mapping: dict[str, str]
        :rtype: list[parallels.core.reports.model.issue.Issue]
        """
        issues = []
        for config_file_path in self.config_file_paths:
            with self._server.runner() as runner:
                config_file_contents = runner.get_file_contents(config_file_path)
            issues += self._replace_file_contents(
                config_file_path,
                self._get_database_hostname_replace_result(config_file_contents, database_hostname_mapping),
                messages.APPLICATION_REPLACE_FILE_CONTENT_REASON_FIX_DATABASE_HOSTNAME
            )
        return issues

    def fix_database_name(self, database_name_mapping):
        """Fix database name, when it changed during conflict resolution

        :type database_name_mapping: dict[str | unicode, str | unicode]
        :rtype: list[parallels.core.reports.model.issue.Issue]
        """
        issues = []
        for config_file_path in self.config_file_paths:
            with self._server.runner() as runner:
                config_file_contents = runner.get_file_contents(config_file_path)
            issues += self._replace_file_contents(
                config_file_path,
                self._get_database_name_replace_result(config_file_contents, database_name_mapping),
                messages.APPLICATION_REPLACE_FILE_CONTENT_REASON_FIX_DATABASE_NAME
            )
        return issues

    def fix_database_user_name(self, database_user_name_mapping):
        """Fix database user name, when it changed during conflict resolution

        :type database_user_name_mapping: dict[str | unicode, str | unicode]
        :rtype: list[parallels.core.reports.model.issue.Issue]
        """
        issues = []
        for config_file_path in self.config_file_paths:
            with self._server.runner() as runner:
                config_file_contents = runner.get_file_contents(config_file_path)
            issues += self._replace_file_contents(
                config_file_path,
                self._get_database_user_name_replace_result(config_file_contents, database_user_name_mapping),
                messages.APPLICATION_REPLACE_FILE_CONTENT_REASON_FIX_DATABASE_USER_NAME
            )
        return issues

    def _get_database_hostname_replace_result(self, config_file_contents, database_hostname_mapping):
        """Replace database hostnames in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_hostname_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        processor = LineProcessor(config_file_contents)
        changed_lines = []

        for line in processor.iter_lines():
            line_contents_lower = line.contents.lower()
            if 'connectionstring' in line_contents_lower or 'sqlserver' in line_contents_lower:
                split_result = self._split_hostname(line.contents)
                if split_result is None:
                    continue

                before, hostname, after = split_result

                if hostname in database_hostname_mapping and database_hostname_mapping[hostname] != hostname:
                    old_line_contents = line.contents
                    line.contents = before + database_hostname_mapping[hostname] + after
                    changed_lines.append(ChangedLineInfo(old_line_contents, line.contents, line.number))
                else:
                    mssql_host_parts = split_mssql_host_parts_str(hostname, can_be_incorrect=True)
                    if mssql_host_parts is None:
                        continue

                    host, instance, port = mssql_host_parts
                    ips = resolve_all_safe(host)
                    for ip in ips:
                        full_name = join_mssql_hostname_and_instance(ip, instance, port)
                        if full_name in database_hostname_mapping and database_hostname_mapping[full_name] != hostname:
                            old_line_contents = line.contents
                            line.contents = before + database_hostname_mapping[full_name] + after
                            changed_lines.append(ChangedLineInfo(old_line_contents, line.contents, line.number))

        return ReplaceResults(processor.serialize(), changed_lines)

    def _get_database_name_replace_result(self, config_file_contents, database_name_mapping):
        """Replace database names in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_name_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        return replace_line_bounds(
            config_file_contents,
            condition_strs=self.CONNECTION_STRING_MARKERS,
            start_strs=self._append_suffix(self.DATABASE_NAME_MARKERS, '='),
            end_str=';',
            replace_mapping=database_name_mapping
        )

    def _get_database_user_name_replace_result(self, config_file_contents, database_user_name_mapping):
        """Replace database user names in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_user_name_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        return replace_line_bounds(
            config_file_contents,
            condition_strs=self.CONNECTION_STRING_MARKERS,
            start_strs=self._append_suffix(self.DATABASE_USER_NAME_MARKERS, '='),
            end_str=';',
            replace_mapping=database_user_name_mapping
        )

    @classmethod
    def _split_hostname(cls, line):
        """Split line into 3 parts: before database hostname, database hostname, after database hostname

        For example, if configuration file line is:
        connectionString="Server=192.168.1.1\MSSQL2005;Database=DotNetNuke_1;uid=DotNetNuke_e;pwd=123qwe;"
        then this function will return
        ('connectionString="Server=', '192.168.1.1\MSSQL2005', ';Database=DotNetNuke_1;uid=DotNetNuke_e;pwd=123qwe;')

        This function considers that database hostname could be specified by different parameters:
        "Data Source", "Server", and so on.

        :type line: str | unicode
        :rtype: tuple[str | unicode]
        """
        for start_str in cls._append_suffix(cls.DATABASE_HOSTNAME_MARKERS, '='):
            split_results = split_by_start_end(
                start_str=start_str, end_str=';', data_str=line, case_insensitive=True
            )
            if split_results is not None:
                return split_results

        return None

    @staticmethod
    def _append_suffix(strs, suffix):
        return [s + suffix for s in strs]