# -*- coding: utf-8 -*-
from collections import defaultdict
from flask import Response, request, jsonify, current_app, abort

from saas.tools.abcd.app.blueprint_tvm import BlueprintTvm
from saas.tools.abcd.models.models import db, Account, Provision, Author, Operation, Segment, ResourceType, Unit
from saas.tools.abcd.models.database_handles import get_or_create, get_instance, create_or_update


saas_abcd_api = BlueprintTvm('saas_abcd_provider_api', __name__)
content_type = 'application/json; charset=utf-8'

# REST API Description
# https://wiki.yandex-team.ru/resource-model/providers/providerifacespec/#restapiprovajjdera


def _get_request_args():
    args = request.get_json(force=True)
    current_app.logger.info('Request args: %s', args)
    return args


def _provider_check(provider_id):
    saas_provider_ids = current_app.config['SAAS_PROVIDER_IDS']
    if provider_id not in saas_provider_ids:
        current_app.logger.error('Error matching SaaS provider id, there is %s, expected value - %s',
                                 provider_id, saas_provider_ids)
        abort(400)


def _account_check(account_id):
    if account_id == 'None' or account_id is None:
        current_app.logger.error('Account ID cannot be None')
        abort(404)


def _response(response_data):
    current_app.logger.debug('Response data: %s', response_data)
    return jsonify(response_data)


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts', methods=['POST'])
def createAccount(provider_id):
    """
    Create account handle.
    :param provider_id: type UUID
    """
    _provider_check(provider_id)
    args = _get_request_args()

    author = get_or_create(db, Author,
                           passport_uid=args['author']['passportUid'],
                           staff_login=args['author']['staffLogin'])
    operation = get_or_create(db, Operation,
                              id=args.get('operationId'),
                              author_id=author.passport_uid)
    # Check account with same abc id
    check_account = get_instance(db, Account, abc_service_id=args['abcServiceId'])
    if check_account:
        abort(409)

    account = Account(
        key=args.get('abcServiceSlug'),
        folder_id=args['folderId'],
        display_name=args.get('abcServiceSlug'),
        abc_service_id=args['abcServiceId'],
        free_tier=args.get('freeTier'),
        operation_id=operation.id)
    db.session.add(account)
    db.session.commit()
    return _response(account.as_dict())


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/_getPage', methods=['POST'])
def listAccounts(provider_id):
    """
    ListAccounts and ListAccountsByFolder method.
    :param provider_id: type UUID
    """
    _provider_check(provider_id)
    args = _get_request_args()

    provisions = args.get('withProvisions', False)
    deleted = args.get('includeDeleted', False)
    page_num = args.get('pageToken', 1)
    limit = args.get('limit', 100)

    accounts_page = db.session.query(Account).paginate(page=page_num, per_page=limit)
    result = {
        'accounts': [account.as_dict(with_provisions=provisions, deleted=deleted) for account in accounts_page.items]
    }
    if accounts_page.has_next:
        result['nextPageToken'] = accounts_page.next_num
    return _response(result)


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/<account_id>/_getOne', methods=['POST'])
def getAccount(provider_id, account_id):
    """
    Handle for getting account.
    :param provider_id: type UUID
    :param account_id: type UUID
    """
    _provider_check(provider_id)
    _account_check(account_id)
    args = _get_request_args()

    account = get_instance(db, Account,
                           id=account_id,
                           folder_id=args.get('folderId'),
                           abc_service_id=args.get('abcServiceId'))
    if not account:
        abort(404)

    return _response(account.as_dict(with_provisions=args.get('withProvisions'),
                                     deleted=args.get('deleted')))


def _updateProvisions(provisions, account_id, operation_id):
    ret = []
    for request_provision in provisions:
        resource_unit = get_or_create(db, Unit, key=request_provision['providedAmountUnitKey'])
        resource_type = get_or_create(db, ResourceType, key=request_provision['resourceKey']['resourceTypeKey'],
                                      unit=resource_unit.key)
        resource_amount = request_provision['providedAmount']
        for segmentation in request_provision['resourceKey']['segmentation']:
            segment = get_or_create(db, Segment, segmentation=segmentation['segmentationKey'], key=segmentation['segmentKey'])
            provision_min_args = {'account_id': account_id, 'resource_key': resource_type.key,
                                  'unit_key': resource_unit.key, 'segmentation': segment}
            provision_full_args = {'provided_amount': resource_amount, 'operation_id': operation_id}
            provision_full_args.update(provision_min_args)
            ret.append(create_or_update(db, Provision, required_args=provision_min_args,
                                        full_args=provision_full_args))
    return ret


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/<account_id>/_provide', methods=['POST'])
def updateProvision(provider_id, account_id):
    """
    Handle for manage account provisions.
    :param provider_id: type UUID
    :param account_id: type UUID
    """
    _provider_check(provider_id)
    _account_check(account_id)
    args = _get_request_args()

    author = get_or_create(db, Author,
                           passport_uid=args['author']['passportUid'],
                           staff_login=args['author']['staffLogin'])

    account = get_instance(db, Account,
                           id=account_id,
                           folder_id=args.get('folderId'),
                           abc_service_id=args.get('abcServiceId'))
    if not account:
        abort(404, description="Account with id={}, folder_id={}, abc_service_id={} was not found".format(
            account_id, args.get('folderId'), args.get('abcServiceId')))
    operation = get_or_create(db, Operation,
                              id=args.get('operationId'),
                              author_id=author.passport_uid)
    provisions = _updateProvisions(args['updatedProvisions'], account_id, operation.id)
    return _response({'provisions': [provision.as_dict() for provision in provisions]})


def _get_resources_summary(provisions):
    UNIT_MULT = {
        'pebi' : 2**50, 'tebi' : 2**40, 'gibi' : 2**30, 'mebi' : 2**20, 'kibi' : 2**10,
        'peta' : 10**15, 'tera' : 10**12, 'giga' : 10**9, 'mega' : 10**6, 'kilo' : 10**3
    }
    ret = defaultdict(int)
    for p in provisions:
        ret[p['resourceKey']['resourceTypeKey']] += \
            p['providedAmount'] * UNIT_MULT.get(p['providedAmountUnitKey'].strip()[:4].lower(), 1)
    return dict(ret)


def _add_dicts(d1, d2):
    return {k : d1.get(k, 0) + d2.get(k, 0) for k in set(d1) | set(d2)}


def _sub_dicts(d1, d2):
    return {k : d1.get(k, 0) - d2.get(k, 0) for k in set(d1) | set(d2)}


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/<account_id>/_moveProvision', methods=['POST'])
def moveProvision(provider_id, account_id):
    """
    Handle for moving account provisions.
    :param provider_id: type UUID
    :param account_id: type UUID
    """
    _provider_check(provider_id)
    _account_check(account_id)
    args = _get_request_args()

    author = get_or_create(db, Author, passport_uid=args['author']['passportUid'],
                                       staff_login=args['author']['staffLogin'])
    src_account = get_instance(db, Account, id=account_id,
                                            folder_id=args['sourceFolderId'],
                                            abc_service_id=args['sourceAbcServiceId'])
    if not src_account:
        abort(404, description=f"Source account with id={account_id}, "
                               f"folder_id={args['sourceFolderId']}, "
                               f"abc_service_id={args['sourceAbcServiceId']} was not found")
    dst_account = get_instance(db, Account, id=args['destinationAccountId'],
                                            folder_id=args['destinationFolderId'],
                                            abc_service_id=args['destinationAbcServiceId'])
    if not dst_account:
        abort(404, description=f"Destination account with id={args['destinationAccountId']}, "
                               f"folder_id={args['destinationFolderId']}, "
                               f"abc_service_id={args['destinationAbcServiceId']} was not found")
    operation = get_or_create(db, Operation, id=args['operationId'],
                              author_id=author.passport_uid)
    # Known state validation
    for checked_provisions in args['knownSourceProvisions'] + args['knownDestinationProvisions']:
        for checked_provision in checked_provisions['knownProvisions']:
            checked_unit = get_or_create(db, Unit, key=checked_provision['providedAmountUnitKey'])
            checked_type = get_instance(db, ResourceType,
                                        key=checked_provision['resourceKey']['resourceTypeKey'],
                                        unit=checked_unit.key)
            if not checked_type:
                abort(404, description=f"Resource type {checked_provision['resourceKey']['resourceTypeKey']} not found")
            for checked_segmentation in checked_provision['resourceKey']['segmentation']:
                checked_segment = get_instance(db, Segment, key=checked_segmentation['segmentKey'])
                if not checked_segment:
                    abort(404, description=f"Segment {checked_segmentation['segmentKey']} not found")
                provision = get_instance(db, Provision, account_id=checked_provisions['accountId'],
                                         resource_key=checked_type.key,
                                         segmentation=checked_segment,
                                         unit_key=checked_unit.key,
                                         provided_amount=checked_provision['providedAmount'])
                if checked_provision['providedAmount'] == 0 and not provision:
                    provision = get_or_create(db, Provision, account_id=checked_provisions['accountId'],
                                              resource_key=checked_type.key,
                                              segmentation=checked_segment,
                                              unit_key=checked_unit.key,
                                              operation_id=operation.id)
                if not provision:
                    abort(404, description=f"Provision with accountId={checked_provisions['accountId']}, "
                                           f"resourceType={checked_type.key}, segment={checked_segment.key}, "
                                           f"unit={checked_unit.key}, "
                                           f"providedAmount={checked_provision['providedAmount']} not found")

    src_provisions = [s for s in args['knownSourceProvisions']
                      if s['accountId'] == account_id][0]['knownProvisions']
    dst_provisions = [d for d in args['knownDestinationProvisions']
                      if d['accountId'] == args['destinationAccountId']][0]['knownProvisions']
    src_provisions_new = args['updatedSourceProvisions']
    dst_provisions_new = args['updatedDestinationProvisions']
    # Optional resource delta validation
    sum_old = _add_dicts(_get_resources_summary(src_provisions), _get_resources_summary(dst_provisions))
    sum_new = _add_dicts(_get_resources_summary(src_provisions_new), _get_resources_summary(dst_provisions_new))
    sum_diff = {res: diff for res, diff in _sub_dicts(sum_new, sum_old).items() if diff != 0}
    if sum_diff:
        current_app.logger.error(f"Found disbalance for resources: {sum_diff}")
        abort(400, description=f"Found disbalance for resources: {sum_diff}")

    updated_src_provisions = [p.as_dict() for p in _updateProvisions(src_provisions_new, src_account.id, operation.id)]
    updated_dst_provisions = [p.as_dict() for p in _updateProvisions(dst_provisions_new, dst_account.id, operation.id)]
    response = {
        'sourceProvisions' : updated_src_provisions,
        'destinationProvisions' : updated_dst_provisions,
        'sourceAccountVersion' : src_account.version,
        'destinationAccountVersion' : dst_account.version
    }
    if 'accountsSpaceKey' in args:
        response['accountsSpaceKey'] = args['accountsSpaceKey']
    return _response(response)


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/<account_id>/_revokeFreeTier', methods=['POST'])
def revokeFreeTier(provider_id, account_id):
    """
    Revoke FreeTier handle. Unsupportable yet for SaaS.
    :param provider_id: type UUID
    :param account_id: type UUID
    """

    _provider_check(provider_id)
    _account_check(account_id)
    args = _get_request_args()

    author = get_or_create(db, Author,
                           passport_uid=args['author']['passportUid'],
                           staff_login=args['author']['staffLogin'])
    account = get_instance(db, Account,
                           id=account_id,
                           folder_id=args.get('folderId'),
                           abc_service_id=args.get('abcServiceID'))
    operation = get_or_create(db, Operation,
                              id=args.get('operationId'),
                              author_id=author.passport_uid)
    account.free_tier = False
    account.operation_id = operation.id
    db.session.commit()

    return _response(account.as_dict())


@saas_abcd_api.route('/quotaManagement/v1/providers/<provider_id>/accounts/<account_id>')
def deleteAccount(provider_id, account_id):
    """
    Remove account handle. Marking account as deleted.
    :param provider_id: type UUID
    :param account_id: type UUID
    """

    _provider_check(provider_id)
    _account_check(account_id)
    args = _get_request_args()

    author = get_or_create(db, Author,
                           passport_uid=args['author']['passportUid'],
                           staff_login=args['author']['staffLogin'])
    operation = get_or_create(db, Operation,
                              id=args.get('operationId'),
                              author_id=author.passport_uid)
    instance = get_instance(db, Account,
                            id=account_id,
                            folder_id=args['folderId'],
                            abc_service_id=args['abcServiceId'])
    instance.deleted = True
    instance.operation_id = operation.id
    db.session.commit()
    return Response([], status=204, content_type=content_type)
