from math import ceil
import base64
import hmac
from datetime import datetime, timedelta
from hashlib import sha256
from urllib.parse import parse_qsl, urlencode

import requests
from constance import config as site_config
from django.conf import settings as project_settings
from django.contrib import auth, messages
from django.contrib.admin.sites import site
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.db.models import Count
from django.db import transaction
from django.http import (HttpResponse,
                         HttpResponseNotFound, HttpResponseRedirect,
                         JsonResponse)
from django.shortcuts import redirect, render
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
from django_countries import countries
import django_lcore
import lcoreapi

from ccvpn.common import get_client_ip, get_price_float
from payments.models import ACTIVE_BACKENDS
from .forms import SignupForm, ReqEmailForm, WgPeerForm
from .models import VPNUser
from . import graphs


def get_locations():
    """ Pretty bad thing that returns get_locations() with translated stuff
    that depends on the request
    """
    countries_d = dict(countries)
    locations = django_lcore.get_clusters()
    for (_), loc in locations:
        code = loc['country_code'].upper()
        loc['country_name'] = countries_d.get(code, code)

        loc['load_percent'] = ceil((loc['usage'].get('net', 0) / (loc['bandwidth'] / 1e6)) * 100)
    locations = list(sorted(locations, key=lambda l: l[0]))
    return locations


def log_errors(request, form):
    errors = []
    for field in form:
        for e in field.errors:
            errors.append(e)
    messages.add_message(request, messages.ERROR, ", ".join(errors))


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)

    grr = request.POST.get('g-recaptcha-response', '')
    if captcha_test(grr, request):
        request.session['signup_captcha_pass'] = True
    elif not request.session.get('signup_captcha_pass'):
        messages.error(request, _("Invalid captcha. Please try again"))
        return render(request, 'ccvpn/signup.html', dict(form=form))

    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()

    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.add_paid_time(timedelta(days=7), 'trial')

    user.vpnuser.save()
    user.vpnuser.lcore_sync()

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

    # invalidate that captcha
    request.session['signup_captcha_pass'] = False

    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(),
        backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_display_name),
        subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values()
                                if b.backend_has_recurring),
                               key=lambda x: x.backend_id),
        default_backend='paypal',
        hcaptcha_site_key=project_settings.HCAPTCHA_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.HCAPTCHA_API

    if not project_settings.HCAPTCHA_SITE_KEY:
        return True

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

    data = dict(secret=project_settings.HCAPTCHA_SECRET_KEY,
                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


def make_export_zip(user, name):
    import io
    import zipfile
    import json

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

    gw_cache = {}

    def process_wg_peer(item):
        keys = {"gateway_port", "id", "local_ipv4", "local_ipv6", "name", "object",
                "private_key", "public_key"}
        return {k: v for (k, v) in item.items() if k in keys}

    def process_ovpn_sess(item):
        keys = {"connect_date", "disconnect_date", "remote", "object", "protocol", "id",
                "stats", "tunnel"}

        def convert(v):
            if isinstance(v, datetime):
                return v.isoformat()
            return v

        obj = {k: convert(v) for (k, v) in item.items() if k in keys}
        gw_url = item["gateway"]["href"]
        if gw_url not in gw_cache:
            gw = django_lcore.api.get(gw_url)
            gw_cache[gw_url] = {
                "name": gw["name"],
                "ipv4": gw["main_addr"]["ipv4"],
                "ipv6": gw["main_addr"]["ipv6"],
            }
        obj["gateway"] = gw_cache[gw_url]
        return obj

    def process_payments(item):
        obj = {
            "id": item.id,
            "backend": item.backend_id,
            "status": item.status,
            "created": item.created.isoformat(),
            "confirmed": item.confirmed_on.isoformat() if item.confirmed_on else None,
            "amount": item.amount / 100,
            "paid_amount": item.amount / 100,
            "time": item.time.total_seconds(),
            "external_id": item.backend_extid,
        }
        if item.subscription:
            obj["subscription"] = item.subscription.backend_extid
        else:
            obj["subscription"] = None
        return obj

    with z.open(name + "/account.json", "w") as jf:
        jf.write(json.dumps({
            "username": user.username,
            "email": user.email,
            "date_joined": user.date_joined.isoformat(),
            "expiration": user.vpnuser.expiration.isoformat() if user.vpnuser.expiration else None,
        }, indent=2).encode('ascii'))

    with z.open(name + "/wireguard_peers.json", "w") as jf:
        try:
            keys = list(map(process_wg_peer, django_lcore.api.get_wg_peers(user.username)))
        except lcoreapi.APINotFoundError:
            keys = []
        jf.write(json.dumps(keys, indent=2).encode('ascii'))

    with z.open(name + "/openvpn_logs.json", "w") as jf:
        base = django_lcore.api.info['current_instance']
        next_page = '/users/' + user.username + '/sessions/'
        try:
            items = django_lcore.api.get(base + next_page).list_iter()
        except lcoreapi.APINotFoundError:
            items = []
        items = list(map(process_ovpn_sess, items))
        jf.write(json.dumps(items, indent=2).encode('ascii'))

    with z.open(name + "/payments.json", "w") as jf:
        items = user.payment_set.all()
        items = list(map(process_payments, items))
        jf.write(json.dumps(items, indent=2).encode('ascii'))
    
    z.close()
    return f.getvalue()


def deactivate_user(user):
    """ clear most information from a user, keeping the username and id """
    user.vpnuser.clear_fields()
    user.vpnuser.save()

    user.is_active = False
    user.email = ""
    user.password = ""
    user.save()

    user.payment_set.update(backend_data="null")
    user.subscription_set.update(backend_data="null")

    django_lcore.sync_user(user.vpnuser)


@login_required
def settings(request):
    can_delete = request.user.vpnuser.get_subscription() is None

    if request.method != 'POST':
        return render(request, 'lambdainst/settings.html', dict(
            can_delete=can_delete,
        ))

    action = request.POST.get('action')

    current_pw = request.POST.get('current_password')
    if not request.user.check_password(current_pw):
        messages.error(request, _("Invalid password"))
        return redirect('lambdainst:account_settings')

    if action == 'email':
        email = request.POST.get('email')
        if email:
            request.user.email = email
            messages.success(request, _("OK! Email address changed."))
        else:
            request.user.email = ''
            messages.success(request, _("OK! Email address unset."))

        request.user.save()
        return redirect('lambdainst:account_settings')

    elif action == 'password':
        pw = request.POST.get('password')
        pw2 = request.POST.get('password2')
        if pw != pw2 or not pw:
            messages.error(request, _("Password and confirmation do not match"))
        else:
            request.user.set_password(pw)
            messages.success(request, _("OK! Password changed."))
            request.user.save()
            django_lcore.sync_user(request.user.vpnuser)
        return redirect('lambdainst:account_settings')

    elif action == 'export':
        timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
        data = make_export_zip(request.user, timestamp)
        r = HttpResponse(content=data, content_type="application/zip")
        r["Content-Disposition"] = 'attachment; filename="ccvpn-export-{}-{}.zip"'.format(request.user.username, timestamp)
        return r

    elif action == 'delete' and can_delete:
        with transaction.atomic():
            deactivate_user(request.user)
        logout(request)
        messages.success(request, _("OK! Your account has been deactivated."))
        return redirect('/')

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


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


def api_locations(request):
    def format_loc(cc, l):
        msg = ""
        tags = l.get('tags', {})
        message = tags.get('message')
        if message:
            msg = " [%s]" % message
        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': _("Servers"),
        '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)

    lcore_keys = {'core_name', 'core_now', 'core_version', 'current_instance', 'key_public'}

    ctx = {
        'api_status': {k: str(v) for k, v in django_lcore.api.info.items() if k in lcore_keys},
        '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)


@login_required
def wireguard(request):
    api = django_lcore.api
    if request.method == 'POST':
        action = request.POST.get('action')
        if action == 'delete_key':
            key = api.get_wg_peer(request.user.username, request.POST.get('peer_id'))
            if key:
                key.delete()
        elif action == 'set_name':
            key = api.get_wg_peer(request.user.username, request.POST.get('peer_id'))
            if key:
                name = request.POST.get('name')
                if name:
                    name = name[:21]
                key.rename(name)
        return redirect(request.path)

    try:
        keys = api.get_wg_peers(request.user.username)
    except lcoreapi.APINotFoundError:
        django_lcore.sync_user(request.user.vpnuser)
        keys = []

    context = dict(
        can_create_key=len(keys) < int(site_config.WIREGUARD_MAX_PEERS),
        menu_item='wireguard',
        enabled=request.user.vpnuser.is_paid,
        config_countries=[(k, v) for (k, v) in django_lcore.get_clusters()],
        keys=keys,
        locations=get_locations(),
    )
    return render(request, 'lambdainst/wireguard.html', context)


@login_required
def wireguard_new(request):
    if not request.user.vpnuser.is_paid:
        return redirect('account:index')

    try:
        keys = django_lcore.api.get_wg_peers(request.user.username)
    except lcoreapi.APINotFoundError:
        django_lcore.sync_user(request.user.vpnuser)
        keys = []
    if len(keys) >= int(site_config.WIREGUARD_MAX_PEERS):
        return redirect('/account/wireguard')

    api = django_lcore.api
    if request.method == 'POST':
        action = request.POST.get('action')
        form = WgPeerForm(request.POST)
        if action == 'add_key':
            if form.is_valid():
                api.create_wg_peer(
                    request.user.username,
                    public_key=form.cleaned_data['public_key'],
                    name=form.cleaned_data['name'],
                )
            else:
                log_errors(request, form)
        return redirect('/account/wireguard')
    context = dict(
        menu_item='wireguard',
        locations=get_locations(),
    )
    return render(request, 'lambdainst/wireguard_new.html', context)

