import datetime
import json
from urllib import request
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode

from django import forms
from django.forms import Widget
from django.urls import reverse
from django.utils import formats
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _

from jet.dashboard.settings import JET_MODULE_YANDEX_METRIKA_CLIENT_ID, JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET
from ..base import DashboardModule


class YandexMetrikaClient:
    OAUTH_BASE_URL = 'https://oauth.yandex.ru/'
    API_BASE_URL = 'https://api-metrika.yandex.ru/'
    CLIENT_ID = JET_MODULE_YANDEX_METRIKA_CLIENT_ID
    CLIENT_SECRET = JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET

    def __init__(self, access_token=None):
        self.access_token = access_token

    def request(self, base_url, url, data=None, headers=None):
        url = '%s%s' % (base_url, url)

        if data is not None:
            data = urlencode(data).encode()

        if headers is None:
            headers = {}

        req = request.Request(url, data, headers)

        try:
            f = request.urlopen(req)
            result = f.read().decode('utf8')
            result = json.loads(result)
        except URLError as e:
            return None, e

        return result, None

    def get_oauth_authorize_url(self, state=''):
        return '%sauthorize' \
               '?response_type=code' \
               '&state=%s' \
               '&client_id=%s' % (self.OAUTH_BASE_URL, state, self.CLIENT_ID)

    def oauth_request(self, url, data=None):
        return self.request(self.OAUTH_BASE_URL, url, data)

    def oath_token_request(self, code):
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'client_id': self.CLIENT_ID,
            'client_secret': self.CLIENT_SECRET
        }
        return self.oauth_request('token', data)

    def api_request(self, url, data=None):
        headers = None
        if self.access_token is not None:
            headers = {'Authorization': 'OAuth %s' % self.access_token}
        return self.request(self.API_BASE_URL, url, data, headers)

    def api_counters_request(self):
        return self.api_request('counters.json')

    def api_stat_traffic_summary(self, counter, date1, date2, group=None):
        if group is None:
            group = 'day'
        return self.api_request('stat/traffic/summary.json?id=%s&date1=%s&date2=%s&group=%s' % (
            counter,
            date1.strftime('%Y%m%d'),
            date2.strftime('%Y%m%d'),
            group
        ))


class AccessTokenWidget(Widget):
    module = None

    def render(self, name, value, attrs=None, renderer=None):
        if value and len(value) > 0:
            link = '<a href="%s">%s</a>' % (
                reverse('jet-dashboard:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk}),
                force_text(_('Revoke access'))
            )
        else:
            link = '<a href="%s">%s</a>' % (
                reverse('jet-dashboard:yandex-metrika-grant', kwargs={'pk': self.module.model.pk}),
                force_text(_('Grant access'))
            )

        if value is None:
            value = ''

        return format_html('%s<input type="hidden" name="access_token" value="%s">' % (link, value))


class YandexMetrikaSettingsForm(forms.Form):
    access_token = forms.CharField(label=_('Access'), widget=AccessTokenWidget)
    counter = forms.ChoiceField(label=_('Counter'))
    period = forms.ChoiceField(label=_('Statistics period'), choices=(
        (0, _('Today')),
        (6, _('Last week')),
        (30, _('Last month')),
        (31 * 3 - 1, _('Last quarter')),
        (364, _('Last year')),
    ))

    def set_module(self, module):
        self.fields['access_token'].widget.module = module
        self.set_counter_choices(module)

    def set_counter_choices(self, module):
        counters = module.counters()
        if counters is not None:
            self.fields['counter'].choices = (('', '-- %s --' % force_text(_('none'))),)
            self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['site']), counters))
        else:
            label = force_text(_('grant access first')) if module.access_token is None else force_text(
                _('counters loading failed'))
            self.fields['counter'].choices = (('', '-- %s -- ' % label),)


class YandexMetrikaChartSettingsForm(YandexMetrikaSettingsForm):
    show = forms.ChoiceField(label=_('Show'), choices=(
        ('visitors', capfirst(_('visitors'))),
        ('visits', capfirst(_('visits'))),
        ('page_views', capfirst(_('views'))),
    ))
    group = forms.ChoiceField(label=_('Group'), choices=(
        ('day', _('By day')),
        ('week', _('By week')),
        ('month', _('By month')),
    ))


class YandexMetrikaPeriodVisitorsSettingsForm(YandexMetrikaSettingsForm):
    group = forms.ChoiceField(label=_('Group'), choices=(
        ('day', _('By day')),
        ('week', _('By week')),
        ('month', _('By month')),
    ))


class YandexMetrikaBase(DashboardModule):
    settings_form = YandexMetrikaSettingsForm
    ajax_load = True
    contrast = True
    period = None
    access_token = None
    expires_in = None
    token_type = None
    counter = None
    error = None

    def settings_dict(self):
        return {
            'period': self.period,
            'access_token': self.access_token,
            'expires_in': self.expires_in,
            'token_type': self.token_type,
            'counter': self.counter
        }

    def load_settings(self, settings):
        try:
            self.period = int(settings.get('period'))
        except TypeError:
            self.period = 0
        self.access_token = settings.get('access_token')
        self.expires_in = settings.get('expires_in')
        self.token_type = settings.get('token_type')
        self.counter = settings.get('counter')

    def init_with_context(self, context):
        raise NotImplementedError('subclasses of YandexMetrika must provide a init_with_context() method')

    def counters(self):
        client = YandexMetrikaClient(self.access_token)
        counters, exception = client.api_counters_request()

        if counters is not None:
            return counters['counters']
        else:
            return None

    def format_grouped_date(self, date, group):
        if group == 'week':
            date = u'%s — %s' % (
                (date - datetime.timedelta(days=7)).strftime('%d.%m'),
                date.strftime('%d.%m')
            )
        elif group == 'month':
            date = date.strftime('%b, %Y')
        else:
            date = formats.date_format(date, 'DATE_FORMAT')
        return date

    def counter_attached(self):
        if self.access_token is None:
            self.error = mark_safe(_(
                'Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using '
                'widget') % reverse(
                'jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
            return False
        elif self.counter is None:
            self.error = mark_safe(
                _('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse(
                    'jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
            return False
        else:
            return True

    def api_stat_traffic_summary(self, group=None):
        if self.counter_attached():
            date1 = datetime.datetime.now() - datetime.timedelta(days=self.period)
            date2 = datetime.datetime.now()

            client = YandexMetrikaClient(self.access_token)
            result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, group)

            if exception is not None:
                error = _('API request failed.')
                if isinstance(exception, HTTPError) and exception.code == 403:
                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse(
                        'jet-dashboard:update_module', kwargs={'pk': self.model.pk})
                self.error = mark_safe(error)
            else:
                return result


class YandexMetrikaVisitorsTotals(YandexMetrikaBase):
    """
    Yandex Metrika widget that shows total number of visitors, visits and viewers for a particular period of time.
    Period may be following: Today, Last week, Last month, Last quarter, Last year
    """

    title = _('Yandex Metrika visitors totals')
    template = 'jet.dashboard/modules/yandex_metrika_visitors_totals.html'

    #: Which period should be displayed. Allowed values - integer of days
    period = None

    def __init__(self, title=None, period=None, **kwargs):
        kwargs.update({'period': period})
        super(YandexMetrikaVisitorsTotals, self).__init__(title, **kwargs)

    def init_with_context(self, context):
        result = self.api_stat_traffic_summary()

        if result is not None:
            try:
                self.children.append({'title': _('visitors'), 'value': result['totals']['visitors']})
                self.children.append({'title': _('visits'), 'value': result['totals']['visits']})
                self.children.append({'title': _('views'), 'value': result['totals']['page_views']})
            except KeyError:
                self.error = _('Bad server response')


class YandexMetrikaVisitorsChart(YandexMetrikaBase):
    """
    Yandex Metrika widget that shows visitors/visits/viewer chart for a particular period of time.
    Data is grouped by day, week or month
    Period may be following: Today, Last week, Last month, Last quarter, Last year
    """

    title = _('Yandex Metrika visitors chart')
    template = 'jet.dashboard/modules/yandex_metrika_visitors_chart.html'
    style = 'overflow-x: auto;'

    #: Which period should be displayed. Allowed values - integer of days
    period = None

    #: What data should be shown. Possible values: ``visitors``, ``visits``, ``page_views``
    show = None

    #: Sets grouping of data. Possible values: ``day``, ``week``, ``month``
    group = None
    settings_form = YandexMetrikaChartSettingsForm

    class Media:
        js = ('jet.dashboard/vendor/chart.js/Chart.min.js', 'jet.dashboard/modules/yandex_metrika.js')

    def __init__(self, title=None, period=None, show=None, group=None, **kwargs):
        kwargs.update({'period': period, 'show': show, 'group': group})
        super(YandexMetrikaVisitorsChart, self).__init__(title, **kwargs)

    def settings_dict(self):
        settings = super(YandexMetrikaVisitorsChart, self).settings_dict()
        settings['show'] = self.show
        settings['group'] = self.group
        return settings

    def load_settings(self, settings):
        super(YandexMetrikaVisitorsChart, self).load_settings(settings)
        self.show = settings.get('show')
        self.group = settings.get('group')

    def init_with_context(self, context):
        result = self.api_stat_traffic_summary(self.group)

        if result is not None:
            try:
                for data in result['data']:
                    date = datetime.datetime.strptime(data['date'], '%Y%m%d')
                    key = self.show if self.show is not None else 'visitors'
                    self.children.append((date, data[key]))
            except KeyError:
                self.error = _('Bad server response')


class YandexMetrikaPeriodVisitors(YandexMetrikaBase):
    """
    Yandex Metrika widget that shows visitors, visits and viewers for a particular period of time.
    Data is grouped by day, week or month
    Period may be following: Today, Last week, Last month, Last quarter, Last year
    """

    title = _('Yandex Metrika period visitors')
    template = 'jet.dashboard/modules/yandex_metrika_period_visitors.html'

    #: Which period should be displayed. Allowed values - integer of days
    period = None

    #: Sets grouping of data. Possible values: ``day``, ``week``, ``month``
    group = None
    contrast = False
    settings_form = YandexMetrikaPeriodVisitorsSettingsForm

    def __init__(self, title=None, period=None, group=None, **kwargs):
        kwargs.update({'period': period, 'group': group})
        super(YandexMetrikaPeriodVisitors, self).__init__(title, **kwargs)

    def settings_dict(self):
        settings = super(YandexMetrikaPeriodVisitors, self).settings_dict()
        settings['group'] = self.group
        return settings

    def load_settings(self, settings):
        super(YandexMetrikaPeriodVisitors, self).load_settings(settings)
        self.group = settings.get('group')

    def init_with_context(self, context):
        result = self.api_stat_traffic_summary(self.group)

        if result is not None:
            try:
                for data in reversed(result['data']):
                    date = datetime.datetime.strptime(data['date'], '%Y%m%d')
                    date = self.format_grouped_date(date, self.group)
                    self.children.append((date, data))
            except KeyError:
                self.error = _('Bad server response')
