# -*- coding: utf-8 -*-

import json
import os
import shutil
import subprocess

from . import confupdater
from . import tools
from . import validator

VERSION = '0.0.1'

CONF_FILE = '/srv/vm/config.json'
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
ACME_CRON = '/etc/periodic/daily/acme-sh'
CERT_PUB_FILE = '/etc/ssl/services.pem'
CERT_KEY_FILE = '/etc/ssl/services.key'
CERT_SAN_FILE = '/etc/ssl/san.cnf'

NGINX_TEMPLATE = '''server {{
    listen [::]:{port} ssl http2;
    server_name {host}.{domain};

    access_log /var/log/nginx/{app}.access.log;
    error_log /var/log/nginx/{app}.error.log;

    location / {{
        proxy_pass http://{ip}:8080;
    }}

    error_page 502 /502.html;
    location = /502.html {{
        root /srv/vm/templates;
    }}

    location = /vm-ping {{
        add_header Content-Type text/plain;
        return 200 "vm-pong";
    }}
}}
'''

NGINX_DEFAULT_TEMPLATE = '''server {{
    listen [::]:80 default_server ipv6only=off;

    location / {{
        return 301 https://$host:{port}$request_uri;
    }}

    location /.well-known/acme-challenge/ {{
        root /etc/acme.sh.d;
    }}

    location = /vm-ping {{
        add_header Content-Type text/plain;
        return 200 "vm-pong";
    }}
}}

server {{
    listen [::]:{port} ssl http2 default_server ipv6only=off;

    location / {{
        proxy_pass http://127.0.0.1:8080;
    }}

    location /static {{
        root /srv/vm;
    }}

    error_page 502 /502.html;
    location = /502.html {{
        root /srv/vm/templates;
    }}

    location = /vm-ping {{
        add_header Content-Type text/plain;
        return 200 "vm-pong";
    }}
}}
'''

ISSUE_TEMPLATE = '''
\x1b[1;32m   _____             _   _         __      ____  __ 
  / ____|           | | | |        \\\\ \\\\    / /  \\\\/  |
 | (___  _ __   ___ | |_| |_ ___ _ _\\\\ \\\\  / /| \\\\  / |
  \\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| |
  ____) | |_) | (_) | |_| ||  __/ |   \\\\  /  | |  | |
 |_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_|    \\\\/   |_|  |_|
        | |                                         
        |_|\x1b[0m

 \x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán.
 Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení.
 Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít
 za následek občanské nebo trestní sankce.

 \x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited.
 You must have explicit, authorized permission to access or configure this
 device. Unauthorized attempts and actions to access or use this system may
 result in civil or criminal penalties.


 Pro přístup k aplikacím otevřete URL \x1b[1m{url}\x1b[0m ve Vašem
 internetovém prohlížeči.

\x1b[0;30m
'''

ACME_CRON_TEMPLATE = '''#!/bin/sh

[ -x /usr/bin/acme.sh ] && /usr/bin/acme.sh --cron >/dev/null
'''

CERT_SAN = '''[ req ]
distinguished_name = dn
x509_extensions = ext
[ dn ]
[ ext ]
subjectAltName=DNS:{domain},DNS:*.{domain}"
'''

class VMMgr:
    def __init__(self):
        # Load JSON configuration
        with open(CONF_FILE, 'r') as f:
            self.conf = json.load(f)
        self.domain = self.conf['host']['domain']
        self.port = self.conf['host']['port']

    def save_conf(self):
        # Save a sorted JSON configuration object with indentation
        with open(CONF_FILE, 'w') as f:
            json.dump(self.conf, f, sort_keys=True, indent=4)

    def update_login(self, app, login, password):
        # Update login and password for an app in the configuration
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        if login is not None:
            self.conf['apps'][app]['login'] = login
        if password is not None:
            self.conf['apps'][app]['password'] = password
        self.save_conf()

    def show_tiles(self, app):
        # Update visibility for the app in the configuration
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        self.conf['apps'][app]['visible'] = True
        self.save_conf()

    def hide_tiles(self, app):
        # Update visibility for the app in the configuration
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        self.conf['apps'][app]['visible'] = False
        self.save_conf()

    def start_app(self, app):
        # Start the actual app service
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        tools.start_service(app)

    def stop_app(self, app):
        # Stop the actual app service
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        tools.stop_service(app)
        # Stop the app service's dependencies if they are not used by another running app
        deps = self.build_deps_tree()
        for dep in self.get_app_deps(app):
            if not any([tools.is_service_started(d) for d in deps[dep]]):
                tools.stop_service(dep)

    def build_deps_tree(self):
        # Fisrt, build a dictionary of {app: [needs]}
        needs = {}
        for app in self.conf['apps']:
            needs[app] = self.get_app_deps(app)
        # Then reverse it to {need: [apps]}
        deps = {}
        for app, need in needs.items():
            for n in need:
                deps.setdefault(n, []).append(app)
        return deps

    def get_app_deps(self, app):
        # Get "needs" line from init script and split it to list, skipping first two elements (docker, net)
        try:
            with open(os.path.join('/etc/init.d', app), 'r') as f:
                return [l.split()[2:] for l in f.readlines() if l.startswith('\tneed')][0]
        except:
            return []

    def enable_autostart(self, app):
        # Add the app to OpenRC default runlevel
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        subprocess.run(['/sbin/rc-update', 'add', app])

    def disable_autostart(self, app):
        # Remove the app from OpenRC default runlevel
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        subprocess.run(['/sbin/rc-update', 'del', app])

    def register_proxy(self, app):
        # Rebuild nginx configuration using IP of referenced app container and reload nginx
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        self.update_proxy_conf(app, tools.get_container_ip(app))
        tools.reload_nginx()

    def update_proxy_conf(self, app, ip):
        with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
            f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], ip=ip, domain=self.domain, port=self.port))

    def unregister_proxy(self, app):
        # Remove nginx configuration to prevent proxy mismatch when the container IP is reassigned to another container
        if not validator.is_valid_app(app, self.conf):
            raise validator.InvalidValueException('app', app)
        self.update_proxy_conf(app, tools.NULL_IP)
        tools.reload_nginx()

    def update_host(self, domain, port, restart_nginx=True):
        # Update domain and port and rebuild all configurtion. Defer nginx restart when updating from web interface
        if not validator.is_valid_domain(domain):
            raise validator.InvalidValueException('domain', domain)
        if not validator.is_valid_port(port):
            raise validator.InvalidValueException('port', port)
        self.domain = self.conf['host']['domain'] = domain
        self.port = self.conf['host']['port'] = port
        self.save_conf()
        self.rebuild_nginx(restart_nginx)
        self.update_apps_urls()

    def rebuild_nginx(self, restart_nginx):
        # Rebuild nginx config for the portal app
        with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
            f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
        # Unregister nginx proxy for apps (will be repopulated on app restart)
        for app in self.conf['apps']:
            self.update_proxy_conf(app, tools.NULL_IP)
        # Restart nginx to properly bind the new listen port
        if restart_nginx:
            tools.restart_nginx()

    def rebuild_issue(self):
        # Compile the HTTPS host displayed in terminal banner
        domain = self.domain
        # If the dummy host is used, take an IP address of a primary interface instead
        if domain == 'spotter.vm':
            domain = tools.get_local_ipv4()
            if not domain:
                domain = tools.get_local_ipv6()
            if not domain:
                domain = '127.0.0.1'
        # Rebuild the terminal banner
        with open(ISSUE_FILE, 'w') as f:
            f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(domain, self.port)))

    def update_apps_urls(self):
        # Update configuration for respective applications
        confupdater.update_url(tools.compile_url(self.domain, self.port, None))
        # Restart currently running apps in order to update config and re-register nginx proxy
        for app in self.conf['apps']:
            if tools.is_service_started(app):
                tools.restart_service(app)

    def update_common(self, email, gmaps_api_key):
        # Update common configuration values
        if email != None:
            # Update email
            if not validator.is_valid_email(email):
                raise validator.InvalidValueException('email', email)
            self.conf['common']['email'] = email
            confupdater.update_email(email)
        if gmaps_api_key != None:
            # Update Google Maps API key
            self.conf['common']['gmaps-api-key'] = gmaps_api_key
            confupdater.update_gmaps_api_key(gmaps_api_key)
        # Save config to file
        self.save_conf()
        # Restart currently running apps in order to update config
        for app in self.conf['apps']:
            if tools.is_service_started(app):
                tools.restart_service(app)

    def update_password(self, oldpassword, newpassword):
        # Update LUKS password and adminpwd for WSGI application
        tools.update_luks_password(oldpassword, newpassword)
        self.conf['host']['adminpwd'] = tools.adminpwd_hash(newpassword)
        # Save config to file
        self.save_conf()

    def create_selfsigned(self):
        # Create selfsigned certificate with wildcard alternative subject name
        with open(os.path.join(CERT_SAN_FILE), 'w') as f:
            f.write(CERT_SAN.format(domain=self.domain))
        subprocess.run(['openssl', 'req', '-config', CERT_SAN_FILE, '-x509', '-new', '-out', CERT_PUB_FILE, '-keyout', CERT_KEY_FILE, '-nodes', '-days', '7305', '-subj', '/CN={}'.format(self.domain)], check=True)
        os.chmod(CERT_KEY_FILE, 0o640)

    def request_cert(self):
        # Remove all possible conflicting certificates requested in the past
        certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')]
        for cert in certs:
            if cert != self.domain:
                subprocess.run(['/usr/bin/acme.sh', '--remove', '-d', cert])
        # Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before
        if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)):
            cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
            for app in self.conf['apps']:
                cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
            cmd += ['-w', '/etc/acme.sh.d']
            # Request the certificate
            subprocess.run(cmd, check=True)
        # Otherwise just try to renew
        else:
            # Acme.sh returns code 2 on skipped renew
            try: 
                subprocess.run(['/usr/bin/acme.sh', '--renew', '-d', self.domain], check=True)
            except subprocess.CalledProcessError as e:
                if e.returncode != 2:
                    raise
        # Install the issued certificate
        subprocess.run(['/usr/bin/acme.sh', '--install-cert', '-d', self.domain, '--key-file', CERT_KEY_FILE, '--fullchain-file', CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
        # Install acme.sh cronjob
        with open(ACME_CRON, 'w') as f:
            f.write(ACME_CRON_TEMPLATE)

    def install_cert(self, public_file, private_file):
        # Remove acme.sh cronjob
        if os.path.exists(ACME_CRON):
            os.unlink(ACME_CRON)
        # Copy certificate files
        shutil.copyfile(public_file, CERT_PUB_FILE)
        shutil.copyfile(private_file, CERT_KEY_FILE)
        os.chmod(CERT_KEY_FILE, 0o640)
        # Reload nginx
        tools.reload_nginx()
