import hashlib
import socket
import struct
import time

from django.conf import settings
from django.utils.translation import gettext_lazy as _

from common.exceptions import JMSException
from common.utils import get_logger
from .base import BaseSMSClient

logger = get_logger(__file__)

CMPP_CONNECT = 0x00000001  # 请求连接
CMPP_CONNECT_RESP = 0x80000001  # 请求连接应答
CMPP_TERMINATE = 0x00000002  # 终止连接
CMPP_TERMINATE_RESP = 0x80000002  # 终止连接应答
CMPP_SUBMIT = 0x00000004  # 提交短信
CMPP_SUBMIT_RESP = 0x80000004  # 提交短信应答
CMPP_DELIVER = 0x00000005  # 短信下发
CMPP_DELIVER_RESP = 0x80000005  # 下发短信应答


class CMPPBaseRequestInstance(object):
    def __init__(self):
        self.command_id = ''
        self.body = b''
        self.length = 0

    def get_header(self, sequence_id):
        length = struct.pack('!L', 12 + self.length)
        command_id = struct.pack('!L', self.command_id)
        sequence_id = struct.pack('!L', sequence_id)
        return length + command_id + sequence_id

    def get_message(self, sequence_id):
        return self.get_header(sequence_id) + self.body


class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
    def __init__(self, sp_id, sp_secret):
        if len(sp_id) != 6:
            raise ValueError(_("sp_id is 6 bits"))

        super().__init__()

        source_addr = sp_id.encode('utf-8')
        sp_secret = sp_secret.encode('utf-8')
        version = struct.pack('!B', 0x02)
        timestamp = struct.pack('!L', int(self.get_now()))
        authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
        auth_source_md5 = hashlib.md5(authenticator_source).digest()
        self.body = source_addr + auth_source_md5 + version + timestamp
        self.length = len(self.body)
        self.command_id = CMPP_CONNECT

    @staticmethod
    def get_now():
        return time.strftime('%m%d%H%M%S', time.localtime(time.time()))


class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
    def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
                 service_id='', dest_usr_tl=1):
        if len(msg_content) >= 70:
            raise JMSException('The message length should be within 70 characters')
        if len(dest_terminal_id) > 100:
            raise JMSException('The number of users receiving information should be less than 100')

        super().__init__()

        msg_id = 8 * b'\x00'
        pk_total = struct.pack('!B', 1)
        pk_number = struct.pack('!B', 1)
        registered_delivery = struct.pack('!B', 0)
        msg_level = struct.pack('!B', 0)
        service_id = service_id.ljust(10, '\x00').encode('utf-8')
        fee_user_type = struct.pack('!B', 2)
        fee_terminal_id = ('0' * 21).encode('utf-8')
        tp_pid = struct.pack('!B', 0)
        tp_udhi = struct.pack('!B', 0)
        msg_fmt = struct.pack('!B', 8)
        fee_type = '01'.encode('utf-8')
        fee_code = '000000'.encode('utf-8')
        valid_time = ('\x00' * 17).encode('utf-8')
        at_time = ('\x00' * 17).encode('utf-8')
        src_id = src_id.ljust(21, '\x00').encode('utf-8')
        reserve = b'\x00' * 8
        _msg_length = struct.pack('!B', len(msg_content) * 2)
        _msg_src = msg_src.encode('utf-8')
        _dest_usr_tl = struct.pack('!B', dest_usr_tl)
        _msg_content = msg_content.encode('utf-16-be')
        _dest_terminal_id = b''.join([
            (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
        ])
        self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
        self.command_id = CMPP_SUBMIT
        self.body = msg_id + pk_total + pk_number + registered_delivery \
                    + msg_level + service_id + fee_user_type + fee_terminal_id \
                    + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
                    + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
                    + _msg_length + _msg_content + reserve


class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
    def __init__(self):
        super().__init__()
        self.body = b''
        self.command_id = CMPP_TERMINATE


class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
    def __init__(self, msg_id, result=0):
        super().__init__()
        msg_id = struct.pack('!Q', msg_id)
        result = struct.pack('!B', result)
        self.length = len(self.body)
        self.body = msg_id + result


class CMPPResponseInstance(object):
    def __init__(self):
        self.command_id = None
        self.length = None
        self.response_handler_map = {
            CMPP_CONNECT_RESP: self.connect_response_parse,
            CMPP_SUBMIT_RESP: self.submit_response_parse,
            CMPP_DELIVER: self.deliver_request_parse,
        }

    @staticmethod
    def connect_response_parse(body):
        status, = struct.unpack('!B', body[0:1])
        authenticator_ISMG = body[1:17]
        version, = struct.unpack('!B', body[17:18])
        return {
            'Status': status,
            'AuthenticatorISMG': authenticator_ISMG,
            'Version': version
        }

    @staticmethod
    def submit_response_parse(body):
        msg_id = body[:8]
        result = struct.unpack('!B', body[8:9])
        return {
            'Msg_Id': msg_id, 'Result': result[0]
        }

    @staticmethod
    def deliver_request_parse(body):
        msg_id, = struct.unpack('!Q', body[0:8])
        dest_id = body[8:29]
        service_id = body[29:39]
        tp_pid = struct.unpack('!B', body[39:40])
        tp_udhi = struct.unpack('!B', body[40:41])
        msg_fmt = struct.unpack('!B', body[41:42])
        src_terminal_id = body[42:63]
        registered_delivery = struct.unpack('!B', body[63:64])
        msg_length = struct.unpack('!B', body[64:65])
        msg_content = body[65:msg_length[0] + 65]
        return {
            'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
            'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
            'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
            'Msg_Length': msg_length, 'Msg_content': msg_content
        }

    def parse_header(self, data):
        self.command_id, = struct.unpack('!L', data[4:8])
        sequence_id, = struct.unpack('!L', data[8:12])
        return {
            'length': self.length,
            'command_id': hex(self.command_id),
            'sequence_id': sequence_id
        }

    def parse_body(self, body):
        response_body_func = self.response_handler_map.get(self.command_id)
        if response_body_func is None:
            raise JMSException('Unable to parse the returned result: %s' % body)
        return response_body_func(body)

    def parse(self, data):
        self.length, = struct.unpack('!L', data[0:4])
        header = self.parse_header(data)
        body = self.parse_body(data[12:self.length])
        return header, body


class CMPPClient(object):
    def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
        self.ip = host
        self.port = port
        self.sp_id = sp_id
        self.sp_secret = sp_secret
        self.src_id = src_id
        self.service_id = service_id
        self._sequence_id = 0
        self._is_connect = False
        self._times = 3
        self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._connect()

    @property
    def sequence_id(self):
        s = self._sequence_id
        self._sequence_id += 1
        return s

    def _connect(self):
        self.__socket.settimeout(5)
        error_msg = _('Failed to connect to the CMPP gateway server, err: {}')
        for i in range(self._times):
            try:
                self.__socket.connect((self.ip, self.port))
            except Exception as err:
                error_msg = error_msg.format(str(err))
                logger.warning(error_msg)
                time.sleep(1)
            else:
                self._is_connect = True
                break
        else:
            raise JMSException(error_msg)

    def send(self, instance):
        if isinstance(instance, CMPPBaseRequestInstance):
            message = instance.get_message(sequence_id=self.sequence_id)
        else:
            message = instance
        self.__socket.send(message)

    def recv(self):
        raw_length = self.__socket.recv(4)
        length, = struct.unpack('!L', raw_length)
        header, body = CMPPResponseInstance().parse(
            raw_length + self.__socket.recv(length - 4)
        )
        return header, body

    def close(self):
        if self._is_connect:
            terminate_request = CMPPTerminateRequestInstance()
            self.send(terminate_request)
            self.__socket.close()

    def _cmpp_connect(self):
        connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
        self.send(connect_request)
        header, body = self.recv()
        if body['Status'] != 0:
            raise JMSException('CMPPv2.0 authentication failed: %s' % body)

    def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
        """
        优先发送template_param中message的信息
        若该内容不存在，则根据template_code构建验证码发送
        """
        message = template_param.get('message')
        if message is None:
            code = template_param.get('code')
            message = template_code.replace('{code}', code)
        msg = '【%s】 %s' % (sign_name, message)
        submit_request = CMPPSubmitRequestInstance(
            msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
            dest_usr_tl=len(dest), dest_terminal_id=dest,
            service_id=self.service_id
        )
        self.send(submit_request)
        header, body = self.recv()
        command_id = header.get('command_id')
        if command_id == CMPP_DELIVER:
            deliver_request = CMPPDeliverRespRequestInstance(
                msg_id=body['Msg_Id'], result=body['Result']
            )
            self.send(deliver_request)

    def send_sms(self, dest, sign_name, template_code, template_param):
        try:
            self._cmpp_connect()
            self._cmpp_send_sms(dest, sign_name, template_code, template_param)
        except Exception as e:
            logger.error('CMPPv2.0 Error: %s', e)
            self.close()
            raise JMSException(e)


class CMPP2SMS(BaseSMSClient):
    SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'

    @classmethod
    def new_from_settings(cls):
        return cls(
            host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
            sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
            service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
        )

    def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
        try:
            self.client = CMPPClient(
                host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
            )
        except Exception as err:
            self.client = None
            logger.warning(err)
            raise JMSException(err)

    @staticmethod
    def need_pre_check():
        return False

    def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
        try:
            logger.info(f'CMPPv2.0 sms send: '
                        f'phone_numbers={phone_numbers} '
                        f'sign_name={sign_name} '
                        f'template_code={template_code} '
                        f'template_param={template_param}')
            self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
        except Exception as e:
            raise JMSException(e)


client = CMPP2SMS
