import logging

from datetime import datetime
from typing import Dict, List

from django.core.exceptions import ObjectDoesNotExist
from sshpubkeys import (
    SSHKey as SSHKey_,
    InvalidKeyException,
    TooShortKeyException,
    TooLongKeyException,
    InvalidTypeException,
    MalformedDataException,
)

from staff.lib.db import atomic
from staff.lib.exceptions import ErrorWithStatusCode

from staff.person.models import Staff
from staff.keys.models import SSHKey
from staff.keys.audit import log_ssh_key_updating

from staff.person_profile.errors import log_does_not_exist_staff_login

logger = logging.getLogger(__name__)


class SSHKeyError(ErrorWithStatusCode):
    pass


class SSHKeyNotFoundError(SSHKeyError):
    pass


def get_initial(login):
    return (
        SSHKey.objects
        .values(
            'id',
            'key',
            'description',
            'fingerprint',
            'fingerprint_sha256',
        )
        .filter(staff__login=login, intranet_status=1)
        .order_by('id')
    )


def get_key(login, key_id):
    try:
        key = SSHKey.objects.get(id=key_id, intranet_status=1)
    except ObjectDoesNotExist as e:
        raise SSHKeyError(e)

    if not key.staff.login == login:
        # TODO добавить объект ошибки
        raise SSHKeyError()

    return key.key


@atomic
def add(key_data: List[Dict[str, str]], person: Staff, author_login: str, ip: str = '0.0.0.0'):
    try:
        for key in key_data:
            SSHKey.objects.update_or_create(
                defaults={
                    'intranet_status': 1,
                },
                staff_id=person.id,
                key=key['key'],
                fingerprint=key['fingerprint'],
                fingerprint_sha256=key['fingerprint_sha256'],
                description=key['description'],
            )
            log_ssh_key_updating(
                action='added',
                fingerprint=key['fingerprint'],
                fingerprint_sha256=key['fingerprint_sha256'],
                author_login=author_login,
                owner_login=person.login,
                ip=ip,
            )
    except Exception as e:
        logger.exception('Cannot save SSH keys: %s', key_data)
        raise SSHKeyError(e)


@atomic
def revoke_by_fingerprints(key_fingerprints: List[str], person: Staff, author_login: str, ip: str = '0.0.0.0'):
    for key_fingerprint in key_fingerprints:
        try:
            key = SSHKey.objects.get(fingerprint_sha256=key_fingerprint)
        except SSHKey.DoesNotExist as e:
            raise SSHKeyNotFoundError(e)
        key.intranet_status = 0
        key.save()
        log_ssh_key_updating(
            action='removed',
            fingerprint=key.fingerprint,
            fingerprint_sha256=key.fingerprint_sha256,
            author_login=author_login,
            owner_login=person.login,
            ip=ip,
        )


def update(login, data, author_login, ip='0.0.0.0'):
    now = datetime.now()
    with log_does_not_exist_staff_login(logger=logger, message_params=[login], raise_e=SSHKeyError):
        staff_id = Staff.objects.values_list('id', flat=True).get(login=login)

    old_objects = {
        o.id: o
        for o in SSHKey.objects.filter(staff_id=staff_id, intranet_status=1)
    }

    for key in data:
        pk = key.get('id')
        description = key.get('description', '')
        try:
            if pk is None:
                SSHKey(
                    staff_id=staff_id,
                    modified_at=now,
                    created_at=now,
                    key=key['key'],
                    description=description,
                    fingerprint=key['fingerprint'],
                    fingerprint_sha256=key['fingerprint_sha256'],
                ).save(force_insert=True)
                log_ssh_key_updating(
                    action='added',
                    fingerprint=key['fingerprint'],
                    fingerprint_sha256=key['fingerprint_sha256'],
                    author_login=author_login,
                    owner_login=login,
                    ip=ip,
                )
            else:
                ssh_key = old_objects.pop(pk)
                if ssh_key.description != description:
                    ssh_key.description = description
                    ssh_key.save(force_update=True)
        except Exception as e:
            logger.exception('Cannot save SSH key: %s', key)
            raise SSHKeyError(e)

    if old_objects:
        for old_key in old_objects.values():
            try:
                old_key.intranet_status = 0
                old_key.save(force_update=True)
                log_ssh_key_updating(
                    action='removed',
                    fingerprint=old_key.fingerprint,
                    fingerprint_sha256=old_key.fingerprint_sha256,
                    author_login=author_login,
                    owner_login=login,
                    ip=ip,
                )
            except ObjectDoesNotExist as e:
                logger.exception('Cannot delete SSH key: %s', old_key)
                raise SSHKeyError(e)


def ssh_fingerprint(key, algo='md5'):
    try:
        method = 'hash_%s' % algo
        ssh_key = SSHKey_(key)
        ssh_key.parse()
        fp = getattr(ssh_key, method)()
        try:
            fp = fp.decode('utf-8')
        except AttributeError:
            pass
        if fp.startswith('MD5:'):
            fp = fp[4:]
        return fp
    except (InvalidKeyException, TooShortKeyException, TooLongKeyException,
            InvalidTypeException, MalformedDataException) as e:
        raise SSHKeyError(e)
