"""
Ceph teuthology task for managed smb features.
"""
from io import StringIO
import contextlib
import copy
import json
import logging
import shlex
import time

from teuthology.exceptions import ConfigError, CommandFailedError


log = logging.getLogger(__name__)


def _disable_systemd_resolved(ctx, remote):
    r = remote.run(args=['ss', '-lunH'], stdout=StringIO())
    # this heuristic tries to detect if systemd-resolved is running
    if '%lo:53' not in r.stdout.getvalue():
        return
    log.info('Disabling systemd-resolved on %s', remote.shortname)
    # Samba AD DC container DNS support conflicts with resolved stub
    # resolver when using host networking. And we want host networking
    # because it is the simplest thing to set up.  We therefore will turn
    # off the stub resolver.
    r = remote.run(
        args=['sudo', 'cat', '/etc/systemd/resolved.conf'],
        stdout=StringIO(),
    )
    resolved_conf = r.stdout.getvalue()
    setattr(ctx, 'orig_resolved_conf', resolved_conf)
    new_resolved_conf = (
        resolved_conf + '\n# EDITED BY TEUTHOLOGY: deploy_samba_ad_dc\n'
    )
    if '[Resolve]' not in new_resolved_conf.splitlines():
        new_resolved_conf += '[Resolve]\n'
    new_resolved_conf += 'DNSStubListener=no\n'
    remote.write_file(
        path='/etc/systemd/resolved.conf',
        data=new_resolved_conf,
        sudo=True,
    )
    remote.run(args=['sudo', 'systemctl', 'restart', 'systemd-resolved'])
    r = remote.run(args=['ss', '-lunH'], stdout=StringIO())
    assert '%lo:53' not in r.stdout.getvalue()
    # because docker is a big fat persistent deamon, we need to bounce it
    # after resolved is restarted
    remote.run(args=['sudo', 'systemctl', 'restart', 'docker'])


def _reset_systemd_resolved(ctx, remote):
    orig_resolved_conf = getattr(ctx, 'orig_resolved_conf', None)
    if not orig_resolved_conf:
        return  # no orig_resolved_conf means nothing to reset
    log.info('Resetting systemd-resolved state on %s', remote.shortname)
    remote.write_file(
        path='/etc/systemd/resolved.conf',
        data=orig_resolved_conf,
        sudo=True,
    )
    remote.run(args=['sudo', 'systemctl', 'restart', 'systemd-resolved'])
    setattr(ctx, 'orig_resolved_conf', None)


def _samba_ad_dc_conf(ctx, remote, cengine):
    # this config has not been tested outside of smithi nodes. it's possible
    # that this will break when used elsewhere because we have to list
    # interfaces explicitly. Later I may add a feature to sambacc to exclude
    # known-unwanted interfaces that having to specify known good interfaces.
    cf = {
        "samba-container-config": "v0",
        "configs": {
            "demo": {
                "instance_features": ["addc"],
                "domain_settings": "sink",
                "instance_name": "dc1",
            }
        },
        "domain_settings": {
            "sink": {
                "realm": "DOMAIN1.SINK.TEST",
                "short_domain": "DOMAIN1",
                "admin_password": "Passw0rd",
                "interfaces": {
                    "exclude_pattern": "^docker[0-9]+$",
                },
            }
        },
        "domain_groups": {
            "sink": [
                {"name": "supervisors"},
                {"name": "employees"},
                {"name": "characters"},
                {"name": "bulk"},
            ]
        },
        "domain_users": {
            "sink": [
                {
                    "name": "bwayne",
                    "password": "1115Rose.",
                    "given_name": "Bruce",
                    "surname": "Wayne",
                    "member_of": ["supervisors", "characters", "employees"],
                },
                {
                    "name": "ckent",
                    "password": "1115Rose.",
                    "given_name": "Clark",
                    "surname": "Kent",
                    "member_of": ["characters", "employees"],
                },
                {
                    "name": "user0",
                    "password": "1115Rose.",
                    "given_name": "George0",
                    "surname": "Hue-Sir",
                    "member_of": ["bulk"],
                },
                {
                    "name": "user1",
                    "password": "1115Rose.",
                    "given_name": "George1",
                    "surname": "Hue-Sir",
                    "member_of": ["bulk"],
                },
                {
                    "name": "user2",
                    "password": "1115Rose.",
                    "given_name": "George2",
                    "surname": "Hue-Sir",
                    "member_of": ["bulk"],
                },
                {
                    "name": "user3",
                    "password": "1115Rose.",
                    "given_name": "George3",
                    "surname": "Hue-Sir",
                    "member_of": ["bulk"],
                },
            ]
        },
    }
    cf_json = json.dumps(cf)
    remote.run(args=['sudo', 'mkdir', '-p', '/var/tmp/samba'])
    remote.write_file(
        path='/var/tmp/samba/container.json', data=cf_json, sudo=True
    )
    return [
        '--volume=/var/tmp/samba:/etc/samba-container:ro',
        '-eSAMBACC_CONFIG=/etc/samba-container/container.json',
    ]


@contextlib.contextmanager
def configure_samba_client_container(ctx, config):
    # TODO: deduplicate logic between this task and deploy_samba_ad_dc
    role = config.get('role')
    samba_client_image = config.get(
        'samba_client_image', 'quay.io/samba.org/samba-client:latest'
    )
    if not role:
        raise ConfigError(
            "you must specify a role to discover container engine / pull image"
        )
    (remote,) = ctx.cluster.only(role).remotes.keys()
    cengine = 'podman'
    try:
        log.info("Testing if podman is available")
        remote.run(args=['sudo', cengine, '--help'])
    except CommandFailedError:
        log.info("Failed to find podman. Using docker")
        cengine = 'docker'

    remote.run(args=['sudo', cengine, 'pull', samba_client_image])
    samba_client_container_cmd = [
        'sudo',
        cengine,
        'run',
        '--rm',
        '--net=host',
        '-eKRB5_CONFIG=/dev/null',
        samba_client_image,
    ]

    setattr(ctx, 'samba_client_container_cmd', samba_client_container_cmd)
    try:
        yield
    finally:
        setattr(ctx, 'samba_client_container_cmd', None)


@contextlib.contextmanager
def deploy_samba_ad_dc(ctx, config):
    role = config.get('role')
    ad_dc_image = config.get(
        'ad_dc_image', 'quay.io/samba.org/samba-ad-server:latest'
    )
    samba_client_image = config.get(
        'samba_client_image', 'quay.io/samba.org/samba-client:latest'
    )
    test_user_pass = config.get('test_user_pass', 'DOMAIN1\\ckent%1115Rose.')
    if not role:
        raise ConfigError(
            "you must specify a role to allocate a host for the AD DC"
        )
    (remote,) = ctx.cluster.only(role).remotes.keys()
    ip = remote.ssh.get_transport().getpeername()[0]
    cengine = 'podman'
    try:
        log.info("Testing if podman is available")
        remote.run(args=['sudo', cengine, '--help'])
    except CommandFailedError:
        log.info("Failed to find podman. Using docker")
        cengine = 'docker'
    remote.run(args=['sudo', cengine, 'pull', ad_dc_image])
    remote.run(args=['sudo', cengine, 'pull', samba_client_image])
    _disable_systemd_resolved(ctx, remote)
    remote.run(
        args=[
            'sudo',
            'mkdir',
            '-p',
            '/var/lib/samba/container/logs',
            '/var/lib/samba/container/data',
        ]
    )
    remote.run(
        args=[
            'sudo',
            cengine,
            'run',
            '-d',
            '--name=samba-ad',
            '--network=host',
            '--privileged',
        ]
        + _samba_ad_dc_conf(ctx, remote, cengine)
        + [ad_dc_image]
    )

    # test that the ad dc is running and basically works
    connected = False
    samba_client_container_cmd = [
        'sudo',
        cengine,
        'run',
        '--rm',
        '--net=host',
        f'--dns={ip}',
        '-eKRB5_CONFIG=/dev/null',
        samba_client_image,
    ]
    for idx in range(10):
        time.sleep((2 ** (1 + idx)) / 8)
        log.info("Probing SMB status of DC %s, idx=%s", ip, idx)
        cmd = samba_client_container_cmd + [
            'smbclient',
            '-U',
            test_user_pass,
            '//domain1.sink.test/sysvol',
            '-c',
            'ls',
        ]
        try:
            remote.run(args=cmd)
            connected = True
            log.info("SMB status probe succeeded")
            break
        except CommandFailedError:
            pass
    if not connected:
        raise RuntimeError('failed to connect to AD DC SMB share')

    setattr(ctx, 'samba_ad_dc_ip', ip)
    setattr(ctx, 'samba_client_container_cmd', samba_client_container_cmd)
    try:
        yield
    finally:
        try:
            remote.run(args=['sudo', cengine, 'stop', 'samba-ad'])
        except CommandFailedError:
            log.error("Failed to stop samba-ad container")
        try:
            remote.run(args=['sudo', cengine, 'rm', 'samba-ad'])
        except CommandFailedError:
            log.error("Failed to remove samba-ad container")
        remote.run(
            args=[
                'sudo',
                'rm',
                '-rf',
                '/var/lib/samba/container/logs',
                '/var/lib/samba/container/data',
            ]
        )
        _reset_systemd_resolved(ctx, remote)
        setattr(ctx, 'samba_ad_dc_ip', None)
        setattr(ctx, 'samba_client_container_cmd', None)


def _marks(marks_value):
    if not marks_value:
        return ''
    if isinstance(marks_value, str):
        return marks_value
    if isinstance(marks_value, list):
        return ' or '.join(marks_value)
    raise ValueError(f'unexpected type: {marks_value!r}')


def _workunit_commands(
    key, values, *, default_script='smb/smb_tests.sh', default_target='tests'
):
    commands = []
    if isinstance(values, str):
        values = values.split()
    for value in values:
        script = default_script
        target = default_target
        custom_args = []
        if isinstance(value, str):
            # direct marks expression
            marks = _marks(value)
        elif isinstance(value, list):
            # just a list of marks to include
            marks = _marks(value)
        elif isinstance(value, dict):
            # full control
            opts = value
            script = opts.get('script', script)
            target = opts.get('target', target)
            marks = _marks(opts.get('marks', []))
            custom_args = [str(v) for v in (opts.get('custom_args') or [])]

        cmd = [script]
        if marks:
            cmd.append('-m')
            cmd.append(marks)
        cmd += custom_args
        cmd.append(target)
        commands.append(shlex.join(cmd))
    return commands


def workunit(ctx, config):
    """Workunit wrapper with special behaviors for smb."""
    from . import workunit

    _config = copy.deepcopy(config)
    clients = _config.get('clients') or {}
    env = _config.get('env') or {}

    clients = {k: _workunit_commands(k, v) for k, v in clients.items()}
    mfile = _config.get('metadata_file_path', _DEFAULT_META_FILE)
    env['SMB'] = 'yes'
    env['SMB_TEST_META'] = mfile

    _config['clients'] = clients
    _config['env'] = env
    # annoyingly the stock workunit helper script command uses a tool (from the
    # ceph/teuthology repo) called adjust-ulimits *and* a tool (from packages)
    # called ceph-coverage. They're glued together under the
    # no_coverage_and_limits option that defaults to false for the stock
    # workunit task. Since, for SMB on Ceph, we are using containers and NOT
    # using packages the latter tool is not available even if we invoke other
    # teuthology tasks that installs adjust-ulimits. Just skip the whole thing
    # for now and we can set ulimits via pytest if we really want to set
    # ulimits. Allow the yaml to override our default, however unlikely.
    _config['no_coverage_and_limits'] = config.get(
        'no_coverage_and_limits', True
    )
    log.info('Passing workunit config: %r', _config)
    with write_metadata_file(ctx, _config):
        return workunit.task(ctx, _config)


@contextlib.contextmanager
def write_metadata_file(ctx, config, *, roles=None):
    obj = {
        'samba_client_container_cmd': getattr(
            ctx, 'samba_client_container_cmd', ''
        ),
        'samba_ad_dc_ip': getattr(ctx, 'samba_ad_dc_ip', ''),
        'smb_users': config.get('smb_users') or [],
        'smb_shares': config.get('smb_shares') or [],
    }
    if config.get('admin_node'):
        role = config.get('admin_node')
        (remote,) = ctx.cluster.only(role).remotes.keys()
        n = obj['admin_node'] = remote.inventory_info
        n['shortname'] = remote.shortname
        n['ip_address'] = remote.ip_address
    if config.get('smb_nodes'):
        snodes = obj['smb_nodes'] = []
        for node in config.get('smb_nodes'):
            (remote,) = ctx.cluster.only(node).remotes.keys()
            n = dict(remote.inventory_info)
            n['shortname'] = remote.shortname
            n['ip_address'] = remote.ip_address
            snodes.append(n)
    data = json.dumps(obj)
    log.debug('smb metadata: %r', obj)

    mfile = config.get('metadata_file_path', _DEFAULT_META_FILE)
    if not roles:
        roles = list(config.get('clients') or [])
    remotes = []
    for role in roles:
        (remote,) = ctx.cluster.only(role).remotes.keys()
        remotes.append(remote)
        remote.write_file(
            path=mfile,
            data=data,
            sudo=True,
            mode='0644',
        )
    yield
    for remote in remotes:
        remote.run(
            args=[
                'sudo',
                'rm',
                '-rf',
                '--',
                mfile,
            ],
        )


_DEFAULT_META_FILE = '/var/tmp/ceph-smb-test-meta.json'
