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

import configparser
import importlib
import os
import shutil
import subprocess
import urllib
from spoc import config as spoc_config, net as spoc_net, repo_local, repo_online
from spoc.app import App
from spoc.container import Container, ContainerState
from spoc.depsolver import DepSolver
from spoc.image import Image

from . import config, crypto, net, paths, templates

BIN_ACME_SH =       '/usr/bin/acme.sh'
BIN_BLKID =         '/sbin/blkid'
BIN_CRYPTSETUP =    '/sbin/cryptsetup'
BIN_NGINX =         '/usr/sbin/nginx'
BIN_POWEROFF =      '/sbin/poweroff'
BIN_REBOOT =        '/sbin/reboot'
BIN_SERVICE =       '/sbin/service'
CRYPTTAB_FILE =     '/etc/crypttab'

def register_app(app, host, login, password):
    # Register newly installed application, its subdomain and credentials (called at the end of package install.sh)
    config.register_app(app, {
        'host': host,
        'login': login if login else 'N/A',
        'password': password if password else 'N/A',
        'visible': True,
    })
    register_proxy(app)
    update_app_config(app)

def unregister_app(app):
    unregister_proxy(app)
    config.unregister_app(app)

def register_proxy(app):
    # Setup proxy configuration and reload nginx
    app_host = config.get_apps()[app]['host']
    host = config.get_host()
    # Assume that the main container has always the same name as the app
    app_ip = spoc_net.get_ip(app)
    with open(os.path.join(paths.NGINX_DIR, f'{app}.conf'), 'w') as f:
        f.write(templates.NGINX.format(app=app, host=app_host, ip=app_ip, domain=host['domain'], port=host['port']))
    reload_nginx()

def unregister_proxy(app):
    # Remove proxy configuration and reload nginx
    try:
        os.unlink(os.path.join(paths.NGINX_DIR, f'{app}.conf'))
        reload_nginx()
    except FileNotFoundError:
        pass

def update_host(domain, port):
    config.set_host('domain', domain)
    config.set_host('port', port)
    # Rebuild nginx config for the portal and existing apps. Web interface calls restart_nginx() in WSGI close handler
    with open(os.path.join(paths.NGINX_DIR, 'default.conf'), 'w') as f:
        f.write(templates.NGINX_DEFAULT.format(port=port, domain=domain))
    for app in config.get_apps().keys():
        register_proxy(app)
    update_app_config()

def reload_nginx():
    subprocess.run([BIN_NGINX, '-s', 'reload'])

def restart_nginx():
    subprocess.run([BIN_SERVICE, 'nginx', 'restart'])

def rebuild_issue():
    # Compile the URLs displayed in terminal banner and rebuild the issue and motd files
    host = config.get_host()
    url_host = net.compile_url(host['domain'], host['port'])
    url_ip = net.compile_url(net.get_local_ip(), host['port'])
    issue = templates.ISSUE.format(url_host=url_host, url_ip=url_ip)
    with open(paths.ISSUE_FILE, 'w') as f:
        f.write(issue)
    with open(paths.MOTD_FILE, 'w') as f:
        f.write(issue)

def update_common_settings(email, gmaps_api_key):
    # Update common configuration values
    config.set_common('email', email)
    config.set_common('gmaps-api-key', gmaps_api_key)
    update_app_config()

def update_password(oldpassword, newpassword):
    # Update LUKS password and adminpwd for WSGI application
    pwinput = f'{oldpassword}\n{newpassword}'.encode()
    partition_uuid = open(CRYPTTAB_FILE).read().split()[1][5:]
    partition_name = subprocess.run([BIN_BLKID, '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
    subprocess.run([BIN_CRYPTSETUP, 'luksChangeKey', partition_name], input=pwinput, check=True)
    # Update bcrypt-hashed password in config
    hash = crypto.adminpwd_hash(newpassword)
    config.set_host('adminpwd', hash)

def create_selfsigned_cert():
    # Disable acme.sh cronjob
    os.chmod(paths.ACME_CRON, 0o640)
    # Create selfsigned certificate with wildcard alternative subject name
    domain = config.get_host()['domain']
    crypto.create_selfsigned_cert(domain)
    # Reload nginx
    reload_nginx()

def request_acme_cert():
    # Remove all possible conflicting certificates requested in the past
    domain = config.get_host()['domain']
    try:
        certs = [i for i in os.listdir(paths.ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')]
        for cert in certs:
            if cert != domain:
                subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--remove', '-d', cert])
    except FileNotFoundError:
        pass
    # 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(paths.ACME_DIR, domain)):
        cmd = [BIN_ACME_SH, '--home', paths.ACME_DIR, '--issue', '-d', domain]
        for app,definition in config.get_apps().items():
            cmd += ['-d', f'{definition["host"]}.{domain}']
        cmd += ['-w', paths.ACME_DIR]
        # Request the certificate
        subprocess.run(cmd, check=True)
    # Otherwise just try to renew
    else:
        try:
            subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--renew', '-d', domain], check=True)
        except subprocess.CalledProcessError as e:
            # return code 2 means skipped renew, which is OK
            if e.returncode != 2:
                raise
    # Install the issued certificate
    subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--install-cert', '-d', domain, '--key-file', paths.CERT_KEY_FILE, '--fullchain-file', paths.CERT_PUB_FILE, '--reloadcmd', f'{BIN_SERVICE} nginx reload'], check=True)
    # Enable acme.sh cronjob
    os.chmod(paths.ACME_CRON, 0o750)

def install_manual_cert(public_file, private_file):
    # Disable acme.sh cronjob
    os.chmod(paths.ACME_CRON, 0o640)
    # Copy certificate files
    shutil.copyfile(public_file, paths.CERT_PUB_FILE)
    shutil.copyfile(private_file, paths.CERT_KEY_FILE)
    os.chmod(paths.CERT_KEY_FILE, 0o600)
    # Reload nginx
    reload_nginx()

def shutdown_vm():
    subprocess.run([BIN_POWEROFF])

def reboot_vm():
    subprocess.run([BIN_REBOOT])

def start_app(app_name, queue):
    # Enqueue application start
    queue.start_app(App(app_name))

def stop_app(app_name, queue):
    # Enqueue application stop
    queue.stop_app(App(app_name))

def update_app_visibility(app_name, visible):
    # Update visibility for the app in the configuration
    config.set_app(app_name, 'visible', visible)

def update_app_autostart(app_name, enabled):
    # Add/remove the app to OpenRC default runlevel
    App(app_name, False).set_autostart(enabled)

def update_app_config(app_name=None):
    # Set environment variables and run scripts to update relevant application configuration files
    common = config.get_common()
    host = config.get_host()
    apps = config.get_apps()
    env = os.environ.copy()
    env['LAYERS_DIR'] = spoc_config.LAYERS_DIR
    env['VOLUMES_DIR'] = spoc_config.VOLUMES_DIR
    env['APPS_DIR'] = spoc_config.APPS_DIR
    env['LOG_DIR'] = spoc_config.LOG_DIR
    env['PORT'] = host['port']
    env['EMAIL'] = common['email']
    env['GMAPS_API_KEY'] = common['gmaps-api-key']
    app_names = [app_name] if app_name else repo_local.get_apps().keys()
    for app_name in app_names:
        install_dir = os.path.join(spoc_config.APPS_DIR, app_name, 'install')
        script_path = os.path.join(install_dir, 'update-conf.sh')
        env['HOST'] = f'{apps[app_name]["host"]}.{host["domain"]}'
        try:
            subprocess.run(script_path, cwd=install_dir, env=env)
        except FileNotFoundError:
            pass

def is_app_started(app_name):
    # Assume that the main container has always the same name as the app
    return Container(app_name).get_state() == ContainerState.RUNNING

def is_app_autostarted(app_name):
    # Check OpenRC service enablement
    return App(app_name, False).autostart

def install_app(app_name, queue):
    # Enqueue layers and application download, registration and install script execution
    required_images = []
    new_containers = repo_online.get_app(app_name)['containers'].values()
    for container in new_containers:
        required_images.extend(repo_online.get_image(container['image'])['layers'])
    local_images = repo_local.get_images()
    # Create (deduplicate) set of layers while retaining the correct order
    required_images = list(dict.fromkeys(required_images))
    # Download layers in correct order
    for layer in required_images:
        if layer not in local_images:
            queue.download_image(Image(layer, False))
    queue.install_app(App(app_name, False, False))

def uninstall_app(app_name, queue):
    # Enqueue application uninstall script execution and removal
    app = App(app_name, False)
    queue.uninstall_app(app)
    # Remove unused layers
    removed_containers = [container.name for container in app.containers]
    retained_layers = set()
    for name,definition in repo_local.get_containers().items():
        if name not in removed_containers:
            retained_layers.update(definition['layers'])
    remove_unused_layers(retained_layers, queue)

def update_app(app_name, queue):
    # Enqueue layers and application download, registration and update script execution
    required_images = []
    new_containers = repo_online.get_app(app_name)['containers'].values()
    for container in new_containers:
        required_images.extend(repo_online.get_image(container['image'])['layers'])
    local_images = repo_local.get_images()
    # Create (deduplicate) set of layers while retaining the correct order
    required_images = list(dict.fromkeys(required_images))
    # Download layers in correct order
    for layer in required_images:
        if layer not in local_images:
            queue.download_image(Image(layer, False))
    app = App(app_name, False)
    queue.update_app(app)
    # Remove unused layers
    removed_containers = [container.name for container in app.containers]
    retained_layers = set(required_images)
    for name,definition in repo_local.get_containers().items():
        if name not in removed_containers:
            retained_layers.update(definition['layers'])
    remove_unused_layers(retained_layers, queue)

def remove_unused_layers(retained_layers, queue):
    # Enqueue removal of images which won't be used in any locally defined containers anymore
    depsolver = DepSolver()
    # Build dependency tree to safely remove the images in order of dependency
    for image in set(repo_local.get_images()) - retained_layers:
        image = Image(image)
        depsolver.add(image.name, set(image.layers) - retained_layers, image)
    # Enqueue the removal actions
    for image in reversed(depsolver.solve()):
        queue.delete_image(image)

def update_repo_conf(url, username, password):
    # Include credentials in the repo URL and save to SPOC config
    parts = urllib.parse.urlsplit(url)
    netloc = f'{username}:{password}@{parts.netloc}' if username or password else parts.netloc
    url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
    repo_config = configparser.ConfigParser()
    repo_config.read(spoc_config.CONFIG_FILE)
    repo_config['repo']['url'] = url
    with open(spoc_config.CONFIG_FILE, 'w') as f:
        repo_config.write(f)
    # Reimport the config module, reloading the defined URL values
    importlib.reload(spoc_config)
    repo_online.data = None

def get_repo_conf():
    # Parse the SPOC config repo URL and return as tuple
    parts = urllib.parse.urlsplit(spoc_config.ONLINE_BASE_URL)
    netloc = parts.netloc.split('@', 1)[1] if parts.username or parts.password else parts.netloc
    url = urllib.parse.urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
    username = parts.username if parts.username else ''
    return {'url':url, 'username':username}
