#!/usr/bin/env python3
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import json

from ipaddress import ip_address
from ipaddress import ip_network

from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.util import cmd
from vyos.util import popen
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()

config_containers_registry = '/etc/containers/registries.conf'
config_containers_storage = '/etc/containers/storage.conf'

def _cmd(command):
    if os.path.exists('/tmp/vyos.container.debug'):
        print(command)
    return cmd(command)

# Container management functions
def container_exists(name):
    '''
    https://docs.podman.io/en/latest/_static/api.html#operation/ContainerExistsLibpod
    Check if container exists. Response codes.
    204 - container exists
    404 - no such container
    '''
    tmp = _cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/exists'")
    # If container exists it return status code "0" - code can not be displayed
    return (tmp == "")

def container_status(name):
    '''
    https://docs.podman.io/en/latest/_static/api.html#operation/ContainerInspectLibpod
    '''
    tmp = _cmd(f"curl --unix-socket /run/podman/podman.sock 'http://d/v3.0.0/libpod/containers/{name}/json'")
    data = json.loads(tmp)
    return data['State']['Status']

def ctnr_network_exists(name):
    # Check explicit name for network, returns True if network exists
    c = _cmd(f'podman network ls --quiet --filter name=^{name}$')
    return bool(c)

# Common functions
def get_config(config=None):
    if config:
        conf = config
    else:
        conf = Config()

    base = ['container']
    container = conf.get_config_dict(base, key_mangling=('-', '_'),
                                     get_first_key=True)
    # We have gathered the dict representation of the CLI, but there are default
    # options which we need to update into the dictionary retrived.
    default_values = defaults(base)
    container = dict_merge(default_values, container)

    # Delete container network, delete containers
    tmp = node_changed(conf, ['container', 'network'])
    if tmp: container.update({'net_remove' : tmp})

    tmp = node_changed(conf, ['container', 'name'])
    if tmp: container.update({'container_remove' : tmp})

    return container

def verify(container):
    # bail out early - looks like removal from running config
    if not container:
        return None

    # Add new container
    if 'name' in container:
        for name, container_config in container['name'].items():
            if 'network' in container_config:
                if len(container_config['network']) > 1:
                    raise ConfigError(f'Only one network can be specified for container "{name}"!')


                # Check if the specified container network exists
                network_name = list(container_config['network'])[0]
                if network_name not in container['network']:
                    raise ConfigError('Container network "{network_name}" does not exist!')

                if 'address' in container_config['network'][network_name]:
                    if 'network' not in container_config:
                        raise ConfigError(f'Can not use "address" without "network" for container "{name}"!')

                    address = container_config['network'][network_name]['address']
                    network = None
                    if is_ipv4(address):
                        network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0]
                    elif is_ipv6(address):
                        network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0]

                    # Specified container IP address must belong to network prefix
                    if ip_address(address) not in ip_network(network):
                        raise ConfigError(f'Used container address "{address}" not in network "{network}"!')

                    # We can not use the first IP address of a network prefix as this is used by podman
                    if ip_address(address) == ip_network(network)[1]:
                        raise ConfigError(f'Address "{address}" reserved for the container engine!')


            # Container image is a mandatory option
            if 'image' not in container_config:
                raise ConfigError(f'Container image for "{name}" is mandatory!')

            # If 'allow-host-networks' or 'network' not set.
            if 'allow_host_networks' not in container_config and 'network' not in container_config:
                raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!')

            # Can not set both allow-host-networks and network at the same time
            if {'allow_host_networks', 'network'} <= set(container_config):
                raise ConfigError(f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!')

    # Add new network
    if 'network' in container:
        v4_prefix = 0
        v6_prefix = 0
        for network, network_config in container['network'].items():
            # If ipv4-prefix not defined for user-defined network
            if 'prefix' not in network_config:
                raise ConfigError(f'prefix for network "{net}" must be defined!')

            for prefix in network_config['prefix']:
                if is_ipv4(prefix): v4_prefix += 1
                elif is_ipv6(prefix): v6_prefix += 1

            if v4_prefix > 1:
                raise ConfigError(f'Only one IPv4 prefix can be defined for network "{network}"!')
            if v6_prefix > 1:
                raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!')


    # A network attached to a container can not be deleted
    if {'net_remove', 'name'} <= set(container):
        for network in container['net_remove']:
            for container, container_config in container['name'].items():
                if 'network' in container_config and network in container_config['network']:
                    raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!')

    return None

def generate(container):
    # bail out early - looks like removal from running config
    if not container:
        return None

    render(config_containers_registry, 'containers/registry.tmpl', container)
    render(config_containers_storage, 'containers/storage.tmpl', container)

    return None

def apply(container):
    # Delete old containers if needed. We can't delete running container
    # Option "--force" allows to delete containers with any status
    if 'container_remove' in container:
        for name in container['container_remove']:
            if container_status(name) == 'running':
                _cmd(f'podman stop {name}')
            _cmd(f'podman rm --force {name}')

    # Delete old networks if needed
    if 'net_remove' in container:
        for network in container['net_remove']:
            _cmd(f'podman network rm {network}')

    # Add network
    if 'network' in container:
        for network, network_config in container['network'].items():
            # Check if the network has already been created
            if not ctnr_network_exists(network) and 'prefix' in network_config:
                tmp = f'podman network create {network}'
                # we can not use list comprehension here as the --ipv6 option
                # must immediately follow the specified subnet!!!
                for prefix in sorted(network_config['prefix']):
                    tmp += f' --subnet={prefix}'
                    if is_ipv6(prefix):
                      tmp += ' --ipv6'
                _cmd(tmp)

    # Add container
    if 'name' in container:
        for name, container_config in container['name'].items():
            # Check if the container has already been created
            if not container_exists(name):
                image = container_config['image']
                # Currently the best way to run a command and immediately print stdout
                print(os.system(f'podman pull {image}'))

                # Check/set environment options "-e foo=bar"
                env_opt = ''
                if 'environment' in container_config:
                    env_opt = '-e '
                    env_opt += " -e ".join(f"{k}={v['value']}" for k, v in container_config['environment'].items())

                # Publish ports
                port = ''
                if 'port' in container_config:
                    protocol = ''
                    for portmap in container_config['port']:
                        if 'protocol' in container_config['port'][portmap]:
                            protocol = container_config['port'][portmap]['protocol']
                            protocol = f'/{protocol}'
                        else:
                            protocol = '/tcp'
                        sport = container_config['port'][portmap]['source']
                        dport = container_config['port'][portmap]['destination']
                        port += f' -p {sport}:{dport}{protocol}'

                # Bind volume
                volume = ''
                if 'volume' in container_config:
                    for vol in container_config['volume']:
                        svol = container_config['volume'][vol]['source']
                        dvol = container_config['volume'][vol]['destination']
                        volume += f' -v {svol}:{dvol}'

                if 'allow_host_networks' in container_config:
                    _cmd(f'podman run -dit --name {name} --net host {port} {volume} {env_opt} {image}')
                else:
                    for network in container_config['network']:
                        ipparam = ''
                        if 'address' in container_config['network'][network]:
                            ipparam = '--ip ' + container_config['network'][network]['address']
                        _cmd(f'podman run --name {name} -dit --net {network} {ipparam} {port} {volume} {env_opt} {image}')

            # Else container is already created. Just start it.
            # It's needed after reboot.
            elif container_status(name) != 'running':
                _cmd(f'podman start {name}')

    return None

if __name__ == '__main__':
    try:
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        exit(1)