from datetime import datetime
from difflib import unified_diff
from logging import basicConfig, getLogger, INFO
import os
from pathlib import Path
import shutil
import subprocess
import sys
import yaml
from urllib.parse import urlparse

from notebook import notebookapp
from IPython.core.display import HTML


WORKDIR = 'edit'
META_YML = '.vcp-meta.yml'
MOODLE_DIR = '/opt/moodle'
CONF_RELATIVE = '/etc'
ENV_INHERIT = ['VAULT_ADDR', 'VAULT_TOKEN', 'PATH', 'REQUESTS_CA_BUNDLE']
logger = getLogger(__name__)
basicConfig(level=INFO, format='%(message)s')


def generate_local_path(host, conf_path, version=None):
    ret = Path(WORKDIR).absolute() / host
    if version is None:
        ret /= datetime.now().strftime("%Y%m%d%H%M%S%f")
    else:
        ret /= version
    ret /= Path(conf_path).name
    return ret


def generate_remote_path(container, conf_path, relative_to=CONF_RELATIVE):
    return (Path(MOODLE_DIR) / container / 'conf' /
            Path(conf_path).relative_to(relative_to))


def get_local_path(host, container, conf_path, version=None):
    if version is None:
        version = find_latest_version(host, container, conf_path)
    return generate_local_path(host, conf_path, version)


def _match_metainfo(parent, container, conf_path):
    p = parent / META_YML
    if not p.exists():
        return False
    with p.open() as f:
        params = yaml.safe_load(f)
    return (
        isinstance(params, dict) and
        'container' in params and
        'container_path' in params and
        params['container'] == container and
        params['container_path'] == conf_path)


def _match_metainfo_by_remote_path(parent, remote_path):
    p = parent / META_YML
    if not p.exists():
        return False
    with p.open() as f:
        params = yaml.safe_load(f)
    return (
        isinstance(params, dict) and
        'remote_path' in params and
        params['remote_path'] == remote_path)


def get_versions(host, *args, match=_match_metainfo):
    pdir = Path(WORKDIR).absolute() / host
    return sorted([
        x.name for x in pdir.glob('*')
        if x.is_dir() and match(x, *args)])


def find_latest_version(host, container, conf_path):
    return get_versions(host, container, conf_path)[-1]


def find_latest_version_by_remote_path(host, remote_path):
    return get_versions(
        host, remote_path, match=_match_metainfo_by_remote_path)[-1]


def download_file(host, remote_path, conf_path=None):
    if conf_path is None:
        conf_path = Path(remote_path).name
    dest = generate_local_path(host, conf_path)
    ansible_arg = f'src={remote_path} dest={dest} flat=yes'
    out = subprocess.check_output(
        ['ansible', host, '-m', 'fetch', '-a', ansible_arg])
    host_1 = out.decode('utf-8').split("\n")[0].split()[0]
    logger.info(f'Downloading {remote_path} from {host_1} to {dest}')
    return dest


def download_conf_file(host, container, conf_path, relative_to=CONF_RELATIVE):
    src = generate_remote_path(container, conf_path, relative_to)
    return download_file(host, src, conf_path)


def create_conf_file(host, conf_path):
    dest = generate_local_path(host, conf_path)
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.touch()
    return dest


def _to_backup(conf):
    return conf.parent / (conf.name + '.orig')


def make_backup(conf, quiet=False):
    org = _to_backup(conf)
    if not quiet:
        logger.info(f'Copy {conf} {org}')
    shutil.copy2(conf, org)


def make_metainfo(local_path, container, conf_path, relative_to=CONF_RELATIVE):
    params = {
        'container': container,
        'container_path': conf_path,
        'remote_path':
            str(generate_remote_path(container, conf_path, relative_to)),
        'version': list(local_path.parts)[-2],
    }
    with (local_path.parent / META_YML).open(mode='w') as f:
        yaml.safe_dump(params, stream=f, default_flow_style=False)


def make_simple_metainfo(local_path, remote_path):
    params = {
        'remote_path': remote_path,
        'version': list(local_path.parts)[-2],
    }
    with (local_path.parent / META_YML).open(mode='w') as f:
        yaml.safe_dump(params, stream=f, default_flow_style=False)


def generate_edit_link(conf):
    nb_conf = list(notebookapp.list_running_servers())[0]
    p = (Path(nb_conf['base_url']) / 'edit' /
         conf.absolute().relative_to(nb_conf['notebook_dir']))
    return HTML(f'<a href={p} target="_blank">{p.name}</a>')


def show_diff(path_a, path_b):
    lines_a = []
    lines_b = []
    with path_a.open() as f:
        lines_a = f.readlines()
    with path_b.open() as f:
        lines_b = f.readlines()
    diff = list(unified_diff(
        lines_a, lines_b, fromfile=path_a.name, tofile=path_b.name))
    sys.stdout.writelines(diff)
    return len(diff)


def upload_conf_file(src, host, container, conf_path,
                     relative_to=CONF_RELATIVE):
    dest = generate_remote_path(container, conf_path, relative_to)
    ansible_arg = f'mkdir -p {dest.parent}'
    subprocess.run(
        ['ansible', host, '-a', ansible_arg])
    ansible_arg = f'dest={dest} src={src} backup=yes'
    out = subprocess.check_output(
        ['ansible', host, '-m', 'copy', '-b', '-a', ansible_arg])
    host_1 = out.decode('utf-8').split("\n")[0].split()[0]
    logger.info(f'Uploading {dest} from {src} to {host_1}')


def restart_container(host, container):
    cmd = f'chdir={MOODLE_DIR} docker-compose restart {container}'
    logger.info(f'Restart container {container}')
    subprocess.check_call(['ansible', host, '-a', cmd])


def fetch_conf(host, container, conf_path,
               relative_to=CONF_RELATIVE, create=False):
    local_path = download_conf_file(host, container, conf_path, relative_to)
    make_backup(local_path)
    make_metainfo(local_path, container, conf_path, relative_to)
    return generate_edit_link(local_path)


def create_conf(host, container, conf_path,
                relative_to=CONF_RELATIVE, create=False):
    local_path = create_conf_file(host, conf_path)
    make_backup(local_path, quiet=True)
    make_metainfo(local_path, container, conf_path, relative_to)
    return generate_edit_link(local_path)


def apply_conf(host, container, conf_path,
               relative_to=CONF_RELATIVE, version=None, restart=True):
    diff = show_local_conf_diff(host, container, conf_path, version)
    local_path = get_local_path(host, container, conf_path, version)
    upload_conf_file(local_path, host, container, conf_path, relative_to)
    if restart:
        restart_container(host, container)


def revert_conf(host, container, conf_path,
                relative_to=CONF_RELATIVE, version=None):
    local_path = get_local_path(host, container, conf_path, version)
    backup_path = _to_backup(local_path)
    show_diff(local_path, backup_path)
    upload_conf_file(backup_path, host, container, conf_path, relative_to)
    restart_container(host, container)
    local_path.rename(local_path.parent / (local_path.name + '.revert'))


def show_local_conf(host, container, conf_path,
                    relative_to=CONF_RELATIVE, version=None):
    conf = get_local_path(host, container, conf_path, version)
    with conf.open() as f:
        print(f.read())


def edit_local_conf(host, container, conf_path,
                    relative_to=CONF_RELATIVE, version=None):
    conf = get_local_path(host, container, conf_path, version)
    return generate_edit_link(conf)


def show_local_conf_diff(host, container, conf_path, version=None):
    local_path = get_local_path(host, container, conf_path, version)
    show_diff(_to_backup(local_path), local_path)


def save_shibboleth_part(conf_path):
    with conf_path.open() as f:
        data = yaml.safe_load(f)
    params = {}
    if 'shibboleth' in data['services']:
        params['shibboleth_container'] = yaml.safe_dump(
            data['services']['shibboleth'])
    vars_path = conf_path.parent / 'extra_vars.yml'
    with vars_path.open(mode='w') as f:
        yaml.safe_dump(params, f)
    return vars_path


def init_shibboleth_part(conf_dir, hostname, volumes):
    shibboleth_volumes = ['/sys/fs/cgroup:/sys/fs/cgroup']
    shibboleth_volumes.extend(volumes)
    params = {
        'shibboleth_container': yaml.safe_dump({
            'image': 'harbor.vcloud.nii.ac.jp/vcp/moodle:shibboleth-3.0.4',
            'privileged': True,
            'ports': ['443:443'],
            'volumes': shibboleth_volumes,
            'container_name': 'shibboleth',
            'hostname': hostname,
        }),
    }
    vars_path = conf_dir / 'shibboleth.yml'
    with vars_path.open(mode='w') as f:
        yaml.safe_dump(params, f)
    return vars_path


def setup_shibboleth_part(local_path, **params):
    if params is None or len(params) == 0:
        return save_shibboleth_part(local_path)
    else:
        return init_shibboleth_part(local_path.parent, **params)


def generate_docker_compose(host, conf_path, extra_vars, extra_vars_file):
    template = 'template/docker/compose/docker-compose.yml'
    ansible_arg = f'src={template} dest={conf_path.parent}/'
    env = dict([(x, os.environ[x]) for x in ENV_INHERIT])
    args = ['ansible', host, '-m', 'template', '-c', 'local',
            '-a', ansible_arg]
    for k, v in extra_vars.items():
        args.extend(['-e', f'{k}={v}'])
    for x in extra_vars_file:
        args.extend(['-e', f'@{str(x)}'])
    subprocess.run(args=args, env=env, check=True)


def update_docker_compose(host, extra_vars={}, shibboleth_params={}):
    remote_path = MOODLE_DIR + '/docker-compose.yml'
    local_path = download_file(host, remote_path)
    make_backup(local_path)
    make_simple_metainfo(local_path, remote_path)
    shibboleth_vars = setup_shibboleth_part(local_path, **shibboleth_params)
    generate_docker_compose(host, local_path, extra_vars, [shibboleth_vars])
    show_diff(_to_backup(local_path), local_path)
    return generate_edit_link(local_path)


def append_shibboleth_container(host, moodle_url, volumes=[], extra_vars={}):
    hostname = urlparse(moodle_url).netloc
    return update_docker_compose(
        host, extra_vars,
        shibboleth_params={'hostname': hostname, 'volumes': volumes},
    )


def upload_docker_compose(host, version=None, apply=False):
    remote_path = MOODLE_DIR + '/docker-compose.yml'
    if version is None:
        version = find_latest_version_by_remote_path(host, remote_path)
    local_path = (
        Path(WORKDIR).absolute() / host / version / 'docker-compose.yml')

    ansible_arg = f'dest={remote_path} src={local_path} backup=yes'
    out = subprocess.check_output(
        ['ansible', host, '-m', 'copy', '-b', '-a', ansible_arg])
    host_1 = out.decode('utf-8').split("\n")[0].split()[0]
    logger.info(f'Uploading {remote_path} from {local_path} to {host_1}')
    if not apply:
        return
    ansible_arg = f'chdir=/opt/moodle docker-compose up -d --remove-orphans'
    args = ['ansible', host, '-a', ansible_arg]
    logger.info('Apply the changes in docker-compose.yml.')
    subprocess.run(args=args, check=True)


def generate_proxy_conf(host, conf_path, extra_vars):
    template = 'template/docker/compose/moodle-proxy.conf.template'
    ansible_arg = f'src={template} dest={conf_path.parent}/moodle-proxy.conf'

    env = dict([(x, os.environ[x]) for x in ENV_INHERIT])
    args = [
        'ansible', host, '-m', 'template', '-c', 'local', '-a', ansible_arg]
    for k, v in extra_vars.items():
        args.extend(['-e', f'{k}={v}'])
    subprocess.run(args=args, env=env, check=True)


def update_proxy_conf(host, extra_vars={}):
    conf_path = Path('/usr/local/apache2/conf/moodle-proxy.conf')
    container = 'proxy'
    link = fetch_conf(host, container, str(conf_path), str(conf_path.parent))
    version = find_latest_version(host, container, str(conf_path))
    local_path = generate_local_path(host, conf_path, version)
    generate_proxy_conf(host, local_path, extra_vars)
    show_local_conf_diff(host, container, conf_path, version)
    return link


def apply_proxy_conf(host, version=None, restart=True):
    conf_path = Path('/usr/local/apache2/conf/moodle-proxy.conf')
    apply_conf(host, 'proxy', str(conf_path), str(conf_path.parent),
               version, restart)

[((21, 9, 21, 28), 'logging.getLogger', 'getLogger', ({(21, 19, 21, 27): '__name__'}, {}), '(__name__)', False, 'from logging import basicConfig, getLogger, INFO\n'), ((22, 0, 22, 45), 'logging.basicConfig', 'basicConfig', (), '', False, 'from logging import basicConfig, getLogger, INFO\n'), ((93, 10, 94, 60), 'subprocess.check_output', 'subprocess.check_output', ({(94, 8, 94, 59): "['ansible', host, '-m', 'fetch', '-a', ansible_arg]"}, {}), "(['ansible', host, '-m', 'fetch', '-a', ansible_arg])", False, 'import subprocess\n'), ((120, 4, 120, 27), 'shutil.copy2', 'shutil.copy2', ({(120, 17, 120, 21): 'conf', (120, 23, 120, 26): 'org'}, {}), '(conf, org)', False, 'import shutil\n'), ((148, 11, 148, 60), 'IPython.core.display.HTML', 'HTML', ({(148, 16, 148, 59): 'f"""<a href={p} target="_blank">{p.name}</a>"""'}, {}), '(f\'<a href={p} target="_blank">{p.name}</a>\')', False, 'from IPython.core.display import HTML\n'), ((160, 4, 160, 31), 'sys.stdout.writelines', 'sys.stdout.writelines', ({(160, 26, 160, 30): 'diff'}, {}), '(diff)', False, 'import sys\n'), ((168, 4, 169, 45), 'subprocess.run', 'subprocess.run', ({(169, 8, 169, 44): "['ansible', host, '-a', ansible_arg]"}, {}), "(['ansible', host, '-a', ansible_arg])", False, 'import subprocess\n'), ((171, 10, 172, 65), 'subprocess.check_output', 'subprocess.check_output', ({(172, 8, 172, 64): "['ansible', host, '-m', 'copy', '-b', '-a', ansible_arg]"}, {}), "(['ansible', host, '-m', 'copy', '-b', '-a',\n    ansible_arg])", False, 'import subprocess\n'), ((180, 4, 180, 55), 'subprocess.check_call', 'subprocess.check_call', ({(180, 26, 180, 54): "['ansible', host, '-a', cmd]"}, {}), "(['ansible', host, '-a', cmd])", False, 'import subprocess\n'), ((285, 4, 285, 50), 'subprocess.run', 'subprocess.run', (), '', False, 'import subprocess\n'), ((315, 10, 316, 65), 'subprocess.check_output', 'subprocess.check_output', ({(316, 8, 316, 64): "['ansible', host, '-m', 'copy', '-b', '-a', ansible_arg]"}, {}), "(['ansible', host, '-m', 'copy', '-b', '-a',\n    ansible_arg])", False, 'import subprocess\n'), ((324, 4, 324, 41), 'subprocess.run', 'subprocess.run', (), '', False, 'import subprocess\n'), ((336, 4, 336, 50), 'subprocess.run', 'subprocess.run', (), '', False, 'import subprocess\n'), ((340, 16, 340, 65), 'pathlib.Path', 'Path', ({(340, 21, 340, 64): '"""/usr/local/apache2/conf/moodle-proxy.conf"""'}, {}), "('/usr/local/apache2/conf/moodle-proxy.conf')", False, 'from pathlib import Path\n'), ((351, 16, 351, 65), 'pathlib.Path', 'Path', ({(351, 21, 351, 64): '"""/usr/local/apache2/conf/moodle-proxy.conf"""'}, {}), "('/usr/local/apache2/conf/moodle-proxy.conf')", False, 'from pathlib import Path\n'), ((31, 11, 31, 26), 'pathlib.Path', 'Path', ({(31, 16, 31, 25): 'conf_path'}, {}), '(conf_path)', False, 'from pathlib import Path\n'), ((51, 17, 51, 34), 'yaml.safe_load', 'yaml.safe_load', ({(51, 32, 51, 33): 'f'}, {}), '(f)', False, 'import yaml\n'), ((65, 17, 65, 34), 'yaml.safe_load', 'yaml.safe_load', ({(65, 32, 65, 33): 'f'}, {}), '(f)', False, 'import yaml\n'), ((132, 8, 132, 66), 'yaml.safe_dump', 'yaml.safe_dump', (), '', False, 'import yaml\n'), ((141, 8, 141, 66), 'yaml.safe_dump', 'yaml.safe_dump', (), '', False, 'import yaml\n'), ((158, 16, 159, 67), 'difflib.unified_diff', 'unified_diff', (), '', False, 'from difflib import unified_diff\n'), ((238, 15, 238, 32), 'yaml.safe_load', 'yaml.safe_load', ({(238, 30, 238, 31): 'f'}, {}), '(f)', False, 'import yaml\n'), ((241, 41, 242, 43), 'yaml.safe_dump', 'yaml.safe_dump', ({(242, 12, 242, 42): "data['services']['shibboleth']"}, {}), "(data['services']['shibboleth'])", False, 'import yaml\n'), ((245, 8, 245, 33), 'yaml.safe_dump', 'yaml.safe_dump', ({(245, 23, 245, 29): 'params', (245, 31, 245, 32): 'f'}, {}), '(params, f)', False, 'import yaml\n'), ((253, 32, 260, 10), 'yaml.safe_dump', 'yaml.safe_dump', ({(253, 47, 260, 9): "{'image': 'harbor.vcloud.nii.ac.jp/vcp/moodle:shibboleth-3.0.4',\n    'privileged': True, 'ports': ['443:443'], 'volumes': shibboleth_volumes,\n    'container_name': 'shibboleth', 'hostname': hostname}"}, {}), "({'image':\n    'harbor.vcloud.nii.ac.jp/vcp/moodle:shibboleth-3.0.4', 'privileged': \n    True, 'ports': ['443:443'], 'volumes': shibboleth_volumes,\n    'container_name': 'shibboleth', 'hostname': hostname})", False, 'import yaml\n'), ((264, 8, 264, 33), 'yaml.safe_dump', 'yaml.safe_dump', ({(264, 23, 264, 29): 'params', (264, 31, 264, 32): 'f'}, {}), '(params, f)', False, 'import yaml\n'), ((300, 15, 300, 35), 'urllib.parse.urlparse', 'urlparse', ({(300, 24, 300, 34): 'moodle_url'}, {}), '(moodle_url)', False, 'from urllib.parse import urlparse\n'), ((90, 20, 90, 37), 'pathlib.Path', 'Path', ({(90, 25, 90, 36): 'remote_path'}, {}), '(remote_path)', False, 'from pathlib import Path\n'), ((145, 19, 145, 53), 'notebook.notebookapp.list_running_servers', 'notebookapp.list_running_servers', ({}, {}), '()', False, 'from notebook import notebookapp\n'), ((146, 9, 146, 34), 'pathlib.Path', 'Path', ({(146, 14, 146, 33): "nb_conf['base_url']"}, {}), "(nb_conf['base_url'])", False, 'from pathlib import Path\n'), ((26, 10, 26, 23), 'pathlib.Path', 'Path', ({(26, 15, 26, 22): 'WORKDIR'}, {}), '(WORKDIR)', False, 'from pathlib import Path\n'), ((28, 15, 28, 29), 'datetime.datetime.now', 'datetime.now', ({}, {}), '()', False, 'from datetime import datetime\n'), ((36, 12, 36, 28), 'pathlib.Path', 'Path', ({(36, 17, 36, 27): 'MOODLE_DIR'}, {}), '(MOODLE_DIR)', False, 'from pathlib import Path\n'), ((37, 12, 37, 27), 'pathlib.Path', 'Path', ({(37, 17, 37, 26): 'conf_path'}, {}), '(conf_path)', False, 'from pathlib import Path\n'), ((73, 11, 73, 24), 'pathlib.Path', 'Path', ({(73, 16, 73, 23): 'WORKDIR'}, {}), '(WORKDIR)', False, 'from pathlib import Path\n'), ((312, 8, 312, 21), 'pathlib.Path', 'Path', ({(312, 13, 312, 20): 'WORKDIR'}, {}), '(WORKDIR)', False, 'from pathlib import Path\n')]