# -*- 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

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': False,
    })
    register_proxy(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_esc=domain.replace('.', '\\.')))
    for app in config.get_apps().keys():
        register_proxy(app)

def reload_nginx():
    subprocess.run(['/usr/sbin/nginx', '-s', 'reload'])

def restart_nginx():
    subprocess.run(['/sbin/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_password(oldpassword, newpassword):
    # Update LUKS password and adminpwd for WSGI application
    pwinput = f'{oldpassword}\n{newpassword}'.encode()
    partition_uuid = open('/etc/crypttab').read().split()[1][5:]
    partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
    subprocess.run(['/sbin/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']
    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(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--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(paths.ACME_DIR, domain)):
        cmd = ['/usr/bin/acme.sh', '--issue', '-d', domain]
        for app,definition in config.get_apps():
            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(['/usr/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(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--install-cert', '-d', domain, '--key-file', crypto.CERT_KEY_FILE, '--fullchain-file', crypto.CERT_PUB_FILE, '--reloadcmd', '/sbin/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(['/sbin/poweroff'])

def reboot_vm():
    subprocess.run(['/sbin/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 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()
    for layer in set(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_containers = [definition for name,definition in repo_local.get_containers().items() if name not in removed_containers]
    remove_unused_layers(retained_containers, 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()
    for layer in set(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_containers = [definition for name,definition in repo_local.get_containers().items() if name not in removed_containers] + new_containers
    remove_unused_layers(retained_containers, queue)

def remove_unused_layers(retained_containers, queue):
    # Enqueue removal of images which won't be used in any locally defined containers anymore
    used_images = set()
    for definition in retained_containers:
        used_images.update(definition['layers'])
    # Build dependency tree to safely remove the images in order of dependency
    depsolver = DepSolver()
    for image in set(repo_local.get_images()) - used_images:
        image = Image(image)
        depsolver.add(image.name, set(image.layers) - used_images, 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}
