# -*- coding: utf-8 -*-
"""
    wechatpy.component
    ~~~~~~~~~~~~~~~

    This module provides client library for WeChat Open Platform

    :copyright: (c) 2015 by hunter007.
    :license: MIT, see LICENSE for more details.
"""
from __future__ import absolute_import, unicode_literals
import time
import logging

import six
import requests
import xmltodict

from wechatpy.utils import to_text, to_binary, get_querystring, json
from wechatpy.fields import StringField, DateTimeField
from wechatpy.messages import MessageMetaClass
from wechatpy.session.memorystorage import MemoryStorage
from wechatpy.exceptions import WeChatClientException, APILimitedException
from wechatpy.crypto import WeChatCrypto
from wechatpy.client import WeChatComponentClient


logger = logging.getLogger(__name__)


class BaseComponentMessage(six.with_metaclass(MessageMetaClass)):
    """Base class for all component messages and events"""
    type = 'unknown'
    appid = StringField('AppId')
    create_time = DateTimeField('CreateTime')

    def __init__(self, message):
        self._data = message

    def __repr__(self):
        _repr = "{klass}({msg})".format(
            klass=self.__class__.__name__,
            msg=repr(self._data)
        )
        if six.PY2:
            return to_binary(_repr)
        else:
            return to_text(_repr)


class ComponentVerifyTicketMessage(BaseComponentMessage):
    """
    component_verify_ticket协议
    """
    type = 'component_verify_ticket'
    verify_ticket = StringField('ComponentVerifyTicket')


class ComponentUnauthorizedMessage(BaseComponentMessage):
    """
    取消授权通知
    """
    type = 'unauthorized'
    authorizer_appid = StringField('AuthorizerAppid')


class BaseWeChatComponent(object):

    _http = requests.Session()

    API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin'

    def __init__(self,
                 component_appid,
                 component_appsecret,
                 component_token,
                 encoding_aes_key,
                 session=None):
        """
        :param component_appid: 第三方平台appid
        :param component_appsecret: 第三方平台appsecret
        :param component_token: 公众号消息校验Token
        :param encoding_aes_key: 公众号消息加解密Key
        """
        self.component_appid = component_appid
        self.component_appsecret = component_appsecret
        self.expires_at = None
        self.crypto = WeChatCrypto(
            component_token, encoding_aes_key, component_appid)
        self.session = session or MemoryStorage()

        if isinstance(session, six.string_types):
            from shove import Shove
            from wechatpy.session.shovestorage import ShoveStorage

            querystring = get_querystring(session)
            prefix = querystring.get('prefix', ['wechatpy'])[0]

            shove = Shove(session)
            storage = ShoveStorage(shove, prefix)
            self.session = storage

    @property
    def component_verify_ticket(self):
        ticket, expires_at = self.session.get('component_verify_ticket')
        return ticket

    def _request(self, method, url_or_endpoint, **kwargs):
        if not url_or_endpoint.startswith(('http://', 'https://')):
            api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
            url = '{base}{endpoint}'.format(
                base=api_base_url,
                endpoint=url_or_endpoint
            )
        else:
            url = url_or_endpoint

        if 'params' not in kwargs:
            kwargs['params'] = {}
        if isinstance(kwargs['params'], dict) and \
                'component_access_token' not in kwargs['params']:
            kwargs['params'][
                'component_access_token'] = self.access_token
        if isinstance(kwargs['data'], dict):
            kwargs['data'] = json.dumps(kwargs['data'])

        res = self._http.request(
            method=method,
            url=url,
            **kwargs
        )
        try:
            res.raise_for_status()
        except requests.RequestException as reqe:
            raise WeChatClientException(
                errcode=None,
                errmsg=None,
                client=self,
                request=reqe.request,
                response=reqe.response
            )

        return self._handle_result(res, method, url, **kwargs)

    def _handle_result(self, res, method=None, url=None, **kwargs):
        result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
        if 'errcode' in result:
            result['errcode'] = int(result['errcode'])

        if 'errcode' in result and result['errcode'] != 0:
            errcode = result['errcode']
            errmsg = result['errmsg']
            if errcode == 42001:
                logger.info('Component access token expired, fetch a new one and retry request')
                self.fetch_component_access_token()
                token, expires_at = self.session.get('component_access_token')
                kwargs['params']['component_access_token'] = token
                return self._request(
                    method=method,
                    url_or_endpoint=url,
                    **kwargs
                )
            elif errcode == 45009:
                # api freq out of limit
                raise APILimitedException(
                    errcode,
                    errmsg,
                    client=self,
                    request=res.request,
                    response=res
                )
            else:
                raise WeChatClientException(
                    errcode,
                    errmsg,
                    client=self,
                    request=res.request,
                    response=res
                )

        return result

    def fetch_access_token(self):
        """
        获取 component_access_token
        详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\
        &t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN

        :return: 返回的 JSON 数据包
        """
        url = '{0}{1}'.format(
            self.API_BASE_URL,
            '/component/api_component_token'
        )
        return self._fetch_access_token(
            url=url,
            data=json.dumps({
                'component_appid': self.component_appid,
                'component_appsecret': self.component_appsecret,
                'component_verify_ticket': self.component_verify_ticket
            })
        )

    def _fetch_access_token(self, url, data):
        """ The real fetch access token """
        logger.info('Fetching component access token')
        res = self._http.post(
            url=url,
            data=data
        )
        try:
            res.raise_for_status()
        except requests.RequestException as reqe:
            raise WeChatClientException(
                errcode=None,
                errmsg=None,
                client=self,
                request=reqe.request,
                response=reqe.response
            )
        result = res.json()
        if 'errcode' in result and result['errcode'] != 0:
            raise WeChatClientException(
                result['errcode'],
                result['errmsg'],
                client=self,
                request=res.request,
                response=res
            )

        expires_in = 7200
        if 'expires_in' in result:
            expires_in = result['expires_in']
        self.session.set(
            'component_access_token',
            result['component_access_token'],
            expires_in
        )
        self.expires_at = int(time.time()) + expires_in
        return result

    @property
    def access_token(self):
        """ WeChat component access token """
        access_token, expires_at = self.session.get('component_access_token')
        if access_token:
            if not self.expires_at:
                # user provided access_token, just return it
                return access_token

            timestamp = time.time()
            if self.expires_at - timestamp > 60:
                return access_token

        self.fetch_access_token()
        access_token, expires_at = self.session.get('component_access_token')
        return access_token

    def get(self, url, **kwargs):
        return self._request(
            method='get',
            url_or_endpoint=url,
            **kwargs
        )

    def post(self, url, **kwargs):
        return self._request(
            method='post',
            url_or_endpoint=url,
            **kwargs
        )


class WeChatComponent(BaseWeChatComponent):

    def create_preauthcode(self):
        """
        获取预授权码
        """
        return self.post(
            '/component/api_create_preauthcode',
            data={
                'component_appid': self.component_appid
            }
        )

    def query_auth(self, authorization_code):
        """
        使用授权码换取公众号的授权信息

        :params authorization_code: 授权code,会在授权成功时返回给第三方平台，详见第三方平台授权流程说明
        """
        return self.post(
            '/component/api_query_auth',
            data={
                'component_appid': self.component_appid,
                'authorization_code': authorization_code
            }
        )

    def refresh_authorizer_token(
            self, authorizer_appid, authorizer_refresh_token):
        """
        获取（刷新）授权公众号的令牌

        :params authorizer_appid: 授权方appid
        :params authorizer_refresh_token: 授权方的刷新令牌
        """
        return self.post(
            '/component/api_authorizer_token',
            data={
                'component_appid': self.component_appid,
                'authorizer_appid': authorizer_appid,
                'authorizer_refresh_token': authorizer_refresh_token
            }
        )

    def get_authorizer_info(self, authorizer_appid):
        """
        获取授权方的账户信息

        :params authorizer_appid: 授权方appid
        """
        return self.post(
            '/component/api_get_authorizer_info',
            data={
                'component_appid': self.component_appid,
                'authorizer_appid': authorizer_appid,
            }
        )

    def get_authorizer_option(self, authorizer_appid, option_name):
        """
        获取授权方的选项设置信息

        :params authorizer_appid: 授权公众号appid
        :params option_name: 选项名称
        """
        return self.post(
            '/component/api_get_authorizer_option',
            data={
                'component_appid': self.component_appid,
                'authorizer_appid': authorizer_appid,
                'option_name': option_name
            }
        )

    def set_authorizer_option(
            self, authorizer_appid, option_name, option_value):
        """
        设置授权方的选项信息

        :params authorizer_appid: 授权公众号appid
        :params option_name: 选项名称
        :params option_value: 设置的选项值
        """
        return self.post(
            '/component/api_set_authorizer_option',
            data={
                'component_appid': self.component_appid,
                'authorizer_appid': authorizer_appid,
                'option_name': option_name,
                'option_value': option_value
            }
        )

    def get_client_by_authorization_code(self, authorization_code):
        """
        通过授权码直接获取 Client 对象

        :params authorization_code: 授权code,会在授权成功时返回给第三方平台，详见第三方平台授权流程说明
        """
        result = self.query_auth(authorization_code)
        access_token = result['authorization_info']['authorizer_access_token']
        refresh_token = result['authorization_info']['authorizer_refresh_token']  # NOQA
        authorizer_appid = result['authorization_info']['authorizer_appid']  # noqa
        return WeChatComponentClient(
            authorizer_appid, self, access_token, refresh_token,
            session=self.session
        )

    def get_client_by_appid(self, authorizer_appid):
        """
        通过 authorizer_appid 获取 Client 对象

        :params authorizer_appid: 授权公众号appid
        """
        access_token_key = '{0}_access_token'.format(authorizer_appid)
        refresh_token_key = '{0}_refresh_token'.format(authorizer_appid)
        access_token, _ = self.session.get(access_token_key)
        refresh_token, _ = self.session.get(refresh_token_key)

        if not access_token:
            ret = self.refresh_authorizer_token(
                authorizer_appid,
                refresh_token
            )
            access_token = ret['authorizer_access_token']
            refresh_token = ret['authorizer_refresh_token']

        return WeChatComponentClient(
            authorizer_appid,
            self,
            access_token,
            refresh_token,
            session=self.session
        )

    def cache_component_verify_ticket(self, msg, signature, timestamp, nonce):
        """
        处理 wechat server 推送的 component_verify_ticket消息

        :params msg: 加密内容
        :params signature: 消息签名
        :params timestamp: 时间戳
        :params nonce: 随机数
        """
        content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
        message = xmltodict.parse(to_text(content))['xml']
        o = ComponentVerifyTicketMessage(message)
        self.session.set(o.type, o.verify_ticket, 1200)

    def get_unauthorized(self, msg, signature, timestamp, nonce):
        """
        处理取消授权通知

        :params msg: 加密内容
        :params signature: 消息签名
        :params timestamp: 时间戳
        :params nonce: 随机数
        """
        content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
        message = xmltodict.parse(to_text(content))['xml']
        return ComponentUnauthorizedMessage(message)
