import asyncio
import logging
import time
from itertools import chain

import aiohttp

from uz.client.exceptions import (
    FailedObtainToken, HTTPError, BadRequest, ResponseError, ImproperlyConfigured)
from uz.client.model import DATE_FMT, Train, Station, Coach
from uz.client.utils import parse_gv_token, get_random_user_agent


logger = logging.getLogger('uz.client')


class UZClient(object):

    base_url = 'http://booking.uz.gov.ua/en'

    def __init__(self, session=None, request_timeout=10):
        self._session = session
        self.request_timeout = request_timeout

        self._token_lock = asyncio.Lock()
        self._token = None
        self._token_date = 0
        self._token_max_age = 600  # 10 minutes
        self._user_agent = None

    def __enter__(self):
        self._session = aiohttp.ClientSession()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.session.close()

    @property
    def session(self):
        if self._session is None:
            raise ImproperlyConfigured('Session is not configured')
        return self._session

    @property
    def user_agent(self):
        if self._user_agent is None:
            self._user_agent = get_random_user_agent()
        return self._user_agent

    def _is_token_outdated(self):
        return (time.time() - self._token_date) > self._token_max_age

    async def get_token(self):
        if self._is_token_outdated():
            async with self._token_lock:
                if self._is_token_outdated():
                    self._user_agent = None
                    self.session.cookies.clear()
                    headers = {'User-Agent': self.user_agent}
                    page = await self.call('', raw=True, headers=headers)
                    page = page.decode('utf-8')
                    self._token = parse_gv_token(page)
                    if self._token is None:
                        raise FailedObtainToken(page)
                    self._token_date = time.time()
        return self._token

    async def get_headers(self):
        return {
            'User-Agent': self.user_agent,
            'GV-Ajax': '1',
            'GV-Referer': self.base_url,
            'GV-Token': await self.get_token()
        }

    def uri(self, endpoint):
        return '{}/{}'.format(self.base_url, endpoint)

    def get_session_id(self):
        sid = self.session.cookies.get('_gv_sessid')
        return sid and sid.value

    async def call(self, endpoint, method='POST', raw=False, *args, **kwargs):
        if 'headers' not in kwargs:
            kwargs['headers'] = await self.get_headers()

        uri = self.uri(endpoint)
        logger.debug('Fetching: %s', uri)
        logger.debug('Headers: %s', kwargs['headers'])
        logger.debug('Cookies: %s', self.session.cookies)

        with aiohttp.Timeout(self.request_timeout):
            async with self.session.request(
                    method, uri, *args, **kwargs) as response:
                body = await response.read()
                if not response.status == 200:
                    try:
                        json = await response.json()
                    except Exception:  # TODO: narrow exception
                        json = None
                    ex = BadRequest if response.status == 400 else HTTPError
                    raise ex(response.status, body, kwargs.get('data'), json)
                if raw:
                    return body
                json = await response.json()
                if json.get('error'):
                    raise ResponseError(response.status, body, kwargs.get('data'), json)
                return json

    async def search_stations(self, name):
        endpoint = 'purchase/station/{}/'.format(name)
        result = await self.call(endpoint)
        return [Station.from_dict(i) for i in result['value']]

    async def fetch_first_station(self, name):
        stations = await self.search_stations(name)
        return stations and stations[0] or None

    async def list_trains(self, date, source_station, destination_station):
        data = dict(
            station_id_from=source_station.id,
            station_id_till=destination_station.id,
            date_dep=date.strftime(DATE_FMT),
            time_dep='00:00',
            time_dep_till='',
            another_ec=0,
            search='')
        result = await self.call('purchase/search/', data=data)
        return [Train.from_dict(i) for i in result['value']]

    async def fetch_train(self, date, source_station, destination_station, train_num):
        trains = await self.list_trains(date, source_station, destination_station)
        for train in trains:
            if train.num == train_num:
                return train

    async def list_coaches(self, train, coach_type):
        data = dict(
            station_id_from=train.source_station.id,
            station_id_till=train.destination_station.id,
            train=train.num,
            model=train.model,
            date_dep=train.departure_time.timestamp,
            round_trip=0,
            another_ec=0,
            coach_type=coach_type.letter
        )
        result = await self.call('purchase/coaches/', data=data)
        return [Coach.from_dict(i) for i in result['coaches']]

    async def list_seats(self, train, coach):
        data = dict(
            station_id_from=train.source_station.id,
            station_id_till=train.destination_station.id,
            train=train.num,
            coach_num=coach.num,
            coach_class=coach.klass,
            coach_type_id=coach.type_id,
            date_dep=train.departure_time.timestamp
        )
        result = await self.call('purchase/coach/', data=data)
        return set(chain(*result['value']['places'].values()))

    async def book_seat(self, train, coach, seat, firstname, lastname):
        data = dict(
            code_station_from=train.source_station.id,
            code_station_to=train.destination_station.id,
            train=train.num,
            date=train.departure_time.timestamp,
            round_trip=0)

        place = dict(
            ord=0,
            coach_num=coach.num,
            coach_class=coach.klass,
            coach_type_id=coach.type_id,
            place_num=seat,
            firstname=firstname,
            lastname=lastname,
            bedding=0,
            child='',
            stud='',
            transp=0,
            reserve=0)
        for key, value in place.items():
            data['places[0][{}]'.format(key)] = value
        result = await self.call('cart/add/', data=data)
        return result