import requests
import io
import zipfile
import hmac
import base64
from hashlib import sha256
from urllib.parse import urlencode, parse_qsl
from datetime import timedelta, datetime

from django.http import (
    HttpResponse, JsonResponse,
    HttpResponseRedirect,
    HttpResponseNotFound, HttpResponseForbidden
)
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.admin.sites import site
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.utils import timezone
from django.conf import settings as project_settings
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Count
from django.contrib import auth
from django.contrib.auth.models import User
from django_countries import countries
from constance import config as site_config
import lcoreapi

from ccvpn.common import get_client_ip, get_price_float
from payments.models import ACTIVE_BACKENDS
from .forms import SignupForm, ReqEmailForm
from .models import GiftCode, VPNUser
from .core import core_api
from . import core
from . import graphs
from . import openvpn


def get_locations():
    """ Pretty bad thing that returns get_locations() with translated stuff
    that depends on the request
    """
    countries_d = dict(countries)
    locations = core.get_locations()
    for k, v in locations:
        cc = v['country_code'].upper()
        v['country_name'] = countries_d.get(cc, cc)
    return locations


def ca_crt(request):
    return HttpResponse(content=project_settings.OPENVPN_CA,
                        content_type='application/x-x509-ca-cert')


def logout(request):
    auth.logout(request)
    return redirect('index')


def signup(request):
    if request.user.is_authenticated:
        return redirect('account:index')

    if request.method != 'POST':
        form = SignupForm()
        return render(request, 'ccvpn/signup.html', dict(form=form))

    form = SignupForm(request.POST)

    if not form.is_valid():
        return render(request, 'ccvpn/signup.html', dict(form=form))

    user = User.objects.create_user(form.cleaned_data['username'],
                                    form.cleaned_data['email'],
                                    form.cleaned_data['password'])
    user.save()

    if core.VPN_AUTH_STORAGE == 'core':
        core.create_user(form.cleaned_data['username'], form.cleaned_data['password'])

    try:
        user.vpnuser.referrer = User.objects.get(id=request.session.get('referrer'))
    except User.DoesNotExist:
        pass

    user.vpnuser.campaign = request.session.get('campaign')

    user.vpnuser.save()

    user.backend = 'django.contrib.auth.backends.ModelBackend'
    auth.login(request, user)

    return redirect('account:index')


@login_required
def discourse_login(request):
    sso_secret = project_settings.DISCOURSE_SECRET
    discourse_url = project_settings.DISCOURSE_URL

    if project_settings.DISCOURSE_SSO is not True:
        return HttpResponseNotFound()

    payload = request.GET.get('sso', '')
    signature = request.GET.get('sig', '')

    expected_signature = hmac.new(sso_secret.encode('utf-8'),
                                  payload.encode('utf-8'),
                                  sha256).hexdigest()

    if signature != expected_signature:
        return HttpResponseNotFound()

    if request.method == 'POST' and 'email' in request.POST:
        form = ReqEmailForm(request.POST)
        if not form.is_valid():
            return render(request, 'ccvpn/require_email.html', dict(form=form))

        request.user.email = form.cleaned_data['email']
        request.user.save()

    if not request.user.email:
        form = ReqEmailForm()
        return render(request, 'ccvpn/require_email.html', dict(form=form))

    try:
        payload = base64.b64decode(payload).decode('utf-8')
        payload_data = dict(parse_qsl(payload))
    except (TypeError, ValueError):
        return HttpResponseNotFound()

    payload_data.update({
        'external_id': request.user.id,
        'username': request.user.username,
        'email': request.user.email,
        'require_activation': 'true',
    })

    payload = urlencode(payload_data)
    payload = base64.b64encode(payload.encode('utf-8'))
    signature = hmac.new(sso_secret.encode('utf-8'), payload, sha256).hexdigest()
    redirect_query = urlencode(dict(sso=payload, sig=signature))
    redirect_path = '/session/sso_login?' + redirect_query

    return HttpResponseRedirect(discourse_url + redirect_path)


@login_required
def index(request):
    ref_url = project_settings.ROOT_URL + '?ref=' + str(request.user.id)

    twitter_url = 'https://twitter.com/intent/tweet?'
    twitter_args = {
        'text': _("Awesome VPN! 3€ per month, with a free 7 days trial!"),
        'via': 'CCrypto_VPN',
        'url': ref_url,
        'related': 'CCrypto_VPN,CCrypto_org'
    }

    class price_fn:
        """ Clever hack to get the price in templates with {{price.3}} with
        3 an arbitrary number of months
        """
        def __getitem__(self, months):
            n = int(months) * get_price_float()
            c = project_settings.PAYMENTS_CURRENCY[1]
            return '%.2f %s' % (n, c)

    context = dict(
        title=_("Account"),
        ref_url=ref_url,
        twitter_link=twitter_url + urlencode(twitter_args),
        subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True),
        backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id),
        subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values()
                                if b.backend_has_recurring),
                               key=lambda x: x.backend_id),
        default_backend='paypal',
        recaptcha_site_key=project_settings.RECAPTCHA_SITE_KEY,
        price=price_fn(),
        user_motd=site_config.MOTD_USER,
    )
    return render(request, 'lambdainst/account.html', context)


def captcha_test(grr, request):
    api_url = project_settings.RECAPTCHA_API

    if api_url == 'TEST' and grr == 'TEST-TOKEN':
        # FIXME: i'm sorry.
        return True

    data = dict(secret=project_settings.RECAPTCHA_SECRET_KEY,
                remoteip=get_client_ip(request),
                response=grr)

    try:
        r = requests.post(api_url, data=data)
        r.raise_for_status()
        d = r.json()
        return d.get('success')
    except (requests.ConnectionError, requests.HTTPError, ValueError):
        return False


@login_required
def trial(request):
    if request.method != 'POST' or not request.user.vpnuser.can_have_trial:
        return redirect('account:index')

    grr = request.POST.get('g-recaptcha-response', '')
    if captcha_test(grr, request):
        request.user.vpnuser.give_trial_period()
        request.user.vpnuser.save()
        messages.success(request, _("OK!"))
    else:
        messages.error(request, _("Invalid captcha"))

    return redirect('account:index')


@login_required
def settings(request):
    if request.method != 'POST':
        return render(request, 'lambdainst/settings.html')

    pw = request.POST.get('password')
    pw2 = request.POST.get('password2')
    if pw and pw2:
        if pw != pw2:
            messages.error(request, _("Passwords do not match"))
        else:
            request.user.set_password(pw)

            if core.VPN_AUTH_STORAGE == 'core':
                core.update_user_password(request.user, pw)

            messages.success(request, _("OK!"))

    email = request.POST.get('email')
    if email:
        request.user.email = email
    else:
        request.user.email = ''

    request.user.save()

    return render(request, 'lambdainst/settings.html', dict(title=_("Settings")))


@login_required
def gift_code(request):
    try:
        code = GiftCode.objects.get(code=request.POST.get('code', '').strip(), available=True)
    except GiftCode.DoesNotExist:
        code = None

    if code is None:
        messages.error(request, _("Gift code not found or already used."))
    elif not code.use_on(request.user):
        messages.error(request, _("Gift code only available to free accounts."))
    else:
        messages.success(request, _("OK!"))

    return redirect('account:index')


@login_required
def logs(request):
    page_size = 20
    page = int(request.GET.get('page', 0))
    offset = page * page_size

    base = core_api.info['current_instance']
    path = '/users/' + request.user.username + '/sessions/'
    try:
        l = core_api.get(base + path, offset=offset, limit=page_size)
        total_count = l['total_count']
        items = l['items']
    except lcoreapi.APINotFoundError:
        total_count = 0
        items = []
    return render(request, 'lambdainst/logs.html', {
        'sessions': items,
        'page': page,
        'prev': page - 1 if page > 0 else None,
        'next': page + 1 if offset + page_size < total_count else None,
        'last_page': total_count // page_size,
        'title': _("Logs"),
    })


@login_required
def config(request):
    return render(request, 'lambdainst/config.html', dict(
        title=_("Config"),
        config_os=openvpn.CONFIG_OS,
        config_countries=(c for _, c in get_locations()),
        config_protocols=openvpn.PROTOCOLS,
    ))


@login_required
def config_dl(request):
    allowed_cc = [cc for (cc, _) in get_locations()]

    os = request.GET.get('client_os')

    common_options = {
        'username': request.user.username,
        'protocol': request.GET.get('protocol'),
        'os': os,
        'http_proxy': request.GET.get('http_proxy'),
        'ipv6': 'enable_ipv6' in request.GET,
    }

    # Should be validated since it's used in the filename
    # other common options are only put in the config file
    protocol = common_options['protocol']
    if protocol not in ('udp', 'udpl', 'tcp'):
        return HttpResponseNotFound()

    location = request.GET.get('gateway')

    if location == 'all':
        # Multiple gateways in a zip archive

        f = io.BytesIO()
        z = zipfile.ZipFile(f, mode='w')

        for gw_name in allowed_cc + ['random']:
            if os == 'chromeos':
                filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol)
            else:
                filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)
            config = openvpn.make_config(gw_name=gw_name, **common_options)
            z.writestr(filename, config.encode('utf-8'))

        z.close()

        r = HttpResponse(content=f.getvalue(), content_type='application/zip')
        r['Content-Disposition'] = 'attachment; filename="%s.zip"' % filename
        return r
    else:
        # Single gateway
        if location[3:] in allowed_cc:
            gw_name = location[3:]
        else:
            gw_name = 'random'
        if os == 'chromeos':
            filename = 'ccrypto-%s-%s.onc' % (gw_name, protocol)
        else:
            filename = 'ccrypto-%s-%s.ovpn' % (gw_name, protocol)

        config = openvpn.make_config(gw_name=gw_name, **common_options)

        if 'plain' in request.GET:
            return HttpResponse(content=config, content_type='text/plain')
        else:
            if os == 'chromeos':
                r = HttpResponse(content=config, content_type='application/x-onc')
            else:
                r = HttpResponse(content=config, content_type='application/x-openvpn-profile')
            r['Content-Disposition'] = 'attachment; filename="%s"' % filename
            return r


@csrf_exempt
def api_auth(request):
    if request.method != 'POST':
        return HttpResponseNotFound()

    if core.VPN_AUTH_STORAGE != 'inst':
        return HttpResponseNotFound()

    username = request.POST.get('username')
    password = request.POST.get('password')
    secret = request.POST.get('secret')

    if secret != core.LCORE_INST_SECRET:
        return HttpResponseForbidden(content="Invalid secret")

    user = authenticate(username=username, password=password)
    if not user or not user.is_active:
        return JsonResponse(dict(status='fail', message="Invalid credentials"))

    if not user.vpnuser.is_paid:
        return JsonResponse(dict(status='fail', message="Not allowed to connect"))

    user.vpnuser.last_vpn_auth = timezone.now()
    user.vpnuser.save()

    return JsonResponse(dict(status='ok'))


def api_locations(request):
    def format_loc(cc, l):
        msg = ' [%s]' % l['message'] if l['message'] else ''
        return {
            'country_name': l['country_name'] + msg,
            'country_code': cc,
            'hostname': l['hostname'],
            'bandwidth': l['bandwidth'],
            'servers': l['servers'],
        }
    return JsonResponse(dict(locations=[format_loc(cc, l) for cc, l in get_locations()]))


def status(request):
    locations = get_locations()

    ctx = {
        'title': _("Status"),
        'n_users': VPNUser.objects.filter(expiration__gte=timezone.now()).count(),
        'n_sess': core.current_active_sessions(),
        'n_gws': sum(l['servers'] for cc, l in locations),
        'n_countries': len(set(cc for cc, l in locations)),
        'total_bw': sum(l['bandwidth'] for cc, l in locations),
        'locations': locations,
    }
    return render(request, 'lambdainst/status.html', ctx)


@user_passes_test(lambda user: user.is_staff)
def admin_status(request):
    graph_name = request.GET.get('graph_name')
    graph_period = request.GET.get('period')
    if graph_period not in ('y', 'm'):
        graph_period = 'm'
    if graph_name:
        if graph_name == 'users':
            content = graphs.users_graph(graph_period)
        elif graph_name == 'payments_paid':
            content = graphs.payments_paid_graph(graph_period)
        elif graph_name == 'payments_success':
            content = graphs.payments_success_graph(graph_period)
        else:
            return HttpResponseNotFound()
        return HttpResponse(content=content, content_type='image/svg+xml')

    payment_status = ((b, b.get_info()) for b in ACTIVE_BACKENDS.values())
    payment_status = ((b, i) for (b, i) in payment_status if i)

    ctx = {
        'api_status': {k: str(v) for k, v in core_api.info.items()},
        'payment_backends': sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id),
        'payment_status': payment_status,
    }
    ctx.update(site.each_context(request))
    return render(request, 'lambdainst/admin_status.html', ctx)


@user_passes_test(lambda user: user.is_staff)
def admin_ref(request):
    last_week = datetime.now() - timedelta(days=7)
    last_month = datetime.now() - timedelta(days=30)

    top_ref = User.objects.annotate(n_ref=Count('referrals')).order_by('-n_ref')[:10]
    top_ref_week = User.objects.filter(referrals__user__date_joined__gt=last_week) \
                               .annotate(n_ref=Count('referrals')) \
                               .order_by('-n_ref')[:10]
    top_ref_month = User.objects.filter(referrals__user__date_joined__gt=last_month) \
                                .annotate(n_ref=Count('referrals')) \
                                .order_by('-n_ref')[:10]

    ctx = {
        'top_ref': top_ref,
        'top_ref_week': top_ref_week,
        'top_ref_month': top_ref_month,
    }
    ctx.update(site.each_context(request))
    return render(request, 'lambdainst/admin_ref.html', ctx)





