from difflib import ndiff

from tango.utils import CaselessDict
from dsconfig.tangodb import get_devices_from_dict
from dsconfig.utils import green, red, yellow

from .appending_dict import SetterDict


def get_device_data(device, mapping, data):
    if device in mapping:
        server, instance, clss = mapping[device]
        return CaselessDict(data["servers"][server][instance][clss]).get(device, {})
    return {}


def property_diff(old, new, indentation=""):
    diff = ndiff(old, new)
    lines = []
    for line in diff:
        if line.startswith("+"):
            lines.append(green(f"{indentation}{line}"))
        elif line.startswith("-"):
            lines.append(red(f"{indentation}{line}"))
        else:
            lines.append(f"{indentation}{line}")
    return "\n".join(lines)


def format_property(value, indentation="", max_lines=10):
    if len(value) > max_lines:
        ending = f"\n{indentation}... [{len(value) - max_lines} lines]"
    else:
        ending = ""
    return "\n".join(f"{indentation}{line}" for line in value[:max_lines]) + ending


def get_changes(data, calls):
    """
    Combine a list of database calls into "changes" that can
    be more easily turned into a readable representation
    """

    devices = get_devices_from_dict(data["servers"])
    device_mapping = CaselessDict(
        (device, (server, instance, clss))
        for server, instance, clss, device, _ in devices
    )
    classes = data.get("classes", {})

    # The idea is to first go through all database calls and collect
    # the changes per device. Then we can print it all out in a more
    # readable format since it will all be collected per device.
    # For a more complete example, see test_json2tango.py
    #
    # Example of resulting dict:
    # {
    #     "sys/tg_test/1": {
    #         "change_type: "update"
    #         "server": "TangoTest/test",
    #         "device_class": "TangoTest",
    #         "properties": {
    #             "hej": {
    #                 "change_type": "add"
    #                 "value": ["a", "b", "c"]
    #             },
    #             "svej": {
    #                 "change_type": "delete"
    #                 "old_value": ["dsaodkasokd"]
    #             },
    #             "flepp": {
    #                 "change_type": "update"
    #                 "old_value": ["1", "3"],
    #                 "value": ["1", "2", "3"]
    #             }
    #         }
    #     }
    # }

    changes = {"devices": SetterDict(), "classes": SetterDict()}

    # Go through all database calls and store the changes
    for method, args, kwargs in calls:
        if method == "put_device_alias":
            device, alias = args
            if device in device_mapping:
                old_alias = get_device_data(device, device_mapping, data).get("alias")
            else:
                old_alias = None
            new_data = changes["devices"][device]
            new_data.update({"alias": {"old_value": old_alias, "value": alias}})
            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "add_device":
            (info,) = args
            change_type = "move" if info.name in device_mapping else "add"
            changes["devices"][info.name].update(
                change_type=change_type, server=info.server, device_class=info._class
            )
            if info.name in device_mapping:
                old_server, old_instance, old_class = device_mapping[info.name]
                changes["devices"][info.name]["old_server"] = "{}/{}".format(
                    old_server, old_instance
                )
                changes["devices"][info.name]["old_class"] = old_class

        elif method == "delete_device":
            (device,) = args
            server, instance, clss = device_mapping[device]
            device_data = data["servers"][server][instance][clss]
            properties = device_data.get("properties", {})
            changes["devices"][device].update(
                change_type="delete",
                server=f"{server}/{instance}",
                device_class=clss,
                properties=properties,
            )

        elif method == "put_device_property":
            device, properties = args
            old_data = get_device_data(device, device_mapping, data)
            caseless_props = CaselessDict(old_data.get("properties", {}))
            new_data = changes["devices"][device]
            if "properties" not in new_data:
                new_data["properties"] = {}
            for name, value in list(properties.items()):
                old_value = caseless_props.get(name)
                if value != old_value:
                    if old_value is not None:
                        new_data["properties"][name] = {
                            "change_type": "update",
                            "value": value,
                            "old_value": old_value,
                        }
                    else:
                        new_data["properties"][name] = {
                            "change_type": "add",
                            "value": value,
                        }

            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "delete_device_property":
            device, properties = args
            old_data = get_device_data(device, device_mapping, data)
            caseless_props = CaselessDict(old_data.get("properties", {}))
            new_data = changes["devices"][device]
            prop_changes = new_data.setdefault("properties", {})
            for prop in properties:
                old_value = caseless_props[prop]
                prop_changes[prop] = {"change_type": "delete", "old_value": old_value}
            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "put_device_attribute_property":
            device, properties = args
            new_data = changes["devices"][device]
            attr_props = new_data.setdefault("attribute_properties", {})
            old_data = get_device_data(device, device_mapping, data)
            caseless_attrs = CaselessDict(old_data.get("attribute_properties", {}))
            for attr, props in list(properties.items()):
                caseless_props = CaselessDict(caseless_attrs.get(attr, {}))
                for name, value in list(props.items()):
                    old_value = caseless_props.get(name)
                    if value != old_value:
                        if old_value is not None:
                            attr_props[attr][name] = {
                                "change_type": "update",
                                "value": value,
                                "old_value": old_value,
                            }
                        else:
                            attr_props[attr][name] = {
                                "change_type": "add",
                                "value": value,
                            }

            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "delete_device_attribute_property":
            device, attributes = args
            new_data = changes["devices"][device]
            prop_changes = attr_props = new_data.setdefault("attribute_properties", {})
            old_data = get_device_data(device, device_mapping, data)
            caseless_attrs = CaselessDict(old_data.get("attribute_properties", {}))
            for attr, props in list(attributes.items()):
                caseless_props = CaselessDict(caseless_attrs[attr])
                for prop in props:
                    old_value = caseless_props.get(prop)
                    attr_props[attr] = {
                        prop: {"change_type": "delete", "old_value": old_value}
                    }
            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "put_class_property":
            clss, properties = args
            old_data = classes.get(clss, {})
            caseless_props = CaselessDict(old_data.get("properties", {}))
            new_data = changes["classes"][clss]
            prop_changes = new_data.setdefault("properties", {})
            for name, value in list(properties.items()):
                old_value = caseless_props.get(name)
                if value != old_value:
                    if old_value is not None:
                        prop_changes[name] = {
                            "change_type": "update",
                            "value": value,
                            "old_value": old_value,
                        }
                    else:
                        prop_changes[name] = {"change_type": "add", "value": value}
            if "change_type" not in new_data:
                if clss in classes:
                    new_data["change_type"] = "update"
                else:
                    new_data["change_type"] = "add"

        elif method == "delete_class_property":
            clss, properties = args
            old_data = classes.get(clss, {})
            caseless_props = CaselessDict(old_data.get("properties", {}))
            new_data = changes["classes"][clss]
            prop_changes = new_data.setdefault("properties", {})
            for prop in properties:
                old_value = caseless_props.get(prop)
                prop_changes[prop] = {"change_type": "delete", "old_value": old_value}
            if "change_type" not in new_data:
                new_data["change_type"] = "update"

        elif method == "put_class_attribute_property":
            clss, properties = args
            new_data = changes["classes"][clss]
            attr_props = new_data.setdefault("attribute_properties", {})
            old_data = classes.get(clss, {})
            caseless_attrs = CaselessDict(old_data.get("attribute_properties", {}))
            for attr, props in list(properties.items()):
                caseless_props = CaselessDict(caseless_attrs.get(attr, {}))
                for name, value in list(props.items()):
                    old_value = caseless_props.get(name)
                    if value != old_value:
                        if old_value is not None:
                            attr_props[attr] = {
                                name: {
                                    "change_type": "update",
                                    "old_value": old_value,
                                    "value": value,
                                }
                            }
                        else:
                            attr_props[attr] = {
                                name: {
                                    "change_type": "add",
                                    "value": value,
                                }
                            }
            if "change_type" not in new_data:
                if clss in classes:
                    new_data["change_type"] = "update"
                else:
                    new_data["change_type"] = "add"

        elif method == "delete_class_attribute_property":
            clss, attributes = args
            new_data = changes["classes"][clss]
            attr_props = new_data.setdefault("attribute_properties", {})
            old_data = classes.get(clss, {})
            caseless_attrs = CaselessDict(old_data.get("properties", {}))
            for attr, props in list(attributes.items()):
                caseless_props = CaselessDict(caseless_attrs.get(attr, {}))
                for prop in props:
                    old_value = caseless_props.get(prop)
                    attr_props[attr] = {
                        prop: {"change_type": "delete", "old_value": old_value}
                    }
            if "change_type" not in new_data:
                new_data["change_type"] = "update"

    return {
        "devices": changes["devices"].to_dict(),
        "classes": changes["classes"].to_dict(),
    }


def show_actions(data, calls):
    """
    Print out a human readable representation of changes
    """

    changes = get_changes(data, calls)

    # Go through all the devices that have been touched
    # and print out a representation of the changes.
    # TODO: is there a more reasonable way to sort this?
    indent = " " * 2
    for device in sorted(changes["devices"]):
        info = changes["devices"][device]
        change_type = info["change_type"]
        if change_type == "add":
            if info.get("old_server"):
                print("{} Device: {}".format(yellow("="), device))
            else:
                print("{} Device: {}".format(green("+"), device))
        elif change_type == "delete":
            print("{} Device: {}".format(red("-"), device))
        else:
            print("{} Device: {}".format(yellow("="), device))

        if info.get("server"):
            if info.get("old_server"):
                print(
                    "{}Server: {} -> {}".format(
                        indent, red(info["old_server"]), green(info["server"])
                    )
                )
            else:
                print("{}Server: {}".format(indent, info["server"]))

        if info.get("device_class"):
            if info.get("old_class"):
                if info["old_class"] != info["device_class"]:
                    print(
                        "{}Class: {} -> {}".format(
                            indent, info["old_class"], info["device_class"]
                        )
                    )
            else:
                print("{}Class: {}".format(indent, info["device_class"]))

        if info.get("alias"):
            alias = info.get("alias").get("value")
            old_alias = info.get("alias").get("old_value")
            if old_alias:
                if old_alias != alias:
                    print(
                        "{}Alias: {} -> {}".format(indent, red(old_alias), green(alias))
                    )
            else:
                print(f"{indent}Alias: {alias}")

        if info.get("properties"):
            lines = []
            for prop, change in sorted(info.get("properties", {}).items()):
                change_type = change["change_type"]
                value = change.get("value", [])
                old_value = change.get("old_value", [])
                if change_type == "update":
                    # change property
                    lines.append(yellow(f"{indent * 2}= {prop}"))
                    lines.append(property_diff(old_value, value, indent * 3))
                elif change_type == "add":
                    # new property
                    lines.append(green(f"{indent * 2}+ {prop}"))
                    lines.append(green(format_property(value, indent * 3)))
                else:
                    # delete property
                    lines.append(red(f"{indent * 2}- {prop}"))
                    lines.append(red(format_property(old_value, indent * 3)))
            if lines:
                print(f"{indent * 1}Properties:")
                print("\n".join(lines))

        if info.get("attribute_properties"):
            lines = []
            for attr, props in sorted(info.get("attribute_properties", {}).items()):
                attr_lines = []
                for prop, change in sorted(props.items()):
                    change_type = change["change_type"]
                    value = change.get("value", [])
                    old_value = change.get("old_value", [])
                    if change_type == "update":
                        # change
                        attr_lines.append(yellow(f"{indent * 3}= {prop}"))
                        attr_lines.append(property_diff(old_value, value, indent * 4))
                    elif change_type == "add":
                        # addition
                        attr_lines.append(green(f"{indent * 3}+ {prop}"))
                        attr_lines.append(green(format_property(value, indent * 4)))
                    else:
                        # removal
                        attr_lines.append(red(f"{indent * 3}- {prop}"))
                        attr_lines.append(red(format_property(old_value, indent * 4)))
                if attr_lines:
                    lines.append(f"{indent * 2}{attr}")
                    lines.extend(attr_lines)
            if lines:
                print(f"{indent}Attribute properties:")
                print("\n".join(lines))

    for clss in sorted(changes["classes"]):
        info = changes["classes"][clss]

        if info.get("properties"):
            lines = []
            for prop, change in sorted(info.get("properties", {}).items()):
                change_type = change["change_type"]
                value = change.get("value", [])
                old_value = change.get("old_value", [])
                if change_type == "update":
                    lines.append(yellow(f"{indent * 2}= {prop}"))
                    lines.append(property_diff(old_value, value, indent * 3))
                elif change_type == "add":
                    lines.append(green(f"{indent * 2}+ {prop}"))
                    lines.append(green(format_property(value, indent * 3)))
                else:
                    # delete property
                    lines.append(red(f"{indent * 2}- {prop}"))
                    lines.append(red(format_property(old_value, indent * 3)))
            if lines:
                print(f"{indent * 1} Class Properties:")
                print("\n".join(lines))
        print()
