from django.shortcuts import render, redirect, reverse
from django.conf import settings
from django.contrib import messages
import wharf.tasks as tasks
from celery.result import AsyncResult
from celery.states import state, PENDING, SUCCESS, FAILURE, STARTED
from django.core.cache import cache
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponseBadRequest, HttpResponse, HttpResponseServerError

import requests
import time
from . import forms
from . import models
import re
from datetime import datetime
import json
import hmac
import hashlib
import timeout_decorator

from redis import StrictRedis

redis = StrictRedis.from_url(settings.CELERY_BROKER_URL)
ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')

def run_cmd(cmd):
    res = tasks.run_ssh_command.delay(cmd)
    return res.get().strip()

def cmd_key(cmd):
    return "cmd:%s" % cmd

def run_cmd_with_cache(cmd):
    key = cmd_key(cmd)
    existing = cache.get(key)
    if existing:
        return existing
    res = run_cmd(cmd)
    cache.set(key, res, None)
    return res

def clear_cache(cmd):
    key = cmd_key(cmd)
    cache.delete(key)

def run_cmd_with_log(app_name, description, cmd, after):
    res = tasks.run_ssh_command.delay(cmd)
    if app_name == None: # global
        app_name = '_'
    else:
        models.TaskLog(
            task_id=res.id,
            when=datetime.now(),
            app=models.App.objects.get(name=app_name),
            description=description
        ).save()
    return redirect(reverse('wait_for_command', kwargs={'app_name': app_name, 'task_id': res.id, 'after': after}))

def get_log(res):
    key = tasks.task_key(res.id)
    if res.state > state(PENDING):
        raw = redis.get(key)
        if raw == None:
            return ""
        return raw.decode('utf-8')
    else:
        return ""

def wait_for_command(request, app_name, task_id, after):
    res = AsyncResult(task_id)
    if app_name != '_':
        app = models.App.objects.get(name=app_name)
        task, created = models.TaskLog.objects.get_or_create(task_id=task_id, defaults={'app': app, 'when': datetime.now()})
        description = task.description
    else:
        description = ""
    if res.state == state(SUCCESS):
        return redirect(reverse(after, kwargs={'app_name': app_name, 'task_id': task_id}))
    log = ansi_escape.sub("", get_log(res))
    if res.state == state(FAILURE):
        log += str(res.traceback)
    return render(request, 'command_wait.html', {
        'app': app_name,
        'task_id': task_id,
        'log': log,
        'state': res.state,
        'running': res.state in [state(PENDING), state(STARTED)],
        'description': description
        })

def show_log(request, task_id):
    res = AsyncResult(task_id)
    task = models.TaskLog.objects.get(task_id=task_id)
    log = ansi_escape.sub("", get_log(res))
    if res.state == state(FAILURE):
        log += str(res.traceback)
    return render(request, 'command_wait.html', {
        'app': task.app.name,
        'task_id': task_id,
        'log': log,
        'state': res.state,
        'running': False,
        'description': task.description})

def app_list():
    data = run_cmd_with_cache("apps:list")
    lines = data.split("\n")
    if lines[0] != "=====> My Apps":
        raise Exception(data)
    return lines[1:]

def index(request):
    try:
        apps = app_list()
    except Exception as e:
        if e.__class__.__name__ in ["AuthenticationException"]: # Can't use class directly as Celery mangles things
            return render(request, 'setup_key.html', {'key': tasks.get_public_key.delay().get()})
        else:
            raise
    if request.method == 'POST':
        app_form = forms.CreateAppForm(request.POST)
        if app_form.is_valid():
            return create_app(app_form.cleaned_data['name'])
    else:
        app_form = forms.CreateAppForm()
    config_form = forms.ConfigForm()
    config = global_config()
    return render(request, 'list_apps.html', {'apps': apps, 'app_form': app_form, 'config_form': config_form, 'config': sorted(config.items())})

def refresh_all(request):
    cache.clear()
    return redirect(reverse('index'))

def generic_config(app, data):
    lines = data.split("\n")
    if lines[0] != "=====> %s env vars" % app:
        raise Exception(data)
    config = {}
    for line in lines[1:]:
        (name, value) = line.split(":", 1)
        config[name] = value.lstrip()
    return config

def app_config(app_name):
    data = run_cmd_with_cache("config %s" % app_name)
    return generic_config(app_name, data)

def global_config():
    data = run_cmd_with_cache("config --global")
    return generic_config("global", data)

def app_config_set(app, key, value):
    return run_cmd_with_log(app, "Setting %s" % key, "config:set %s %s=%s" % (app, key, value), "check_app_config_set")

def check_config_set(request, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    lines = data.split("\n")
    if lines[0] != '-----> Setting config vars':
        raise Exception(data)
    messages.success(request, 'Config updated')

def check_app_config_set(request, app_name, task_id):
    check_config_set(request, task_id)
    clear_cache("config %s" % app_name)
    return redirect(reverse('app_info', args=[app_name]))

def global_config_set(request):
    form = forms.ConfigForm(request.POST)
    if form.is_valid():
        return run_cmd_with_log(None, "Setting %s" % form.cleaned_data['key'], "config:set --global %s=%s" % (form.cleaned_data['key'], form.cleaned_data['value']), "check_global_config_set")
    else:
        raise Exception

def check_global_config_set(request, task_id):
    check_config_set(request, task_id)
    clear_cache("config --global")
    return redirect(reverse('index'))

def generic_list(app_name, data, name_field, fields):
    lines = data.split("\n")
    if lines[0].find("is not a dokku command") != -1:
        raise Exception("Need plugin!")
    if lines[0].find("There are no") != -1:
        return None
    fields = dict([[x,{}] for x in fields])
    last_field = None
    for f in fields.keys():
        index = lines[0].find(f)
        if index == -1:
            raise Exception("Can't find '%s' in '%s'" % (f, lines[0].strip()))
        if f == name_field:
            index = 0
        fields[f]["start"] = index
        if last_field != None:
            fields[last_field]["end"] = index
        last_field = f
    fields[last_field]["end"] = None
    results = []
    for line in lines[1:]:
        info = {}
        for f in fields.keys():
            if fields[f]["end"] == None:
                info[f] = line[fields[f]["start"]:].strip()
            else:
                info[f] = line[fields[f]["start"]:fields[f]["end"]].strip()
        results.append(info)
    results = dict([[x[name_field], x] for x in results])
    if app_name in results:
        return results[app_name]
    else:
        return None

def db_list(app_name, data):
    return generic_list(app_name, data, "NAME", ["NAME", "VERSION", "STATUS", "EXPOSED PORTS", "LINKS"])

def postgres_list(app_name):
    data = run_cmd_with_cache("postgres:list")
    try:
        return db_list(app_name, data)
    except:
        clear_cache("postgres:list")
        raise

def redis_list(app_name):
    data = run_cmd_with_cache("redis:list")
    try:
        return db_list(app_name, data)
    except:
        clear_cache("redis:list")
        raise

def letsencrypt(app_name):
    data = run_cmd_with_cache("letsencrypt:ls")
    return generic_list(app_name, data, "App name", ["App name", "Certificate Expiry", "Time before expiry", "Time before renewal"])

def process_info(app_name):
    data = run_cmd_with_cache("ps:report %s" % app_name)
    lines = data.split("\n")
    if lines[0].find("%s process information" % app_name) == -1 and lines[0].find("%s ps information" % app_name) == -1: # Different versions
        raise Exception(data)
    results = {}
    processes = {}
    process_re = re.compile("Status\s+([^\.]+\.\d+):?\s+(\S+)")
    for line in lines[1:]:
        if line.strip().startswith("Status "):
            matches = process_re.search(line)
            if matches == None:
                raise Exception(line)
            matches = matches.groups()
            processes[matches[0]] = matches[1]
        else:
            (name, rest) = line.split(":", 1)
            results[name.strip()] = rest.strip()
    results["processes"] = processes
    return results

def domains_list(app_name):
    data = run_cmd_with_cache("domains:report %s" % app_name)
    vhosts = re.search("Domains app vhosts: (.*)", data)
    return [x.strip() for x in vhosts.groups()[0].split(" ") if x != ""]

def add_domain(request, app_name):
    form = forms.CreateDomainForm(request.POST)
    if form.is_valid():
        commands = ["domains:add %s %s" % (app_name, form.cleaned_data['name'])]
        if letsencrypt(app_name) != None:
            commands.append("letsencrypt %s" % app_name)
        return run_cmd_with_log(app_name, "Add domain %s" % form.cleaned_data['name'], commands, "check_domain")
    else:
        raise Exception

def check_domain(request, app_name, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    if data.find("Reloading nginx") != -1:
        clear_cache("domains:report %s" % app_name)
        messages.success(request, "Added domain name to %s" % app_name)
        return redirect(reverse('app_info', args=[app_name]))
    else:
        raise Exception(data)

def remove_domain(request, app_name):
    name = request.POST['name']
    commands = ["domains:remove %s %s" % (app_name, name)]
    if letsencrypt(app_name) != None:
        commands.append("letsencrypt %s" % app_name)
    return run_cmd_with_log(app_name, "Remove domain %s" % name, commands, "check_domain")

def app_info(request, app_name):
    app, _ = models.App.objects.get_or_create(name=app_name)
    config = app_config(app_name)
    if "GITHUB_URL" in config:
        app.github_url = config["GITHUB_URL"]
        app.save()
    if request.method == 'POST':
        form = forms.ConfigForm(request.POST)
        if form.is_valid():
            return app_config_set(app_name, form.cleaned_data['key'], form.cleaned_data['value'])
    else:
        form = forms.ConfigForm()
    return render(request, 'app_info.html', {
        'postgres': postgres_list(app_name),
        'redis': redis_list(app_name),
        'letsencrypt': letsencrypt(app_name),
        'process': process_info(app_name),
        'logs': ansi_escape.sub("", run_cmd("logs %s --num 100" % app_name)),
        'domains': domains_list(app_name),
        'domain_form': forms.CreateDomainForm(),
        'form': form,
        'app': app_name,
        'git_url': config.get('GITHUB_URL', None),
        'config': sorted(config.items()),
        'task_logs': models.TaskLog.objects.filter(app=app).order_by('-when').all(),
    })

def deploy(request, app_name):
    if request.POST['action'] == "deploy":
        res = tasks.deploy.delay(app_name, request.POST['url'])
        clear_cache("config %s" % app_name)
        clear_cache("domains:report %s" % app_name)
        clear_cache("ps:report %s" % app_name)
        return redirect(reverse('wait_for_command', kwargs={'app_name': app_name, 'task_id': res.id, 'after': "check_deploy"}))
    elif request.POST['action'] == "rebuild":
        return run_cmd_with_log(app_name, "Rebuilding", "ps:rebuild %s" % app_name, "check_rebuild")
    else:
        raise Exception(request.POST['action'])

def create_postgres(request, app_name):
    return run_cmd_with_log(app_name, "Add Postgres", ["postgres:create %s" % app_name, "postgres:link %s %s" % (app_name, app_name)], "check_postgres")

def create_redis(request, app_name):
    return run_cmd_with_log(app_name, "Add Redis", ["redis:create %s" % app_name, "redis:link %s %s" % (app_name, app_name)], "check_redis")

def check_deploy(request, app_name, task_id):
    clear_cache("config %s" % app_name)
    messages.success(request, "%s redeployed" % app_name)
    return redirect(reverse('app_info', args=[app_name]))

def check_rebuild(request, app_name, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    if data.find("Application deployed:") == -1:
        raise Exception(data)
    messages.success(request, "%s rebuilt" % app_name)
    clear_cache("config %s" % app_name)
    return redirect(reverse('app_info', args=[app_name]))

def check_postgres(request, app_name, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    if data.find("Postgres container created") == -1:
        raise Exception(data)
    messages.success(request, "Postgres added to %s" % app_name)
    clear_cache("postgres:list")
    clear_cache("config %s" % app_name)
    return redirect(reverse('app_info', args=[app_name]))

def check_redis(request, app_name, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    if data.find("Redis container created") == -1:
        raise Exception(data)
    messages.success(request, "Redis added to %s" % app_name)
    clear_cache("redis:list")
    clear_cache("config %s" % app_name)
    return redirect(reverse('app_info', args=[app_name]))

def create_app(app_name):
    models.App(name=app_name).save()
    return run_cmd_with_log(app_name, "Add app %s" % app_name, "apps:create %s" % app_name, "check_app")

def check_app(request, app_name, task_id):
    res = AsyncResult(task_id)
    data = get_log(res)
    if data.find("Creating %s... done" % app_name) == -1:
        raise Exception(data)
    messages.success(request, "Created %s" % app_name)
    clear_cache("apps:list")
    return redirect(reverse('app_info', args=[app_name]))

def setup_letsencrypt(request, app_name):
    return run_cmd_with_log(app_name, "Enable Let's Encrypt", "letsencrypt %s" % app_name, "check_letsencrypt")

def check_letsencrypt(request, app_name, task_id):
    res = AsyncResult(task_id)
    log = get_log(res)
    if log.find("Certificate retrieved successfully") !=-1:
        clear_cache("letsencrypt:ls")
        return redirect(reverse('app_info', args=[app_name]))
    else:
        return render(request, 'command_wait.html', {'app': app_name, 'task_id': task_id, 'log': log, 'state': res.state, 'running': res.state in [state(PENDING), state(STARTED)]})

@csrf_exempt
def github_webhook(request):
    secret = settings.GITHUB_SECRET.encode('utf-8')
    hash = "sha1=%s" % hmac.new(secret, request.body, hashlib.sha1).hexdigest()
    if "HTTP_X_HUB_SIGNATURE" not in request.META:
        return HttpResponseBadRequest("No X-Hub-Signature header")
    header = request.META["HTTP_X_HUB_SIGNATURE"]
    if header != hash:
        return HttpResponseBadRequest("%s doesn't equal %s" % (hash, header))
    data = json.loads(request.read())
    if "hook_id" in data: # assume Ping
        if "push" not in data["hook"]["events"]:
            return HttpResponseBadRequest("No Push event set!")
        return HttpResponse("All good")
    default_ref = "refs/heads/%s" % data["repository"]["default_branch"]
    if data["ref"] != default_ref:
        return HttpResponse("Push to non-default branch (saw %s, expected %s)" % (data["ref"], default_ref))
    clone_url = data["repository"]["clone_url"]
    apps = models.App.objects.filter(github_url=clone_url)
    if not apps.exists():
        return HttpResponseBadRequest("Can't find an entry for clone URL %s" % clone_url)
    app = apps.first()
    res = tasks.deploy.delay(app.name, clone_url)
    clear_cache("config %s" % app.name)
    return HttpResponse("Running deploy. Deploy log is at %s" % request.build_absolute_uri(reverse('show_log', kwargs={'task_id': res.id})))

@timeout_decorator.timeout(5, use_signals=False)
def check_status():
    # Clearing the cache and then trying a command makes sure that
    # - The cache is up
    # - Celery is up
    # - We can run dokku commands
    clear_cache("config --global")
    run_cmd_with_cache("config --global")

def status(request):
    try:
        check_status()
        return HttpResponse("All good")
    except timeout_decorator.TimeoutError:
        return HttpResponseServerError("Timeout trying to get status")