# ~*~ coding: utf-8 ~*~
import json
import os
import re
import shlex
import sys
from collections import defaultdict

from django.utils.translation import gettext as _

from assets import const

__all__ = ['JMSInventory']


def shell_join(args):
    return ' '.join(shlex.quote(str(arg)) for arg in args)


def escape_ssh_config_percent(value):
    return str(value).replace('%', '%%')


class JMSInventory:
    def __init__(
            self, assets, account_policy='privileged_first',
            account_prefer='root,Administrator', host_callback=None,
            exclude_localhost=False, task_type=None, protocol=None
    ):
        """
        :param assets:
        :param account_prefer: account username name if not set use account_policy
        :param account_policy: privileged_only, privileged_first, skip
        """
        self.assets = self.clean_assets(assets)
        self.account_prefer = self.get_account_prefer(account_prefer)
        self.account_policy = account_policy
        self.host_callback = host_callback
        self.exclude_hosts = {}
        self.exclude_localhost = exclude_localhost
        self.task_type = task_type
        self.protocol = protocol

    @staticmethod
    def clean_assets(assets):
        from assets.models import Asset
        asset_ids = [asset.id for asset in assets]
        assets = Asset.objects.filter(id__in=asset_ids, is_active=True) \
            .prefetch_related('platform', 'zone', 'accounts')
        return assets

    @staticmethod
    def get_username(asset, account):
        if asset.category == const.Category.DS:
            return account.full_username
        return account.username

    @staticmethod
    def group_by_platform(assets):
        groups = defaultdict(list)
        for asset in assets:
            groups[asset.platform].append(asset)
        return groups

    @staticmethod
    def get_gateway_ssh_settings(gateway):
        platform = gateway.platform
        try:
            protocol = platform.protocols.get(name='ssh')
        except platform.protocols.model.DoesNotExist:
            return {}
        return protocol.setting

    def make_proxy_command(self, gateway, path_dir):
        gateway_target = "{}@{}".format(
            escape_ssh_config_percent(gateway.username),
            escape_ssh_config_percent(gateway.address),
        )
        proxy_command_args = [
            "ssh", "-o", "Port={}".format(escape_ssh_config_percent(gateway.port)),
            "-o", "StrictHostKeyChecking=no",
        ]

        if gateway.private_key:
            proxy_command_args.extend([
                "-i", escape_ssh_config_percent(gateway.get_private_key_path(path_dir))
            ])

        setting = self.get_gateway_ssh_settings(gateway)
        if setting.get('nc', False):
            proxy_command_args.extend(["--", gateway_target, "nc", "-w", "10", "%h", "%p"])
        else:
            proxy_command_args.extend(["-W", "%h:%p", "-q", "--", gateway_target])

        if gateway.password:
            password = escape_ssh_config_percent(gateway.password)
            proxy_command_args = ["sshpass", "-p", password, *proxy_command_args]

        proxy_command_value = "ProxyCommand=" + shell_join(proxy_command_args)
        return {"ansible_ssh_common_args": "-o " + shlex.quote(proxy_command_value)}

    def make_account_ansible_vars(self, asset, account, path_dir):
        username = self.get_username(asset, account)
        var = {
            'ansible_user': username,
        }
        if not account.secret:
            return var

        if account.secret_type == 'password':
            var['ansible_password'] = account.escape_jinja2_syntax(account.secret)
        elif account.secret_type == 'ssh_key':
            var['ansible_ssh_private_key_file'] = account.get_private_key_path(path_dir)
        return var

    @staticmethod
    def make_custom_become_ansible_vars(account, su_from_auth, path_dir):
        su_method = su_from_auth['ansible_become_method']
        var = {
            'jms_custom_become': True,
            'jms_custom_become_method': su_method,
            'jms_custom_become_user': account.su_from.username,
            'jms_custom_become_password': account.escape_jinja2_syntax(account.su_from.secret),
            'jms_custom_become_private_key_path': account.su_from.get_private_key_path(path_dir)
        }
        return var

    @staticmethod
    def make_protocol_setting_vars(host, protocols):
        # 针对 ssh sqlserver 协议的特殊处理
        for p in protocols:
            if p.name == 'ssh':
                if hasattr(p, 'setting'):
                    setting = getattr(p, 'setting')
                    host['jms_asset']['old_ssh_version'] = setting.get('old_ssh_version', False)
            if p.name == 'sqlserver':
                if hasattr(p, 'setting'):
                    setting = getattr(p, 'setting')
                    encryption = setting.get('encrypt', True)
                    version = setting.get('version', ">=2014")
                    if version == '<2014':
                        host['jms_asset']['tds_version'] = '7.0'
                    if not encryption:
                        host['jms_asset']['encryption'] = 'off'

    def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway, path_dir,
                          ansible_config):
        from accounts.const import AutomationTypes
        if not account:
            host['error'] = _("No account available")
            return host

        port = protocol.port if protocol else 22
        host['ansible_host'] = asset.address
        host['ansible_port'] = port

        su_from = account.su_from
        if platform.su_enabled and su_from:
            su_from_auth = account.get_ansible_become_auth()
            host.update(su_from_auth)
            host.update(self.make_custom_become_ansible_vars(account, su_from_auth, path_dir))
        elif platform.su_enabled and not su_from and \
                self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account):
            host.update(self.make_account_ansible_vars(asset, account, path_dir))
            if platform.type not in ["windows", "windows_ad"]:
                host['ansible_become'] = True
            host['ansible_become_user'] = 'root'
            host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret)
        else:
            host.update(self.make_account_ansible_vars(asset, account, path_dir))

        if platform.is_huawei():
            host['ansible_connection'] = 'network_cli'
            host['ansible_network_os'] = 'ce'

        if gateway:
            ansible_connection = host.get('ansible_connection', 'ssh')
            if ansible_connection in ('local', 'winrm', 'rdp'):
                host['jms_gateway'] = {
                    'address': gateway.address, 'port': gateway.port,
                    'username': gateway.username, 'secret': gateway.password,
                    'private_key_path': gateway.get_private_key_path(path_dir)
                }
                host['jms_asset']['port'] = port
            else:
                ansible_ssh_common_args = self.make_proxy_command(gateway, path_dir)
                host['jms_asset'].update(ansible_ssh_common_args)
                host.update(ansible_ssh_common_args)

    def get_primary_protocol(self, ansible_config, protocols):
        invalid_protocol = type('protocol', (), {'name': 'null', 'port': 0})
        ansible_connection = ansible_config.get('ansible_connection')
        # 数值越小，优先级越高，若用户在 ansible_config 中配置了，则提高用户配置方式的优先级
        protocol_priority = {'ssh': 10, 'winrm': 9, ansible_connection: 1}
        if self.protocol:
            protocol_priority.update({self.protocol: 0})
        protocol_sorted = sorted(protocols, key=lambda x: protocol_priority.get(x.name, 999))
        protocol = protocol_sorted[0] if protocol_sorted else invalid_protocol
        return protocol

    @staticmethod
    def fill_ansible_config(ansible_config, protocol):
        if protocol.name in ('ssh', 'winrm', 'rdp'):
            ansible_config['ansible_connection'] = protocol.name
        if protocol.name == 'winrm':
            if protocol.setting.get('use_ssl', False):
                ansible_config['ansible_winrm_scheme'] = 'https'
                ansible_config['ansible_winrm_transport'] = 'ssl'
                ansible_config['ansible_winrm_server_cert_validation'] = 'ignore'
            else:
                ansible_config['ansible_winrm_scheme'] = 'http'
                ansible_config['ansible_winrm_transport'] = 'ntlm'
            ansible_config['ansible_winrm_connection_timeout'] = 120
        return ansible_config

    def asset_to_host(self, asset, account, automation, protocols, platform, path_dir):
        name = re.sub(r'[ \[\]/]', '_', asset.name)
        host = {'name': name}
        if account is None:
            host['error'] = _('No account available')
            return host

        try:
            ansible_config = dict(automation.ansible_config)
        except (AttributeError, TypeError):
            ansible_config = {}

        protocol = self.get_primary_protocol(ansible_config, protocols)

        tp, category = asset.type, asset.category

        secret_info = {k: v for k, v in asset.secret_info.items() if v}
        username = self.get_username(asset, account)
        host.update({
            'local_python_interpreter': sys.executable,
            'jms_asset': {
                'id': str(asset.id), 'name': asset.name, 'address': asset.address,
                'type': tp, 'category': category,
                'protocol': protocol.name, 'port': protocol.port,
                'spec_info': asset.spec_info, 'secret_info': secret_info,
                'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
                'origin_address': asset.address
            },
            'jms_account': {
                'id': str(account.id),
                'username': username,
                'secret': account.escape_jinja2_syntax(account.secret),
                'secret_type': account.secret_type, 'private_key_path': account.get_private_key_path(path_dir)
            } if account else None
        })

        self.make_protocol_setting_vars(host, protocols)
        protocols = host['jms_asset']['protocols']
        host['jms_asset'].update({f"{p['name']}_port": p['port'] for p in protocols})
        if host['jms_account'] and tp == 'oracle':
            host['jms_account']['mode'] = 'sysdba' if account.privileged else None

        ansible_config = self.fill_ansible_config(ansible_config, protocol)
        host.update(ansible_config)

        gateway = None
        if not asset.is_gateway and asset.zone:
            gateway = asset.zone.select_gateway()

        self.make_account_vars(
            host, asset, account, automation, protocol, platform, gateway, path_dir, ansible_config
        )
        return host

    @staticmethod
    def sorted_accounts(accounts):
        connectivity_score = {'ok': 2, '-': 1, 'err': 0}
        sort_key = lambda x: (x.privileged, connectivity_score.get(x.connectivity, 0), x.date_updated)
        accounts_sorted = sorted(accounts, key=sort_key, reverse=True)
        return accounts_sorted

    def get_asset_sorted_accounts(self, asset):
        accounts = list(asset.all_accounts.filter(is_active=True))
        accounts_sorted = self.sorted_accounts(accounts)
        return accounts_sorted

    @staticmethod
    def get_account_prefer(account_prefer):
        account_usernames = []
        if isinstance(account_prefer, str) and account_prefer:
            account_usernames = list(map(lambda x: x.lower(), account_prefer.split(',')))
        return account_usernames

    def get_refer_account(self, accounts):
        account = None
        if accounts:
            account = list(filter(
                lambda a: a.username.lower() in self.account_prefer, accounts
            ))
            account = account[0] if account else None
        return account

    def select_account(self, asset):
        accounts = self.get_asset_sorted_accounts(asset)
        if not accounts:
            return None

        refer_account = self.get_refer_account(accounts)
        if refer_account:
            return refer_account

        account_selected = accounts[0]
        if self.account_policy == 'skip':
            return None
        elif self.account_policy == 'privileged_first':
            return account_selected
        elif self.account_policy == 'privileged_only' and account_selected.privileged:
            return account_selected
        else:
            return None

    @staticmethod
    def set_platform_protocol_setting_to_asset(asset, platform_protocols):
        asset_protocols = asset.protocols.all()
        for p in asset_protocols:
            setattr(p, 'setting', platform_protocols.get(p.name, {}))
        return asset_protocols

    def get_classified_hosts(self, path_dir):
        hosts = []
        platform_assets = self.group_by_platform(self.assets)
        runnable_hosts = []
        error_hosts = []

        for platform, assets in platform_assets.items():
            automation = platform.automation
            platform_protocols = {
                p['name']: p['setting'] for p in platform.protocols.values('name', 'setting')
            }
            for asset in assets:
                protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
                account = self.select_account(asset)
                host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)

                if not automation.ansible_enabled:
                    host['error'] = _('Ansible disabled')

                if isinstance(host, list):
                    hosts.extend(host)
                else:
                    hosts.append(host)

        # 分类主机
        for host in hosts:
            if host.get('error'):
                self.exclude_hosts[host['name']] = host['error']
                error_hosts.append({
                    'name': host['name'],
                    'id': host.get('jms_asset', {}).get('id'),
                    'error': host['error']
                })
            else:
                runnable_hosts.append({
                    'name': host['name'],
                    'ip': host.get('ansible_host', ''),
                    'id': host.get('jms_asset', {}).get('id')
                })
        result = {
            'runnable': runnable_hosts,
            'error': error_hosts,
        }
        return result

    def generate(self, path_dir):
        hosts = []
        platform_assets = self.group_by_platform(self.assets)
        for platform, assets in platform_assets.items():
            automation = platform.automation
            platform_protocols = {
                p['name']: p['setting'] for p in platform.protocols.values('name', 'setting')
            }
            for asset in assets:
                protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
                account = self.select_account(asset)
                host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)

                if not automation.ansible_enabled:
                    host['error'] = _('Ansible disabled')

                if self.host_callback is not None:
                    host = self.host_callback(
                        host, asset=asset, account=account,
                        platform=platform, automation=automation,
                        path_dir=path_dir
                    )

                if isinstance(host, list):
                    hosts.extend(host)
                else:
                    hosts.append(host)

        exclude_hosts = list(filter(lambda x: x.get('error'), hosts))
        if exclude_hosts:
            print(_("Skip hosts below:"))
            for i, host in enumerate(exclude_hosts, start=1):
                print("{}: [{}] \t{}".format(i, host['name'], host['error']))
                self.exclude_hosts[host['name']] = host['error']
        hosts = list(filter(lambda x: not x.get('error'), hosts))
        data = {'all': {'hosts': {}}}
        for host in hosts:
            name = host.pop('name')
            data['all']['hosts'][name] = host
        if not self.exclude_localhost:
            data['all']['hosts'].update({
                'localhost': {
                    'ansible_host': '127.0.0.1',
                    'ansible_connection': 'local'
                }
            })
        return data

    def write_to_file(self, path):
        path_dir = os.path.dirname(path)
        if not os.path.exists(path_dir):
            os.makedirs(path_dir, 0o700, True)
        data = self.generate(path_dir)
        with open(path, 'w') as f:
            f.write(json.dumps(data, indent=4))
