import json
import requests

from io import BytesIO
from time import sleep, time
from zipfile import ZipFile, BadZipfile
from logging import getLogger
from requests.exceptions import RequestException
try:
    from types import NoneType
except ImportError:
    NoneType = type(None)

from .exceptions import BadCredentials, BadRequest


log = getLogger(__name__)


class Client(object):
    API_VERSION = "v1"

    def __init__(
        self,
        environment,
        account_id=None,
        access_token=None,
        json_options=None
    ):
        self.domain, self.domain_stream = environment
        self.access_token = access_token
        self.account_id = account_id
        self.json_options = json_options or {}
        if account_id and not self.get_credentials():
            raise BadCredentials()

    def get_credentials(self):
        """
            See more: http://developer.oanda.com/rest-live/accounts/
        """
        url = "{0}/{1}/accounts/{2}".format(
            self.domain,
            self.API_VERSION,
            self.account_id
        )
        try:
            response = self._Client__call(uri=url)
            assert len(response) > 0
            return response
        except RequestException:
            return False
        except AssertionError:
            return False

    def __get_response(self, uri, params=None, method="get", stream=False):
        """Creates a response object with the given params and option

            Parameters
            ----------
            url : string
                The full URL to request.
            params: dict
                A list of parameters to send with the request.  This
                will be sent as data for methods that accept a request
                body and will otherwise be sent as query parameters.
            method : str
                The HTTP method to use.
            stream : bool
                Whether to stream the response.

            Returns a requests.Response object.
        """
        if not hasattr(self, "session") or not self.session:
            self.session = requests.Session()
            if self.access_token:
                self.session.headers.update(
                    {'Authorization': 'Bearer {}'.format(self.access_token)}
                )

        # Remove empty params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        kwargs = {
            "url": uri,
            "verify": True,
            "stream": stream
        }

        kwargs["params" if method == "get" else "data"] = params

        return getattr(self.session, method)(**kwargs)

    def __call(self, uri, params=None, method="get"):
        """Only returns the response, nor the status_code
        """
        try:
            resp = self.__get_response(uri, params, method, False)
            rjson = resp.json(**self.json_options)
            assert resp.ok
        except AssertionError:
            msg = "OCode-{}: {}".format(resp.status_code, rjson["message"])
            raise BadRequest(msg)
        except Exception as e:
            msg = "Bad response: {}".format(e)
            log.error(msg, exc_info=True)
            raise BadRequest(msg)
        else:
            return rjson

    def __call_stream(self, uri, params=None, method="get"):
        """Returns an stream response
        """
        try:
            resp = self.__get_response(uri, params, method, True)
            assert resp.ok
        except AssertionError:
            raise BadRequest(resp.status_code)
        except Exception as e:
            log.error("Bad response: {}".format(e), exc_info=True)
        else:
            return resp

    def get_instruments(self):
        """
            See more:
            http://developer.oanda.com/rest-live/rates/#getInstrumentList
        """
        url = "{0}/{1}/instruments".format(self.domain, self.API_VERSION)
        params = {"accountId": self.account_id}
        try:
            response = self._Client__call(uri=url, params=params)
            assert len(response) > 0
            return response
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_prices(self, instruments, stream=True):
        """
            See more:
            http://developer.oanda.com/rest-live/rates/#getCurrentPrices
        """
        url = "{0}/{1}/prices".format(
            self.domain_stream if stream else self.domain,
            self.API_VERSION
        )
        params = {"accountId": self.account_id, "instruments": instruments}

        call = {"uri": url, "params": params, "method": "get"}

        try:
            if stream:
                return self._Client__call_stream(**call)
            else:
                return self._Client__call(**call)
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_instrument_history(self, instrument, candle_format="bidask",
                               granularity='S5', count=500,
                               daily_alignment=None, alignment_timezone=None,
                               weekly_alignment="Monday", start=None,
                               end=None):
        """
            See more:
            http://developer.oanda.com/rest-live/rates/#retrieveInstrumentHistory
        """
        url = "{0}/{1}/candles".format(self.domain, self.API_VERSION)
        params = {
            "accountId": self.account_id,
            "instrument": instrument,
            "candleFormat": candle_format,
            "granularity": granularity,
            "count": count,
            "dailyAlignment": daily_alignment,
            "alignmentTimezone": alignment_timezone,
            "weeklyAlignment": weekly_alignment,
            "start": start,
            "end": end,
        }
        try:
            return self._Client__call(uri=url, params=params, method="get")
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_orders(self, instrument=None, count=50):
        """
            See more:
            http://developer.oanda.com/rest-live/orders/#getOrdersForAnAccount
        """
        url = "{0}/{1}/accounts/{2}/orders".format(
            self.domain,
            self.API_VERSION,
            self.account_id
        )
        params = {"instrument": instrument, "count": count}
        try:
            return self._Client__call(uri=url, params=params, method="get")
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_order(self, order_id):
        """
            See more:
            http://developer.oanda.com/rest-live/orders/#getInformationForAnOrder
        """
        url = "{0}/{1}/accounts/{2}/orders/{3}".format(
            self.domain,
            self.API_VERSION,
            self.account_id,
            order_id
        )
        try:
            return self._Client__call(uri=url, method="get")
        except RequestException:
            return False
        except AssertionError:
            return False

    def create_order(self, order):
        """
            See more:
            http://developer.oanda.com/rest-live/orders/#createNewOrder
        """
        url = "{0}/{1}/accounts/{2}/orders".format(
            self.domain,
            self.API_VERSION,
            self.account_id
        )
        try:
            return self._Client__call(
                uri=url,
                params=order.__dict__,
                method="post"
            )
        except RequestException:
            return False
        except AssertionError:
            return False

    def update_order(self, order_id, order):
        """
            See more:
            http://developer.oanda.com/rest-live/orders/#modifyExistingOrder
        """
        url = "{0}/{1}/accounts/{2}/orders/{3}".format(
            self.domain,
            self.API_VERSION,
            self.account_id,
            order_id
        )
        try:
            return self._Client__call(
                uri=url,
                params=order.__dict__,
                method="patch"
            )
        except RequestException:
            return False
        except AssertionError:
            return False

    def close_order(self, order_id):
        """
            See more:
            http://developer.oanda.com/rest-live/orders/#closeOrder
        """
        url = "{0}/{1}/accounts/{2}/orders/{3}".format(
            self.domain,
            self.API_VERSION,
            self.account_id,
            order_id
        )
        try:
            return self._Client__call(uri=url, method="delete")
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_trades(self, max_id=None, count=None, instrument=None, ids=None):
        """ Get a list of open trades

            Parameters
            ----------
            max_id : int
                The server will return trades with id less than or equal
                to this, in descending order (for pagination)
            count : int
                Maximum number of open trades to return. Default: 50 Max
                value: 500
            instrument : str
                Retrieve open trades for a specific instrument only
                Default: all
            ids : list
                A list of trades to retrieve. Maximum number of ids: 50.
                No other parameter may be specified with the ids
                parameter.

            See more:
            http://developer.oanda.com/rest-live/trades/#getListOpenTrades
        """
        url = "{0}/{1}/accounts/{2}/trades".format(
            self.domain,
            self.API_VERSION,
            self.account_id
        )
        params = {
            "maxId": int(max_id) if max_id and max_id > 0 else None,
            "count": int(count) if count and count > 0 else None,
            "instrument": instrument,
            "ids": ','.join(ids) if ids else None
        }

        try:
            return self._Client__call(uri=url, params=params, method="get")
        except RequestException:
            return False
        except AssertionError:
            return False

    def get_trade(self, trade_id):
        """ Get information on a specific trade.

            Parameters
            ----------
            trade_id : int
                The id of the trade to get information on.

            See more:
            http://developer.oanda.com/rest-live/trades/#getInformationSpecificTrade
        """
        url = "{0}/{1}/accounts/{2}/trades/{3}".format(
            self.domain,
            self.API_VERSION,
            self.account_id,
            trade_id
        )
        try:
            return self._Client__call(uri=url, method="get")
        except RequestException:
            return False
        except AssertionError:
            return False

    def update_trade(
        self,
        trade_id,
        stop_loss=None,
        take_profit=None,
        trailing_stop=None
    ):
        """ Modify an existing trade.

            Note: Only the specified parameters will be modified. All
            other parameters will remain unchanged. To remove an
            optional parameter, set its value to 0.

            Parameters
            ----------
            trade_id : int
                The id of the trade to modify.
            stop_loss : number
                Stop Loss value.
            take_profit : number
                Take Profit value.
            trailing_stop : number