#!/usr/bin/env python3

# Copyright 2016 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Update Stratis introspection data.
"""

# isort: STDLIB
import sys
import xml.etree.ElementTree as ET

# isort: THIRDPARTY
import dbus
from semantic_version import Version

# isort: FIRSTPARTY
from dbus_python_client_gen import make_class

# a minimal chunk of introspection data, enough for the methods needed.
SPECS = {
    "org.freedesktop.DBus.Introspectable": """
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="xml_data" type="s" direction="out"/>
</method>
</interface>
""",
    "org.storage.stratis2.Manager.r1": """
<interface name="org.storage.stratis2.Manager.r1">
<method name="CreatePool">
<arg name="name" type="s" direction="in"/>
<arg name="redundancy" type="(bq)" direction="in"/>
<arg name="devices" type="as" direction="in"/>
<arg name="key_desc" type="(bs)" direction="in"/>
<arg name="result" type="(b(oao))" direction="out"/>
<arg name="return_code" type="q" direction="out"/>
<arg name="return_string" type="s" direction="out"/>
</method>
<property name="Version" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const" />
</property>
</interface>
""",
    "org.storage.stratis2.pool.r1": """
<interface name="org.storage.stratis2.pool.r1">
<method name="CreateFilesystems">
<arg name="specs" type="as" direction="in"/>
<arg name="results" type="(ba(os))" direction="out"/>
<arg name="return_code" type="q" direction="out"/>
<arg name="return_string" type="s" direction="out"/>
</method>
</interface>
""",
}

_SERVICE = "org.storage.stratis2"

_INTROSPECTABLE_IFACE = "org.freedesktop.DBus.Introspectable"
_MANAGER_IFACE = "org.storage.stratis2.Manager.r1"
_POOL_IFACE = "org.storage.stratis2.pool.r1"
_TIMEOUT = 120000

# pylint: disable=invalid-name
Introspectable = make_class(
    "Introspectable", ET.fromstring(SPECS[_INTROSPECTABLE_IFACE]), _TIMEOUT
)
Manager = make_class("Manager", ET.fromstring(SPECS[_MANAGER_IFACE]), _TIMEOUT)
Pool = make_class("Pool", ET.fromstring(SPECS[_POOL_IFACE]), _TIMEOUT)

OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"

TOP_OBJECT_INTERFACE_PREFIXES = [
    "org.storage.stratis2.FetchProperties",
    "org.storage.stratis2.Manager",
    "org.storage.stratis2.Report",
]
POOL_OBJECT_INTERFACE_PREFIXES = ["org.storage.stratis2.pool"]
BLOCKDEV_OBJECT_INTERFACE_PREFIXES = ["org.storage.stratis2.blockdev"]
FILESYSTEM_OBJECT_INTERFACE_PREFIXES = ["org.storage.stratis2.filesystem"]


def _add_data(proxy, interfaces, table):
    """
    Introspect on the proxy, get the information for the specified interfaces,
    and add it to the table.

    :param proxy: dbus Proxy object
    :param list interfaces: list of interesting interface names
    :param dict table: table from interface names to introspection data as text
    :raises: RuntimeError if some interface not found
    """
    string_data = Introspectable.Methods.Introspect(proxy, {})
    xml_data = ET.fromstring(string_data)

    for interface_name in interfaces:
        try:
            interface = next(
                interface
                for interface in xml_data
                if interface.attrib["name"] == interface_name
            )
            table[interface_name] = ET.tostring(interface).decode("utf-8").rstrip(" \n")
        except StopIteration as err:
            raise RuntimeError(
                "interface %s not found in introspection data" % interface_name
            ) from err


def setup_minimal_object_set(bus):
    """
    Set up the minimal set of objects to be introspected on.

    :param bus: the bus
    :returns: a dict of proxy objects
    :rtype: dict of str * dbus proxy object
    """
    proxy = bus.get_object(_SERVICE, "/org/storage/stratis2", introspect=False)

    (
        (_, (pool_object_path, dev_object_paths)),
        return_code,
        return_msg,
    ) = Manager.Methods.CreatePool(
        proxy,
        {
            "name": "pool_name",
            "redundancy": (True, 0),
            "devices": ["/fake/fake"],
            "key_desc": (False, ""),
        },
    )

    if return_code != 0:
        sys.exit(return_msg)

    pool_proxy = bus.get_object(_SERVICE, pool_object_path, introspect=False)

    blockdev_proxy = bus.get_object(_SERVICE, dev_object_paths[0], introspect=False)

    ((_, (filesystems)), return_code, return_msg,) = Pool.Methods.CreateFilesystems(
        pool_proxy,
        {
            "specs": ["fs_name"],
        },
    )

    if return_code != 0:
        sys.exit(return_msg)

    filesystem_object_path = filesystems[0][0]
    filesystem_proxy = bus.get_object(
        _SERVICE, filesystem_object_path, introspect=False
    )

    return {
        "top_proxy": proxy,
        "pool_proxy": pool_proxy,
        "filesystem_proxy": filesystem_proxy,
        "blockdev_proxy": blockdev_proxy,
    }


def make_introspection_spec(proxies):
    """
    Make the introspection specification using proxy objects.

    :param proxies:a dictionary of distinguishing names to proxy objects
    :type proxies: dict of str * proxy object
    :returns: a table with interface name keys and introspection data values
    :rtype: dict of str * str
    """
    specs = {}

    revision = (
        "r%d" % Version(Manager.Properties.Version.Get(proxies["top_proxy"])).minor
    )

    def get_current_interfaces(interface_prefixes):
        return ["%s.%s" % (prefix, revision) for prefix in interface_prefixes]

    _add_data(
        proxies["top_proxy"],
        [OBJECT_MANAGER_INTERFACE]
        + get_current_interfaces(TOP_OBJECT_INTERFACE_PREFIXES),
        specs,
    )
    _add_data(
        proxies["pool_proxy"],
        get_current_interfaces(POOL_OBJECT_INTERFACE_PREFIXES),
        specs,
    )

    _add_data(
        proxies["blockdev_proxy"],
        get_current_interfaces(BLOCKDEV_OBJECT_INTERFACE_PREFIXES),
        specs,
    )
    _add_data(
        proxies["filesystem_proxy"],
        get_current_interfaces(FILESYSTEM_OBJECT_INTERFACE_PREFIXES),
        specs,
    )

    return specs


def print_spec(specs):
    """
    Print spec formatted according to stratis-cli and black's requirments.

    :param specs: the specification to print
    :type specs: dict of str * str
    """
    print("SPECS = {")

    print(
        ",\n".join(
            '    "%s": """\n%s\n"""' % (key, value)
            for (key, value) in sorted(specs.items())
        )
        + ","
    )
    print("}")


def main():
    """
    The main method.
    """
    bus = dbus.SystemBus()

    proxies = setup_minimal_object_set(bus)

    specs = make_introspection_spec(proxies)

    print_spec(specs)


if __name__ == "__main__":
    main()
