import uuid
from copy import deepcopy

from django.db import IntegrityError
from django.db import transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.generics import get_object_or_404
from rest_framework.validators import UniqueTogetherValidator

from accounts.const import SecretType, Source, AccountInvalidPolicy
from accounts.models import Account, AccountTemplate, GatheredAccount
from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes
from assets.models import Asset
from common.serializers import SecretReadableMixin, SecretReadableCheckMixin, CommonBulkModelSerializer
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger
from accounts.utils import validate_account_username
from .base import BaseAccountSerializer, AuthValidateMixin

logger = get_logger(__name__)


class AccountCreateUpdateSerializerMixin(serializers.Serializer):
    template = serializers.PrimaryKeyRelatedField(
        queryset=AccountTemplate.objects, required=False,
        label=_("Template"), write_only=True, allow_null=True
    )
    push_now = serializers.BooleanField(
        default=False, label=_("Push now"), write_only=True
    )
    params = serializers.JSONField(
        decoder=None, encoder=None, required=False,
        style={'base_template': 'textarea.html'},
        label=_('Params'),
    )
    on_invalid = LabeledChoiceField(
        choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
        write_only=True, allow_null=True, label=_('Exist policy'),
    )
    _template = None
    clean_auth_fields: callable

    class Meta:
        fields = ['template', 'push_now', 'params', 'on_invalid']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_initial_value()

    def set_initial_value(self):
        if not getattr(self, 'initial_data', None):
            return
        if isinstance(self.initial_data, dict):
            initial_data = [self.initial_data]
        else:
            initial_data = self.initial_data

        for data in initial_data:
            if not data.get('asset') and not self.instance:
                raise serializers.ValidationError({'asset': UniqueTogetherValidator.missing_message})
            asset = data.get('asset') or getattr(self.instance, 'asset', None)
            self.from_template_if_need(data)
            self.set_uniq_name_if_need(data, asset)

    def set_uniq_name_if_need(self, initial_data, asset):
        name = initial_data.get('name')
        if name is not None:
            return
        request = self.context.get('request')
        if request and request.method == 'PATCH':
            return
        if not name:
            name = initial_data.get('username')
        if self.instance and self.instance.name == name:
            return
        if Account.objects.filter(name=name, asset=asset).exists():
            name = name + '_' + uuid.uuid4().hex[:4]
        initial_data['name'] = name

    @staticmethod
    def get_template_attr_for_account(template):
        field_names = [
            'name', 'username',
            'secret_type', 'secret',
            'privileged', 'is_active'
        ]

        field_map = {
            'push_params': 'params',
            'auto_push': 'push_now'
        }

        field_names.extend(field_map.keys())

        attrs = {}
        for name in field_names:
            value = getattr(template, name, None)
            if value is None:
                continue

            attr_name = field_map.get(name, name)
            attrs[attr_name] = value

        attrs['secret'] = template.get_secret()
        return attrs

    def from_template_if_need(self, initial_data):
        if isinstance(initial_data, str):
            return

        template_id = initial_data.pop('template', None)
        if not template_id:
            return

        if isinstance(template_id, (str, uuid.UUID)):
            template = AccountTemplate.objects.filter(id=template_id).first()
        else:
            template = template_id
        if not template:
            raise serializers.ValidationError({'template': 'Template not found'})

        self._template = template
        attrs = self.get_template_attr_for_account(template)
        initial_data.update(attrs)
        initial_data.update({
            'source': Source.TEMPLATE,
            'source_id': str(template.id)
        })
        asset_id = initial_data.get('asset')
        if isinstance(asset_id, list) or not asset_id:
            return
        asset = get_object_or_404(Asset, pk=asset_id)
        initial_data['su_from'] = template.get_su_from_account(asset)

    def push_account_if_need(self, instance, push_now, params, stat):
        if not push_now or stat not in ['created', 'updated']:
            return
        transaction.on_commit(lambda: self.start_push(instance, params))

    @staticmethod
    def start_push(instance, params):
        push_accounts_to_assets_task.delay([str(instance.id)], params)

    def get_validators(self):
        _validators = super().get_validators()
        if getattr(self, 'initial_data', None) is None:
            return _validators

        on_invalid = self.initial_data.get('on_invalid')
        if on_invalid == AccountInvalidPolicy.ERROR and not self.parent:
            return _validators
        _validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
        return _validators

    @staticmethod
    def do_create(vd):
        on_invalid = vd.pop('on_invalid', None)

        q = Q()
        if vd.get('name'):
            q |= Q(name=vd['name'])
        if vd.get('username'):
            q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))

        instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
        # 不存在这个资产，不用关系策略
        if not instance:
            instance = Account.objects.create(**vd)
            return instance, 'created'

        if on_invalid == AccountInvalidPolicy.SKIP:
            return instance, 'skipped'
        elif on_invalid == AccountInvalidPolicy.UPDATE:
            for k, v in vd.items():
                setattr(instance, k, v)
            instance.save()
            return instance, 'updated'
        else:
            raise serializers.ValidationError(_('Account already exists'))

    def create(self, validated_data):
        push_now = validated_data.pop('push_now', None)
        params = validated_data.pop('params', None)
        self.clean_auth_fields(validated_data)
        instance, stat = self.do_create(validated_data)
        if instance.source == Source.LOCAL:
            self.push_account_if_need(instance, push_now, params, stat)
        return instance

    def update(self, instance, validated_data):
        # account cannot be modified
        validated_data.pop('username', None)
        validated_data.pop('on_invalid', None)
        push_now = validated_data.pop('push_now', None)
        params = validated_data.pop('params', None)
        validated_data['source_id'] = None
        instance = super().update(instance, validated_data)
        self.push_account_if_need(instance, push_now, params, 'updated')
        return instance


class AccountAssetSerializer(serializers.ModelSerializer):
    platform = ObjectRelatedField(read_only=True, attrs=('id', 'name', 'type'))
    category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
    type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))

    class Meta:
        model = Asset
        fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
        exclude_backup_fields = ['platform', 'auto_config']

    def to_internal_value(self, data):
        if isinstance(data, dict):
            i = data.get('id') or data.get('pk')
        else:
            i = data

        try:
            return Asset.objects.get(id=i)
        except Asset.DoesNotExist:
            raise serializers.ValidationError(_('Asset not found'))


class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
    asset = AccountAssetSerializer(label=_('Asset'))
    source = LabeledChoiceField(
        choices=Source.choices, label=_("Source"), required=False,
        allow_null=True, default=Source.LOCAL
    )
    su_from = ObjectRelatedField(
        required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
        label=_('Su from'), attrs=('id', 'name', 'username')
    )
    ds = ObjectRelatedField(read_only=True, label=_('Directory service'), attrs=('id', 'name', 'domain_name'))

    class Meta(BaseAccountSerializer.Meta):
        model = Account
        automation_fields = [
            'date_last_login', 'login_by', 'date_verified', 'connectivity',
            'date_change_secret', 'change_secret_status'
        ]
        fields = BaseAccountSerializer.Meta.fields + [
            'su_from', 'asset', 'version', 'ds',
            'source', 'source_id', 'secret_reset',
        ] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
        read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
        fields = [f for f in fields if f not in ['spec_info']]
        extra_kwargs = {
            **BaseAccountSerializer.Meta.extra_kwargs,
            'name': {'required': False},
            'source_id': {'required': False, 'allow_null': True},
        }
        fields_unimport_template = ['params']
        # 手动判断唯一性校验
        validators = []

    @classmethod
    def setup_eager_loading(cls, queryset):
        """ Perform necessary eager loading of data. """
        queryset = queryset.prefetch_related(
            'asset', 'asset__platform',
            'asset__platform__automation'
        )
        return queryset

    def validate(self, attrs):
        instance = getattr(self, "instance", None)
        if instance:
            return super().validate(attrs)

        field_errors = {}
        for _fields in Account._meta.unique_together:
            lookup = {field: attrs.get(field) for field in _fields}
            if Account.objects.filter(**lookup).exists():
                verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
                msg_template = _('Account already exists. Field(s): {fields} must be unique.')
                field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
                raise serializers.ValidationError(field_errors)
        return attrs


class AccountDetailSerializer(AccountSerializer):
    has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)

    class Meta(AccountSerializer.Meta):
        model = Account
        fields = AccountSerializer.Meta.fields + ['has_secret', 'spec_info']
        read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']


class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
    asset = serializers.CharField(read_only=True, label=_('Asset'))
    account = serializers.CharField(read_only=True, label=_('Account'))
    state = serializers.CharField(read_only=True, label=_('State'))
    error = serializers.CharField(read_only=True, label=_('Error'))
    changed = serializers.BooleanField(read_only=True, label=_('Changed'))


class AssetAccountBulkSerializer(
    AccountCreateUpdateSerializerMixin, AuthValidateMixin, CommonBulkModelSerializer
):
    su_from_username = serializers.CharField(
        max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"),
        allow_blank=True,
    )

    class Meta:
        model = Account
        fields = [
            'name', 'username', 'secret', 'secret_type', 'secret_reset',
            'passphrase', 'privileged', 'is_active', 'comment', 'template',
            'on_invalid', 'push_now', 'params',
            'su_from_username', 'source', 'source_id',
        ]
        extra_kwargs = {
            'name': {'required': False},
            'secret_type': {'required': False},
            'source': {'required': False, 'allow_null': True},
            'source_id': {'required': False, 'allow_null': True},
        }

    def set_initial_value(self):
        if not getattr(self, 'initial_data', None):
            return
        initial_data = self.initial_data
        self.from_template_if_need(initial_data)

    @staticmethod
    def validate_username(value):
        return validate_account_username(value)

    @staticmethod
    def get_filter_lookup(vd):
        return {
            'username': vd['username'],
            'secret_type': vd['secret_type'],
            'asset': vd['asset'],
        }

    @staticmethod
    def get_uniq_name(vd):
        return vd['name'] + '-' + uuid.uuid4().hex[:4]

    @staticmethod
    def _handle_update_create(vd, lookup):
        instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
        state = 'created' if value else 'updated'
        return instance, True, state

    @staticmethod
    def _handle_skip_create(vd, lookup):
        instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
        state = 'created' if value else 'skipped'
        return instance, value, state

    @staticmethod
    def _handle_err_create(vd, lookup):
        instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
        if not value:
            raise serializers.ValidationError(_('Account already exists'))
        return instance, True, 'created'

    def generate_su_from_data(self, validated_data):
        template = self._template
        asset = validated_data['asset']
        su_from = validated_data.get('su_from')
        su_from_username = validated_data.pop('su_from_username', None)
        if template:
            su_from = template.get_su_from_account(asset)
        elif su_from_username:
            su_from = asset.accounts.filter(username=su_from_username).first()
        validated_data['su_from'] = su_from

    def perform_create(self, vd, handler):
        lookup = self.get_filter_lookup(vd)
        vd = deepcopy(vd)
        self.generate_su_from_data(vd)
        try:
            instance, changed, state = handler(vd, lookup)
        except IntegrityError:
            vd['name'] = self.get_uniq_name(vd)
            instance, changed, state = handler(vd, lookup)
        return instance, changed, state

    def get_create_handler(self, on_invalid):
        if on_invalid == 'update':
            handler = self._handle_update_create
        elif on_invalid == 'skip':
            handler = self._handle_skip_create
        else:
            handler = self._handle_err_create
        return handler

    def perform_bulk_create(self, vd, assets):
        on_invalid = vd.pop('on_invalid', 'skip')
        secret_type = vd.get('secret_type', 'password')

        if not vd.get('name'):
            vd['name'] = vd.get('username')

        create_handler = self.get_create_handler(on_invalid)
        secret_type_supports = Asset.get_secret_type_assets(assets, secret_type)

        _results = {}
        for asset in assets:
            if asset not in secret_type_supports and asset.category != Category.CUSTOM:
                _results[asset] = {
                    'error': _('Asset does not support this secret type: %s') % secret_type,
                    'state': 'error',
                    'account': vd['name'],
                }
                continue

            vd = vd.copy()
            vd['asset'] = asset
            try:
                self.clean_auth_fields(vd)
                instance, changed, state = self.perform_create(vd, create_handler)
                _results[asset] = {
                    'changed': changed, 'instance': instance.id, 'state': state, 'account': vd['name']
                }
            except serializers.ValidationError as e:
                _results[asset] = {'error': e.detail[0], 'state': 'error', 'account': vd['name']}
            except Exception as e:
                logger.exception(e)
                _results[asset] = {'error': str(e), 'state': 'error', 'account': vd['name']}

        results = [{'asset': asset, **result} for asset, result in _results.items()]
        state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
        results = sorted(results, key=lambda x: state_score.get(x['state'], 4))

        if on_invalid != 'error':
            return results

        errors = []
        errors.extend([result for result in results if result['state'] == 'error'])
        for result in results:
            if result['state'] != 'skipped':
                continue
            errors.append({
                'error': _('Account has exist'),
                'state': 'error',
                'asset': str(result['asset']),
                'account': result.get('account'),
            })
        if errors:
            raise serializers.ValidationError(errors)
        return results

    @staticmethod
    def push_accounts_if_need(results, push_now, params):
        if not push_now:
            return

        account_ids = [v['instance'] for v in results if v.get('instance')]
        accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
        if not accounts.exists():
            return

        account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
        push_accounts_to_assets_task.delay(account_ids, params)

    def bulk_create(self, validated_data, assets):
        if not assets:
            raise serializers.ValidationError(
                {'assets': _('At least one asset or node must be specified')},
                {'nodes': _('At least one asset or node must be specified')}
            )

        params = validated_data.pop('params', None)
        push_now = validated_data.pop('push_now', False)
        results = self.perform_bulk_create(validated_data, assets)
        self.push_accounts_if_need(results, push_now, params)
        for res in results:
            res['asset'] = str(res['asset'])
        return results


class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountSerializer):
    spec_info = serializers.DictField(label=_('Spec info'), read_only=True)

    class Meta(AccountSerializer.Meta):
        fields = AccountSerializer.Meta.fields + ['spec_info']
        extra_kwargs = {
            **AccountSerializer.Meta.extra_kwargs,
            'secret': {'write_only': False},
            'spec_info': {'label': _('Spec info')},
        }
        exclude_backup_fields = [
            'passphrase', 'push_now', 'params', 'spec_info'
        ]
        secret_fields = ['secret']


class AccountHistorySerializer(SecretReadableCheckMixin, serializers.ModelSerializer):
    secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
    secret = serializers.CharField(label=_('Secret'), read_only=True)
    id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)

    class Meta:
        model = Account.history.model
        fields = [
            'id', 'secret', 'secret_type', 'version', 'history_date',
            'history_user'
        ]
        read_only_fields = fields
        extra_kwargs = {
            'history_user': {'label': _('User')},
            'history_date': {'label': _('Date')},
        }
        secret_fields = ['secret']


class AccountTaskSerializer(serializers.Serializer):
    ACTION_CHOICES = (
        ('verify', 'verify'),
        ('push', 'push'),
        ('remove', 'remove'),
    )
    action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
    assets = serializers.PrimaryKeyRelatedField(
        queryset=Asset.objects, required=False, allow_empty=True, many=True
    )
    accounts = serializers.PrimaryKeyRelatedField(
        queryset=Account.objects, required=False, allow_empty=True, many=True
    )
    gather_accounts = serializers.PrimaryKeyRelatedField(
        queryset=GatheredAccount.objects, required=False, allow_empty=True, many=True
    )
    task = serializers.CharField(read_only=True)
    params = serializers.JSONField(
        decoder=None, encoder=None, required=False,
        style={'base_template': 'textarea.html'}
    )
