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

import bcrypt
import dns.exception
import dns.resolver
import fcntl
import os
import requests
import shutil
import socket
import subprocess

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID

def compile_url(domain, port, proto='https'):
    port = '' if (proto == 'https' and  port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port)
    return '{}://{}{}'.format(proto, domain, port)

def get_local_ip(version=None):
    # Return first routable IPv4/6 address of the VM (container host)
    # If version is not given, try getting IPv4, if that fails, try getting IPv6, if that fails too, fall back to 127.0.0.1
    if not version:
        return get_local_ip(4) or get_local_ip(6) or '127.0.0.1'
    try:
        output = subprocess.run(['/sbin/ip', 'route', 'get', '1' if version == 4 else '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()
        # Get field right after 'src'
        return output[output.index('src')+1]
    except:
        return None

def get_external_ip(version):
    # Return external IPv4/6 address via remote service
    family = socket.AF_INET if version == 4 else socket.AF_INET6
    allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family
    try:
        requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family
        return requests.get('https://tools.dasm.cz/myip.php', timeout=5).text
    except:
        return None
    finally:
        requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family

resolver = dns.resolver.Resolver()
resolver.timeout = 3
resolver.lifetime = 3
resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844']

def resolve_ip(domain, type):
    # Resolve domain name using Google Public DNS
    try:
        return resolver.query(domain, type)[0].address
    except dns.exception.Timeout:
        raise
    except:
        return None

def ping_url(url):
    try:
        return requests.get('https://tools.dasm.cz/vm-ping.php', params = {'url': url}, timeout=5).text == 'vm-pong'
    except requests.exceptions.Timeout:
        raise
    except:
        return False

def get_service_deps(app):
    # Get "needs" line from init script and split it to list
    try:
        with open(os.path.join('/etc/init.d', app), 'r') as f:
            for line in f.readlines():
                if line.strip().startswith('need'):
                    return line.split()[1:]
    except:
        pass
    return []

def is_service_started(app):
    # Check OpenRC service status without calling any binary
    return os.path.exists(os.path.join('/run/openrc/started', app))

def is_service_autostarted(app):
    # Check OpenRC service enablement
    return os.path.exists(os.path.join('/etc/runlevels/default', app))

def start_service(service):
    subprocess.run(['/sbin/service', service, 'start'], check=True)

def stop_service(service):
    subprocess.run(['/sbin/service', service, 'stop'], check=True)

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

def restart_nginx():
    restart_service(['/sbin/service', 'nginx', 'restart'])

def get_cert_info(cert):
    # Gather certificate data important for setup-host
    with open(cert, 'rb') as f:
        cert = x509.load_pem_x509_certificate(f.read(), default_backend())
    data = {'subject': cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
            'issuer': cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
            'expires': '{} UTC'.format(cert.not_valid_after),
            'method': 'manual'}
    if os.path.exists('/etc/periodic/daily/acme-sh'):
        data['method'] = 'letsencrypt'
    # This is really naive method of inferring if the cert is selfsigned and should never be used in production :)
    elif data['subject'] == data['issuer']:
        data['method'] = 'selfsigned'
    return data

def adminpwd_hash(password):
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

def adminpwd_verify(password, hash):
    return bcrypt.checkpw(password.encode(), hash.encode())

def shutdown_vm():
    subprocess.run(['/sbin/poweroff'])

def reboot_vm():
    subprocess.run(['/sbin/reboot'])

def update_hosts_lease(app, is_request):
    # This is a poor man's DHCP server which uses /etc/hosts as lease database
    # Leases the first unused IP from range 172.17.0.0/16
    # Uses file lock as interprocess mutex
    ip = None
    with open('/var/lock/vmmgr-hosts.lock', 'w') as lock:
        fcntl.lockf(lock, fcntl.LOCK_EX)
        # Load all existing records
        with open('/etc/hosts', 'r') as f:
            leases = [l.strip().split(' ', 1) for l in f]
        # If this call is a request for lease, find the first unassigned IP
        if is_request:
            used_ips = [l[0] for l in leases]
            for i in range(2, 65534):
                ip = '172.17.{}.{}'. format(i // 256, i % 256)
                if ip not in used_ips:
                    leases.append([ip, app])
                    break
        # Otherwise it is a release in which case we just delete the record
        else:
            leases = [l for l in leases if l[1] != app]
        # Write the contents back to the file
        with open('/etc/hosts', 'w') as f:
            for lease in leases:
                f.write('{} {}\n'.format(lease[0], lease[1]))
    return ip

def set_container_ip(pid, ip):
    # Set IP in container based on PID given via lxc.hook.start-host hook
    cmd = 'ip addr add {}/16 broadcast 172.17.255.255 dev eth0 && ip route add default via 172.17.0.1'.format(ip)
    subprocess.run(['nsenter', '-a', '-t', pid, '--', '/bin/sh', '-c', cmd])

def clean_ephemeral_layer(app):
    # Cleans containers ephemeral layer.
    # This is done early in the container start process, so the inode of the delta0 directory must remain unchanged
    layer = os.path.join('/var/lib/lxc', app, 'delta0')
    if os.path.exists(layer):
        for item in os.scandir(layer):
            shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
