try:
    import urllib3.contrib.pyopenssl
    urllib3.contrib.pyopenssl.inject_into_urllib3()
except ImportError:
    pass

import iso8601
import uuid
import logging
import json
import requests

from retrying import retry
from datetime import MINYEAR, datetime
from pytz import timezone
from gevent import sleep
from hashlib import sha1
from redis import Redis
from redis.sentinel import Sentinel
from pkg_resources import parse_version
from restkit.wrappers import BodyWrapper
from barbecue import chef
from fractions import Fraction
from munch import Munch
from zope.interface import implementer

from openprocurement.auction.interfaces import IFeedItem


logger = logging.getLogger('Auction Worker')

EXTRA_LOGGING_VALUES = {
    'X-Request-ID': 'JOURNAL_REQUEST_ID',
    'X-Clint-Request-ID': 'JOURNAL_CLIENT_REQUEST_ID'
}


def generate_request_id(prefix=b'auction-req-'):
    return prefix + str(uuid.uuid4()).encode('ascii')


def filter_by_bidder_id(bids, bidder_id):
    """
    >>> bids = [
    ...     {"bidder_id": "1", "amount": 100},
    ...     {"bidder_id": "1", "amount": 200},
    ...     {"bidder_id": "2", "amount": 101}
    ... ]

    >>> filter_by_bidder_id(bids, "1")
    [{'amount': 100, 'bidder_id': '1'}, {'amount': 200, 'bidder_id': '1'}]

    >>> filter_by_bidder_id(bids, "2")
    [{'amount': 101, 'bidder_id': '2'}]

    """
    return [bid for bid in bids if bid['bidder_id'] == bidder_id]


def filter_start_bids_by_bidder_id(bids, bidder):
    """
    """
    return [bid for bid in bids
            if bid['bidders'][0]['id']['name'] == bidder]


def get_time(item):
    """
    >>> date = get_time({"time": "2015-01-04T15:40:44Z"}) # doctest: +NORMALIZE_WHITESPACE
    >>> date.utctimetuple()  # doctest: +NORMALIZE_WHITESPACE
    time.struct_time(tm_year=2015, tm_mon=1, tm_mday=4, tm_hour=15, tm_min=40,
                     tm_sec=44, tm_wday=6, tm_yday=4, tm_isdst=0)

    >>> date = get_time({"date": "2015-01-04T15:40:44Z"})
    >>> date.utctimetuple()  # doctest: +NORMALIZE_WHITESPACE
    time.struct_time(tm_year=2015, tm_mon=1, tm_mday=4, tm_hour=15, tm_min=40,
                     tm_sec=44, tm_wday=6, tm_yday=4, tm_isdst=0)

    >>> date = get_time({})
    >>> date.utctimetuple()  # doctest: +NORMALIZE_WHITESPACE
    time.struct_time(tm_year=0, tm_mon=12, tm_mday=31, tm_hour=21, tm_min=58,
                     tm_sec=0, tm_wday=6, tm_yday=366, tm_isdst=0)
    """
    if item.get('time', ''):
        bid_time = iso8601.parse_date(item['time'])
    elif item.get('date', ''):
        bid_time = iso8601.parse_date(item['date'])
    else:
        bid_time = datetime(MINYEAR, 1, 1, tzinfo=timezone('Europe/Kiev'))
    return bid_time


def sorting_by_amount(bids, reverse=True):
    """
    >>> bids = [
    ...     {'amount': 3955.0, 'bidder_id': 'df1', 'time': '2015-04-24T11:07:30.723296+03:00'},
    ...     {'amount': 3966.0, 'bidder_id': 'df2', 'time': '2015-04-24T11:07:30.723296+03:00'},
    ...     {'amount': 3955.0, 'bidder_id': 'df4', 'time': '2015-04-23T15:48:41.971644+03:00'},
    ... ]
    >>> sorting_by_amount(bids)  # doctest: +NORMALIZE_WHITESPACE
    [{'amount': 3966.0, 'bidder_id': 'df2', 'time': '2015-04-24T11:07:30.723296+03:00'},
     {'amount': 3955.0, 'bidder_id': 'df1', 'time': '2015-04-24T11:07:30.723296+03:00'},
     {'amount': 3955.0, 'bidder_id': 'df4', 'time': '2015-04-23T15:48:41.971644+03:00'}]

    >>> bids = [
    ...     {'amount': 3966.0, 'bidder_id': 'df1', 'time': '2015-04-24T11:07:20+03:00'},
    ...     {'amount': 3966.0, 'bidder_id': 'df2', 'time': '2015-04-24T11:07:30+03:00'},
    ...     {'amount': 3966.0, 'bidder_id': 'df4', 'time': '2015-04-24T11:07:40+03:00'},
    ... ]
    >>> sorting_by_amount(bids)  # doctest: +NORMALIZE_WHITESPACE
    [{'amount': 3966.0, 'bidder_id': 'df4', 'time': '2015-04-24T11:07:40+03:00'},
     {'amount': 3966.0, 'bidder_id': 'df2', 'time': '2015-04-24T11:07:30+03:00'},
     {'amount': 3966.0, 'bidder_id': 'df1', 'time': '2015-04-24T11:07:20+03:00'}]
    """
    def bids_compare(bid1, bid2):
        if "amount_features" in bid1 and "amount_features" in bid2:
            full_amount_bid1 = Fraction(bid1["amount_features"])
            full_amount_bid2 = Fraction(bid2["amount_features"])
        else:
            full_amount_bid1 = bid1["amount"]
            full_amount_bid2 = bid2["amount"]
        if full_amount_bid1 == full_amount_bid2:
            time_of_bid1 = get_time(bid1)
            time_of_bid2 = get_time(bid2)
            return - cmp(time_of_bid2, time_of_bid1)
        else:
            return cmp(full_amount_bid1, full_amount_bid2)

    return sorted(bids, reverse=reverse, cmp=bids_compare)


def sorting_start_bids_by_amount(bids, features=None, reverse=True):
    """
    >>> from json import load
    >>> import os
    >>> data = load(open(os.path.join(os.path.dirname(__file__),
    ...                               'tests/functional/data/tender_simple.json')))
    >>> sorted_data = sorting_start_bids_by_amount(data['data']['bids'])

    """
    def get_amount(item):
        return item['value']['amount']

    # return sorted(bids, key=get_amount, reverse=reverse)
    return chef(bids, features=features)


def sorting_by_time(bids, reverse=True):
    return sorted(bids, key=get_time, reverse=reverse)


def get_latest_bid_for_bidder(bids, bidder_id):
    return sorted(filter_by_bidder_id(bids, bidder_id),
                  key=get_time, reverse=True)[0]


def get_latest_start_bid_for_bidder(bids, bidder):
    return sorted(filter_start_bids_by_bidder_id(bids, bidder),
                  key=get_time, reverse=True)[0]


def get_tender_data(tender_url, user="", password="", retry_count=10,
                    request_id=None, session=requests):
    if not request_id:
        request_id = generate_request_id()
    extra_headers = {'content-type': 'application/json', 'X-Client-Request-ID': request_id}

    if user or password:
        auth = (user, password)
    else:
        auth = None
    for iteration in xrange(retry_count):
        try:
            logging.info("Get data from {}".format(tender_url),
                         extra={"JOURNAL_REQUEST_ID": request_id})
            response = session.get(tender_url, auth=auth, headers=extra_headers,
                                   timeout=300)
            if response.ok:
                logging.info("Response from {}: status: {} text: {}".format(
                    tender_url, response.status_code, response.text),
                    extra={"JOURNAL_REQUEST_ID": request_id}
                )
                return response.json()
            else:
                logging.error("Response from {}: status: {} text: {}".format(
                    tender_url, response.status_code, response.text),
                    extra={"JOURNAL_REQUEST_ID": request_id}
                )
                if response.status_code == 403:
                    for error in response.json()["errors"]:
                        if error["description"].startswith('Can\'t get auction info'):
                            return None
        except requests.exceptions.RequestException, e:
            logging.error(
                "Request error {} error: {}".format(tender_url, e),
                extra={"JOURNAL_REQUEST_ID": request_id}
            )
        except Exception, e:
            logging.error(
                "Unhandled error {} error: {}".format(tender_url, e),
                extra={"JOURNAL_REQUEST_ID": request_id}
            )
        logging.info("Wait before retry...",
                     extra={"JOURNAL_REQUEST_ID": request_id})
        sleep(pow(iteration, 2))
    return None


def make_request(url, data=None, files=None, user="", password="",
                 retry_count=10, method='patch', request_id=None, session=None):
    if not session:
        session = requests.Session()
    if not request_id:
        request_id = generate_request_id()
    extra_headers = {'X-Client-Request-ID': request_id}
    if data:
        extra_headers['content-type'] = 'application/json'

    if user or password:
        auth = (user, password)
    else:
        auth = None
    for iteration in xrange(retry_count):
        try:
            if data:
                response = getattr(session, method)(
                    url,
                    auth=auth,
                    headers=extra_headers,
                    data=json.dumps(data),
                    timeout=300
                )
            else:
                response = getattr(session, method)(
                    url,
                    auth=auth,
                    headers=extra_headers,
                    files=files,
                    timeout=300
                )

            if response.ok:
                logging.info("Response from {}: status: {} text: {}".format(
                    url, response.status_code, response.text),
                    extra={"JOURNAL_REQUEST_ID": request_id}
                )
                return response.json()
            elif response.status_code == 412 and response.text:
                get_tender_data(url, user=user, password=password,
                                request_id=request_id, session=session)
            elif response.status_code == 403:
                logging.info("Response from {}: status: {} text: {}".format(
                    url, response.status_code, response.text),
                    extra={"JOURNAL_REQUEST_ID": request_id}
                )
                return None
            else:
                logging.error("Response from {}: status: {} text: {}".format(
                    url, response.status_code, response.text),
                    extra={"JOURNAL_REQUEST_ID": request_id}
                )
        except requests.exceptions.RequestException, e:
            logging.error("Request error {} error: {}".format(
                url,
                e),
                extra={"JOURNAL_REQUEST_ID": request_id}
            )
        except Exception, e:
            logging.error("Unhandled error {} error: {}".format(
                url,
                e),
                extra={"JOURNAL_REQUEST_ID": request_id}
            )
        logging.info("Wait before retry...",
                     extra={"JOURNAL_REQUEST_ID": request_id})
        sleep(pow(iteration, 2))


def do_until_success(func, args=(), kw={}, repeat=10):
    for iteration in xrange(repeat):
        try:
            return func(*args, **kw)
        except Exception, e:
            logging.error("Error {} while call {} with args: {}, kw: {}".format(
                e, func, args, kw
            ))
        repeat -= 1
        if repeat == 0:
            logging.error("Stop running {} with args: {}, kw: {}".format(
                func, args, kw
            ))
            break
        sleep(pow(iteration, 2))


def calculate_hash(bidder_id, hash_secret):
    digest = sha1(hash_secret)
    digest.update(bidder_id)
    return digest.hexdigest()


def get_database(config, master=True):
    if config['sentinel']:
        sentinal = Sentinel(config['sentinel'], socket_timeout=0.1,
                            password=config['redis_password'], db=config['redis_database'])
        if master:
            return sentinal.master_for(config['sentinel_cluster_name'])
        else:
            return sentinal.slave_for(config['sentinel_cluster_name'])
    else:
        return Redis.from_url(config['redis'])


@retry(stop_max_attempt_number=3)
def create_mapping(config, auction_id, auction_url):
    return get_database(config).set(auction_id, auction_url)


@retry(stop_max_attempt_number=3)
def get_mapping(config, auction_id, master=False):
    return get_database(config).get(auction_id)


@retry(stop_max_attempt_number=3)
def delete_mapping(config, auction_id):
    return get_database(config).delete(auction_id)


def prepare_extra_journal_fields(headers):
    extra = {}
    for key in EXTRA_LOGGING_VALUES:
        if key in headers:
            extra[EXTRA_LOGGING_VALUES[key]] = headers[key]
    return extra


class StreamWrapper(BodyWrapper):
    """Stream Wrapper fot Proxy Reponse"""
    stop_stream = False

    def __init__(self, resp, connection):
        super(StreamWrapper, self).__init__(resp, connection)

    def close(self):
        """ release connection """
        if self._closed:
            return
        self.eof = True
        self.resp.should_close = True

        if not self.eof:
            self.body.read()
        self.connection.release(True)
        self._closed = True

    def next(self):
        if not self.stop_stream:
            try:
                return super(StreamWrapper, self).next()
            except Exception:
                raise StopIteration


def get_bidder_id(app, session):
    if 'remote_oauth' in session and 'client_id' in session:
        if session['remote_oauth'] in app.logins_cache:
            return app.logins_cache[session['remote_oauth']]
        else:
            resp = app.remote_oauth.get('me')
            if resp.status == 200:
                app.logins_cache[session['remote_oauth']] = resp.data
                return resp.data
            else:
                return False


def unsuported_browser(request):
    if request.user_agent.browser == 'msie':
        if parse_version(request.user_agent.version) <= parse_version('9'):
            return True
        # Add to blacklist IE11
        if parse_version(request.user_agent.version) >= parse_version('11'):
            return True
    elif request.user_agent.browser == 'opera':
        if 'Opera Mini' in request.user_agent.string:
            return True
    return False


def filter_amount(stage):
    if 'amount' in stage:
        del stage['amount']
    if 'coeficient' in stage:
        del stage['coeficient']
    return stage


def get_auction_worker_configuration_path(_for, view_value, key='api_version'):
    value = view_value.get(key, '')
    config = _for.config['main'].get(
        view_value.get('procurementMethodType'),
        _for.config['main']
    )
    if value:
        path = config.get(
            'auction_worker_config_for_{}_{}'.format(key, value),
            config.get('auction_worker_config', '')
        )
        if not path:
            path = _for.config['main'].get(
                'auction_worker_config_for_{}_{}'.format(key, value),
                _for.config['main']['auction_worker_config']
            )
        return path
    else:
        return config.get(
            'auction_worker_config',
            _for.config['main']['auction_worker_config']
        )


def prepare_auction_worker_cmd(_for, tender_id, cmd, item,
                               lot_id='', with_api_version=''):
    config = _for.config['main'].get(
        item.get('procurementMethodType'),
        _for.config['main']
    )
    params = [
        config.get(
            'auction_worker', _for.config['main'].get('auction_worker')),
        cmd, tender_id,
        get_auction_worker_configuration_path(_for, item)
    ]
    if lot_id:
        params += ['--lot', lot_id]

    if with_api_version:
        params += ['--with_api_version', with_api_version]
    return params


@implementer(IFeedItem)
class FeedItem(Munch):
    """"""