# -*- coding: utf-8 -*-

import os
import random
import logging
import copy
import base64
import struct
from .cos_comm import *
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto import Random
from Crypto.Util import Counter
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
from .cos_exception import CosClientError
from .streambody import StreamBody
from abc import ABCMeta, abstractmethod

logger = logging.getLogger(__name__)

_AES_CTR_COUNTER_BITS_LENGTH = 8 * 16
_AES_256_KEY_SIZE = 32


def random_key(key_len):
    return Random.new().read(key_len)


def random_iv():
    iv = Random.new().read(16)
    return iv


def iv_to_big_int(iv):
    iv_pair = struct.unpack(">QQ", iv)
    iv_int = iv_pair[0] << 64 | iv_pair[1]
    return iv_int


class AESCTRCipher(object):
    """数据加密类，用于加密用户数据"""

    def __init__(self):
        """初始化"""
        self.__block_size_len = AES.block_size
        self.__cipher = None
        self.__key_len = _AES_256_KEY_SIZE

    def new_cipher(self, key, start, offset=0):
        """初始化AES加解密对象

        :param key(string): 对称密钥
        :param start(int): 对称加密初始随机值
        :param offset(int): 主要用于解密，想要解密文本的偏移，必须为16的整数倍
        """
        block_index_offset = self.__calc_offset(offset)
        new_start = start + block_index_offset
        my_counter = Counter.new(_AES_CTR_COUNTER_BITS_LENGTH, initial_value=new_start)
        self.__cipher = AES.new(key, AES.MODE_CTR, counter=my_counter)

    def encrypt(self, plaintext):
        """加密数据

        :param plaintext(string): 需要加密的数据
        """
        if self.__cipher is None:
            raise CosClientError('cipher is not initialized')
        return self.__cipher.encrypt(plaintext)

    def decrypt(self, plaintext):
        """解密数据

        :param plaintext(string): 需要解密的数据，数据的起始位置必须为16的整数倍
        """
        if self.__cipher is None:
            raise CosClientError('cipher is not initialized')
        return self.__cipher.decrypt(plaintext)

    # offset 必须为block_size的整数倍
    def __calc_offset(self, offset):
        """通过文本偏移计算counter的偏移

        :param offset(int): 文本偏移
        """
        if not self.__is_block_aligned(offset):
            raise CosClientError('offset is not align to encrypt block')
        return offset // self.__block_size_len

    def __is_block_aligned(self, offset):
        """判断文本偏移是否是block_size对齐

        :param offset(int): 文本偏移
        """
        if offset is None:
            offset = 0
        return 0 == offset % self.__block_size_len

    def adjust_read_offset(self, offset):
        """用于调整读取的offset为block_size对齐"""

        if offset:
            offset = (offset // self.__block_size_len) * self.__block_size_len
        return offset

    def get_key(self):
        """获取随机密钥"""
        return random_key(self.__key_len)

    def get_counter_iv(self):
        """获取对称加密初始随机值"""
        return random_iv()


class RSAKeyPair:
    """封装有公钥和私钥"""

    def __init__(self, public_key, private_key):
        self.publick_key = public_key
        self.private_key = private_key


class RSAKeyPairPath:
    """封装有公钥和私钥的路径"""

    def __init__(self, public_key_path, private_key_path):
        self.public_key_path = public_key_path
        self.private_key_path = private_key_path


class BaseProvider(object):
    """客户端主密钥加密基类"""

    def __init__(self, cipher):
        """初始化
        :param cipher(an AES object): 数据加解密类
        """
        self.data_cipher = cipher

    def get_data_key(self):
        """随机获取数据加解密密钥"""
        return self.data_cipher.get_key()

    @abstractmethod
    def init_data_cipher(self):
        """初始化cipher"""
        pass

    @abstractmethod
    def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
        """根据密钥初始化cipher"""
        pass

    def adjust_read_offset(self, start):
        """用于调整读取的offset为block_size对齐"""
        return self.data_cipher.adjust_read_offset(start)

    def make_data_encrypt_adapter(self, stream):
        """创建数据流加密适配器"""
        size = 0
        if hasattr(stream, '__len__'):
            size = len(stream)
        elif hasattr(stream, 'tell') and hasattr(stream, 'seek'):
            current = stream.tell()
            stream.seek(0, os.SEEK_END)
            size = stream.tell()
            stream.seek(current, os.SEEK_SET)
        else:
            return None
        return DataEncryptAdapter(stream, size, copy.copy(self.data_cipher))

    def make_data_decrypt_adapter(self, rt, offset):
        """创建数据流解密适配器"""
        return DataDecryptAdapter(rt, copy.copy(self.data_cipher), offset)


class RSAProvider(BaseProvider):
    """客户端非对称主密钥加密类"""

    def __init__(self, key_pair_info=None, cipher=AESCTRCipher(), passphrase=None):
        """初始化"""
        super(RSAProvider, self).__init__(cipher=cipher)

        default_rsa_dir = os.path.expanduser('~/.cos_local_rsa')
        default_public_key_path = os.path.join(default_rsa_dir, '.public_key.pem')
        default_private_key_path = os.path.join(default_rsa_dir, '.private_key.pem')
        self.__encrypt_obj = None
        self.__decrypt_obj = None
        self.__data_key = None
        self.__data_iv = None

        if isinstance(key_pair_info, RSAKeyPair):
            self.__encrypt_obj = PKCS1_v1_5.new(RSA.importKey(key_pair_info.publick_key, passphrase=passphrase))
            self.__decrypt_obj = PKCS1_v1_5.new(RSA.importKey(key_pair_info.private_key, passphrase=passphrase))
        elif isinstance(key_pair_info, RSAKeyPairPath):
            self.__encrypt_obj, self.__decrypt_obj = self.__get_key_by_path(key_pair_info.public_key_path,
                                                                            key_pair_info.private_key_path, passphrase)
        else:
            logger.info('key_pair_info is None, try to get key from default path')
            if os.path.exists(default_private_key_path) and os.path.exists(default_public_key_path):
                self.__encrypt_obj, self.__decrypt_obj = self.__get_key_by_path(default_public_key_path,
                                                                                default_private_key_path, passphrase)

        # 为用户自动创建rsa
        if self.__encrypt_obj is None and self.__decrypt_obj is None:
            logger.warn('fail to get rsa key, will generate key')
            private_key = RSA.generate(2048)
            public_key = private_key.publickey()

            self.__encrypt_obj = PKCS1_OAEP.new(public_key)
            self.__decrypt_obj = PKCS1_OAEP.new(private_key)

            if not os.path.exists(default_rsa_dir):
                os.makedirs(default_rsa_dir)

            with open(default_private_key_path, 'wb') as f:
                f.write(private_key.exportKey(passphrase=passphrase))

            with open(default_public_key_path, 'wb') as f:
                f.write(public_key.exportKey(passphrase=passphrase))

    @staticmethod
    def get_rsa_key_pair(public_key, private_key):
        """开放给用户，用于生成RSAKeyPair"""
        if public_key is None or private_key is None:
            raise CosClientError('public_key or private_key is not allowed to be None !!!')
        return RSAKeyPair(public_key, private_key)

    @staticmethod
    def get_rsa_key_pair_path(public_key_path, private_key_path):
        """开放给用户，用于生成RSAKeyPairPath"""
        if public_key_path is None or private_key_path is None:
            raise CosClientError('public_key or private_key is not allowed to be None !!!')
        return RSAKeyPairPath(public_key_path, private_key_path)

    def __get_key_by_path(self, public_path=None, private_path=None, passphrase=None):
        """用于从提供的路径中获取公钥和私钥"""
        if public_path is None or private_path is None:
            return None, None

        encrypt_obj, decrypt_obj = None, None

        if os.path.exists(public_path) and os.path.exists(private_path):
            with open(public_path, 'rb') as f:
                encrypt_obj = PKCS1_OAEP.new(RSA.importKey(f.read(), passphrase=passphrase))

            with open(private_path, 'rb') as f:
                decrypt_obj = PKCS1_OAEP.new(RSA.importKey(f.read(), passphrase=passphrase))

        return encrypt_obj, decrypt_obj

    def init_data_cipher(self):
        """初始化cipher"""
        encrypt_key = None
        encrypt_iv = None
        self.__data_key = self.get_data_key()
        self.__data_iv = self.data_cipher.get_counter_iv()
        start = iv_to_big_int(self.__data_iv)
        self.data_cipher.new_cipher(self.__data_key, start)
        encrypt_key = self.__encrypt_obj.encrypt(self.__data_key)
        encrypt_iv = self.__encrypt_obj.encrypt(self.__data_iv)
        return encrypt_key, encrypt_iv

    def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
        """根据密钥初始化cipher"""
        self.__data_key = self.__decrypt_obj.decrypt(encrypt_key)
        self.__data_iv = self.__decrypt_obj.decrypt(encrypt_iv)
        start = iv_to_big_int(self.__data_iv)
        self.data_cipher.new_cipher(self.__data_key, start, offset)


class AESProvider(BaseProvider):
    """客户端对称主密钥加密类"""

    def __init__(self, aes_key=None, aes_key_path=None, cipher=AESCTRCipher()):
        """初始化"""
        super(AESProvider, self).__init__(cipher=cipher)
        self.__ed_obj = None
        self.__data_key = None
        self.__data_iv = None
        self.__my_counter = Counter.new(_AES_CTR_COUNTER_BITS_LENGTH, initial_value=0)
        self.__aes_key = aes_key
        self.__aes_key_path = aes_key_path

    def init_ed_obj(self):
        default_aes_dir = os.path.expanduser('~/.cos_local_aes')
        default_key_path = os.path.join(default_aes_dir, '.aes_key.pem')
        if self.__aes_key:
            aes_key = to_bytes(base64.b64decode(to_bytes(self.__aes_key)))
            self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
        elif self.__aes_key_path:
            if os.path.exists(self.__aes_key_path):
                with open(self.__aes_key_path, 'rb') as f:
                    aes_key = f.read()
                    aes_key = to_bytes(base64.b64decode(to_bytes(aes_key)))
                    self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
        else:
            logger.info('aes_key and aes_key_path is None, try to get key from default path')
            if os.path.exists(default_key_path):
                with open(default_key_path, 'rb') as f:
                    aes_key = f.read()
                    aes_key = to_bytes(base64.b64decode(to_bytes(aes_key)))
                    self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)

        if self.__ed_obj is None:
            logger.warn('fail to get aes key, will generate key')
            aes_key = random_key(_AES_256_KEY_SIZE)
            self.__ed_obj = AES.new(aes_key, AES.MODE_CTR, counter=self.__my_counter)
            if not os.path.exists(default_aes_dir):
                os.makedirs(default_aes_dir)

            with open(default_key_path, 'wb') as f:
                aes_key = to_bytes(base64.b64encode(to_bytes(aes_key)))
                f.write(aes_key)

    def init_data_cipher(self):
        """初始化cipher"""
        encrypt_key = None
        encrypt_iv = None
        self.__data_key = self.get_data_key()
        self.__data_iv = self.data_cipher.get_counter_iv()
        start = iv_to_big_int(self.__data_iv)
        self.data_cipher.new_cipher(self.__data_key, start)

        self.init_ed_obj()
        encrypt_key = self.__ed_obj.encrypt(self.__data_key)
        encrypt_iv = self.__ed_obj.encrypt(self.__data_iv)
        return encrypt_key, encrypt_iv

    def init_data_cipter_by_user(self, encrypt_key, encrypt_iv, offset=0):
        """根据密钥初始化cipher"""
        self.init_ed_obj()
        self.__data_key = self.__ed_obj.decrypt(encrypt_key)
        self.__data_iv = self.__ed_obj.decrypt(encrypt_iv)
        start = iv_to_big_int(self.__data_iv)
        self.data_cipher.new_cipher(self.__data_key, start, offset)


class MetaHandle(object):
    """用于获取/生成加密的元信息"""

    def __init__(self, encrypt_key=None, encrypt_iv=None):
        """初始化

        :param encrypt_key(string): 加密的数据密钥
        :param encrypt_iv(bytes): 加密counter的初始值
        """
        self.__encrypt_key = encrypt_key
        self.__encrypt_iv = encrypt_iv

    def set_object_meta(self, headers):
        """设置加密元信息到object的头部"""
        meta_data = dict()
        meta_data['x-cos-meta-client-side-encryption-key'] = to_bytes(base64.b64encode(to_bytes(self.__encrypt_key)))
        meta_data['x-cos-meta-client-side-encryption-iv'] = to_bytes(base64.b64encode(to_bytes(self.__encrypt_iv)))
        headers['Metadata'] = meta_data
        return headers

    def get_object_meta(self, headers):
        """从object的头部获取加密元信息"""
        if 'x-cos-meta-client-side-encryption-key' in headers and 'x-cos-meta-client-side-encryption-iv' in headers:
            self.__encrypt_key = base64.b64decode(to_bytes(headers['x-cos-meta-client-side-encryption-key']))
            self.__encrypt_iv = base64.b64decode(to_bytes(headers['x-cos-meta-client-side-encryption-iv']))
        return self.__encrypt_key, self.__encrypt_iv


class DataEncryptAdapter(object):
    """用于读取经过加密后的的数据"""

    def __init__(self, data, content_len, data_cipher):
        self._data = to_bytes(data)
        self._data_cipher = data_cipher
        self._content_len = content_len
        self._read_len = 0

    @property
    def len(self):
        return self._content_len

    def read(self, length):
        """读取加密后的数据"""
        if self._read_len >= self._content_len:
            return ''

        if length is None or length < 0:
            bytes_to_read = self._content_len - self._read_len
        else:
            bytes_to_read = min(length, self._content_len - self._read_len)

        if isinstance(self._data, bytes):
            content = self._data[self._read_len:self._read_len + bytes_to_read]
        else:
            content = self._data.read(bytes_to_read)

        self._read_len += bytes_to_read
        content = self._data_cipher.encrypt(content)
        return content


class DataDecryptAdapter(StreamBody):
    """用于读取经过解密后的数据"""

    def __init__(self, rt, data_cipher, offset=0):
        """初始化
        :param rt(request object): request请求返回的对象
        :param data_cipher(an AES object): 数据加解密类
        :param offset(int): 数据读取的偏移量
        """
        super(DataDecryptAdapter, self).__init__(rt)
        self._data_cipher = data_cipher
        self._offset = offset

    def read(self, length, auto_decompress=False):
        """读取解密后的数据"""
        if self._read_len >= self._content_len:
            return ''

        if self._use_encoding and not auto_decompress:
            content = self._rt.raw.read(length)
        else:
            try:
                content = next(self._rt.iter_content(length))
            except StopIteration:
                return ''

        content = self._data_cipher.decrypt(content)
        if self._read_len < self._offset and self._read_len + len(content) > self._offset:
            content = content[self._offset:]
            self._read_len = self._offset
        return content
