import base64
import hashlib
import hmac
import json
import requests
import logging
import time
from datetime import datetime as dt, timezone
from json import JSONDecodeError

from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

from ._exceptions import FailedRequestError
from ._p2p_method import P2PMethod

_SUBDOMAIN_TESTNET = "api-testnet"
_SUBDOMAIN_MAINNET = "api"
_DOMAIN_MAIN = "bybit"
_DOMAIN_ALT = "bytick"
_TLD_MAIN = "com"


class P2PManager:
    def __init__(
            self,
            testnet,
            api_key="",
            api_secret="",
            domain=None,
            tld=None,
            recv_window=5000,
            rsa=False,
            logging_level=logging.INFO
    ):
        self._testnet = testnet
        self._api_key = api_key
        self._api_secret = api_secret
        self._recv_window = recv_window
        self._rsa = rsa
        self._logging_level = logging_level

        # determine domain to use
        self._subdomain = _SUBDOMAIN_TESTNET if self._testnet else _SUBDOMAIN_MAINNET
        self._domain = _DOMAIN_MAIN if not domain else domain
        self._tld = _TLD_MAIN if not tld else tld
        self._url = "https://{SUBDOMAIN}.{DOMAIN}.{TLD}".format(SUBDOMAIN=self._subdomain, DOMAIN=self._domain, TLD=self._tld)
        self.client = requests.Session()
        self.client.headers.update(
            {
                "Content-Type": "application/json",
                "Accept": "application/json",
            }
        )

        self.logger = logging.getLogger(__name__)
        if len(logging.root.handlers) == 0:
            # no handler on root logger set -> we add handler just for this logger to not mess with custom logic from outside
            handler = logging.StreamHandler()
            handler.setFormatter(
                logging.Formatter(
                    fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
                    datefmt="%Y-%m-%d %H:%M:%S",
                )
            )
            handler.setLevel(self._logging_level)
            self.logger.addHandler(handler)

        self.logger.debug("Initialized P2P API session.")

    def http_req_handler(self, method: P2PMethod, params):
        if params is None:
            params = {}

        # ensure all the required params are passed
        missing_params = [p for p in method.required_params if p not in params]
        if missing_params:
            raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")

        # Bug fix: change floating whole numbers to integers to prevent
        # auth signature errors.
        for i in params.keys():
            if isinstance(params[i], float) and params[i] == int(params[i]):
                params[i] = int(params[i])

        # construct request in accordance with API specs
        timestamp = int(time.time() * 10 ** 3)
        payload = self._generate_payload(method.http_method, params)
        signature = self._generate_sign(payload, timestamp)
        headers = {
            'X-BAPI-API-KEY': self._api_key,
            'X-BAPI-SIGN': signature,
            'X-BAPI-SIGN-TYPE': '2',
            'X-BAPI-TIMESTAMP': str(timestamp),
            'X-BAPI-RECV-WINDOW': str(self._recv_window),
            'Content-Type': 'application/json'
        }

        # build the request
        endpoint = self._url + method.url
        if method.http_method == "GET":
            r = self.client.prepare_request(
                requests.Request(
                    method.http_method, endpoint + f"?{payload}" if payload != "" else "", headers=headers
                )
            )
        elif method.http_method == "POST":
            r = self.client.prepare_request(
                requests.Request(
                    method.http_method, endpoint, data=payload, headers=headers
                )
            )

        # send the request
        try:
            s = self.client.send(r)
        except (
                requests.exceptions.ReadTimeout,
                requests.exceptions.SSLError,
                requests.exceptions.ConnectionError,
        ) as e:
            raise e

        if s.status_code != 200:
            if s.status_code == 403:
                error_msg = ("Access denied error. Possible causes: 1) your IP is located in the US or Mainland China, "
                             "2) IP banned due to ratelimit violation")
            elif s.status_code == 401:
                error_msg = ("Unauthorized. Possible causes: 1) incorrect API key and/or secret, "
                             "2) incorrect environment: Mainnet vs Testnet")
            else:
                error_msg = f"HTTP status code is: {s.status_code}, expected: 200"
                self.logger.error(f"{error_msg}")
            raise FailedRequestError(
                request=f"{endpoint}: {payload}",
                message=error_msg,
                status_code=s.status_code,
                time=dt.now(timezone.utc).strftime("%H:%M:%S"),
                resp_headers=s.headers,
            )

        # Convert response to dictionary, or raise if requests error.
        try:
            s_json = s.json()
        except JSONDecodeError as e:
            self.logger.debug(f"Response text: {s.text}")
            raise FailedRequestError(
                request=f"{endpoint}: {payload}",
                message="Could not decode JSON.",
                status_code=s.status_code,
                time=dt.now(timezone.utc).strftime("%H:%M:%S"),
                resp_headers=s.headers,
            )

        ret_code = "retCode"
        ret_msg = "retMsg"

        if ret_code not in s_json:
            ret_code = "ret_code"
        if ret_msg not in s_json:
            ret_msg = "ret_msg"


        # If Bybit returns an error, raise.
        if s_json[ret_code]:
            # Generate error message.
            error_msg = f"{s_json[ret_msg]} (ErrCode: {s_json[ret_code]})"
            self.logger.error(f"{error_msg}")
            raise FailedRequestError(
                request=f"{endpoint}: {payload}",
                message=s_json[ret_msg],
                status_code=s_json[ret_code],
                time=dt.now(timezone.utc).strftime("%H:%M:%S"),
                resp_headers=s.headers,
            )
        else:
            return s_json

    def _generate_sign(self, payload, timestamp):
        sign_string = str(timestamp) + self._api_key + str(self._recv_window) + payload
        return P2PManager._sign(self._rsa, self._api_secret, sign_string)

    # reference: https://github.com/bybit-exchange/pybit
    @staticmethod
    def _cast_values(params):
        str_params = [
            "itemId",
            "status",
            "side",
            "page",
            "size",
            "currency_id"
        ]
        int_params = [
            "positionIdx"
        ]
        for key, value in params.items():
            if key in str_params:
                if type(value) != str:
                    params[key] = str(value)
            elif key in int_params:
                if type(value) != int:
                    params[key] = int(value)

    # reference: https://github.com/bybit-exchange/pybit
    @staticmethod
    def _generate_payload(http_method, params):
        if http_method == "GET":
            payload = "&".join(
                [
                    str(k) + "=" + str(v)
                    for k, v in sorted(params.items())
                    if v is not None
                ]
            )
            return payload
        else:
            P2PManager._cast_values(params)
            return json.dumps(params)

    # reference: https://github.com/bybit-exchange/pybit
    @staticmethod
    def _sign(use_rsa_authentication, secret, param_str):
        def generate_hmac():
            hash = hmac.new(
                bytes(secret, "utf-8"),
                param_str.encode("utf-8"),
                hashlib.sha256,
            )
            return hash.hexdigest()

        def generate_rsa():
            hash = SHA256.new(param_str.encode("utf-8"))
            encoded_signature = base64.b64encode(
                PKCS1_v1_5.new(RSA.importKey(secret)).sign(
                    hash
                )
            )
            return encoded_signature.decode()

        if not use_rsa_authentication:
            return generate_hmac()
        else:
            return generate_rsa()

