import json
from typing import Any

from Kathara.exceptions import MachineNotRunningError
from Kathara.model.Lab import Lab

from ..foundation.checks.AbstractCheck import AbstractCheck
from ..foundation.model.CheckResult import CheckResult
from ..model.FailedCheck import FailedCheck
from ..model.SuccessfulCheck import SuccessfulCheck
from ..utils import get_output, key_exists


def filter_by_interface_type(interface_type: str, interfaces: list[dict]):
    return list(filter(lambda x: "linkinfo" in x and x["linkinfo"]["info_kind"] == interface_type, interfaces))


def get_interface_by_name(interface_name: str, interfaces: list[dict]):
    return list(filter(lambda x: x["ifname"] == interface_name, interfaces)).pop()


def get_inteface_by_vni(interface_vni: str, interfaces: list[dict]):
    return list(
        filter(
            lambda x: "linkinfo" in x
                      and "id" in x["linkinfo"]["info_data"]
                      and x["linkinfo"]["info_data"]["id"] == int(interface_vni),
            interfaces,
        )
    ).pop()


class BridgeCheck(AbstractCheck):

    def __init__(self, lab: Lab, description: str = None):
        super().__init__(lab, description=description, priority=60)

    def check_bridge_interfaces(
            self, device_name: str, expected_interfaces: list[str], actual_interfaces: list[dict]
    ) -> (CheckResult, set[str]):
        self.description = (
            f"Checking that interfaces {expected_interfaces} " f"are attached to the same bridge on `{device_name}`"
        )
        interface_masters = {}
        interfaces_not_found = []
        interfaces_without_bridge = []
        for interface_name in expected_interfaces:
            try:
                actual_iface_info = get_interface_by_name(interface_name, actual_interfaces)
                if "master" in actual_iface_info:
                    interface_masters[interface_name] = actual_iface_info["master"]
                else:
                    interfaces_without_bridge.append(interface_name)
            except IndexError:
                interfaces_not_found.append(interface_name)

        if interfaces_not_found:
            return (
                FailedCheck(
                    self.description,
                    f"Interfaces `{interfaces_not_found}` are not found on {device_name}",
                ),
                None,
            )
        if interfaces_without_bridge:
            return (
                FailedCheck(
                    self.description,
                    f"Interfaces `{interfaces_without_bridge}` are not connected to any bridge on {device_name}",
                ),
                None,
            )
        masters = set(interface_masters.values())
        masters_num = len(masters)
        if masters_num == 1:
            master = list(masters)[0]
            if get_interface_by_name(master, actual_interfaces)["linkinfo"]["info_kind"]:
                return SuccessfulCheck(self.description), masters
        elif masters_num == 0:
            return FailedCheck(self.description, "No interfaces attached to the bridge"), masters
        elif masters_num > 1:
            reason = "Interfaces are not attached to the same bridge.\n"
            for interface_name, interface_master in interface_masters.items():
                master_type = get_interface_by_name(interface_master, actual_interfaces)["linkinfo"]["info_kind"]
                reason += f"`{interface_name}` to `{interface_master}` (type: {master_type})\n"
            return FailedCheck(self.description, reason), masters

    def check_vlan_filtering(self, device_name: str, bridge_info: dict) -> CheckResult:
        self.description = f"Checking if VLAN filtering is enabled on `{bridge_info['ifname']}` of `{device_name}`"
        if (
                "vlan_filtering" in bridge_info["linkinfo"]["info_data"]
                and bridge_info["linkinfo"]["info_data"]["vlan_filtering"] == 1
        ):
            return SuccessfulCheck(self.description)
        else:
            return FailedCheck(
                self.description,
                f"VLAN filtering not enabled on `{bridge_info['ifname']}` of `{device_name}`",
            )

    def check_vlan_tags(
            self,
            device_name: str,
            interface_name: str,
            interface_configuration: dict,
            actual_interface_vlan: dict,
    ):
        self.description = (
            f"Checking that vlans `{interface_configuration['vlan_tags']}` "
            f"are configured on `{interface_name}` of `{device_name}`"
        )
        expected_vlans = set(interface_configuration["vlan_tags"])
        actual_vlans = set(map(lambda x: x["vlan"], actual_interface_vlan["vlans"]))
        actual_vlans.remove(1)

        if expected_vlans == actual_vlans:
            return SuccessfulCheck(self.description)
        else:
            not_configured = expected_vlans.difference(actual_vlans)
            reason = f"Vlans `{not_configured}` are not configured on `{interface_name}` of `{device_name}`"
            return FailedCheck(self.description, reason)

    def check_vxlan_pvid(
            self, device_name: str, vni: str, pvid: str, actual_interfaces: list[dict], vlans_info: list[dict]
    ):
        self.description = f"Checking that `{device_name}` manages VNI `{vni}` with PVID `{pvid}`"

        try:
            interface_name = get_inteface_by_vni(vni, actual_interfaces)["ifname"]
        except IndexError:
            return FailedCheck(self.description, f"VNI {vni} not configured on `{device_name}`")

        for vlan in vlans_info:
            if vlan["ifname"] == interface_name:
                actual_pvid = set(map(lambda x: x["vlan"], filter(lambda x: "PVID" in x["flags"], vlan["vlans"])))
                if pvid:
                    actual_pvid = actual_pvid.pop()
                    if actual_pvid == pvid:
                        return SuccessfulCheck(self.description)
                    else:
                        return FailedCheck(
                            self.description,
                            f"VNI `{vni}` found with pvid `{actual_pvid}` (instead of {pvid})",
                        )
        return FailedCheck(self.description, f"VNI `{vni}` not found on `{device_name}`")

    def check_vlan_pvid(self, device_name: str, interface_name: str, interface_pvid: str, actual_interface_vlan: dict):
        self.description = f"Checking that `{interface_name}` of `{device_name}` has pvid {interface_pvid}"
        pvid = set(map(lambda x: x["vlan"], filter(lambda x: "PVID" in x["flags"], actual_interface_vlan["vlans"])))
        if pvid:
            actual_pvid = pvid.pop()
            if interface_pvid == actual_pvid:
                return SuccessfulCheck(self.description)
            else:
                reason = (
                    f"`{interface_name}` of `{device_name}` has pvid `{actual_pvid}` "
                    f"(instead of `{interface_pvid}`)"
                )
                return FailedCheck(self.description, reason)
        else:
            reason = f"No pvid configured on `{interface_name}` of `{device_name}`"
            return FailedCheck(self.description, reason)

    def run(self, configuration: dict[str, Any]) -> list[CheckResult]:
        results = []

        if 'bridges' not in configuration:
            return results

        devices_to_bridge_configuration = configuration["bridges"]
        for device_name, bridges_configuration in devices_to_bridge_configuration.items():
            self.logger.info(f"Checking bridges configuration on `{device_name}`...")
            try:
                ip_link_output = get_output(
                    self.kathara_manager.exec(
                        machine_name=device_name,
                        lab_hash=self.lab.hash,
                        command="ip -d -j link",
                    )
                )
                bridge_vlan_output = get_output(
                    self.kathara_manager.exec(
                        machine_name=device_name,
                        lab_hash=self.lab.hash,
                        command="bridge -j vlan",
                    )
                )
            except MachineNotRunningError as e:
                return [FailedCheck(self.description, str(e))]

            actual_interfaces = json.loads(ip_link_output)

            actual_vlans = json.loads(bridge_vlan_output)

            for bridge_conf in bridges_configuration:
                expected_bridge_interfaces = list(bridge_conf["interfaces"].keys())
                vxlan_interfaces = []
                vxlan_interfaces_names = []
                if "vxlan" in bridge_conf:
                    for vni in bridge_conf["vxlan"]:
                        try:
                            interface = get_inteface_by_vni(vni, actual_interfaces)
                        except IndexError:
                            continue
                        vxlan_interfaces_names.append(interface["ifname"])
                        vxlan_interfaces.append(interface)
                    expected_bridge_interfaces.extend(vxlan_interfaces_names)

                check_result, masters = self.check_bridge_interfaces(
                    device_name, expected_bridge_interfaces, actual_interfaces
                )
                results.append(check_result)

                if check_result.passed:
                    check_result = self.check_vlan_filtering(
                        device_name, get_interface_by_name(masters.pop(), actual_interfaces)
                    )
                    results.append(check_result)

                    for interface_name, interface_conf in bridge_conf["interfaces"].items():
                        description = f"Getting VLAN info for `{interface_name}` on `{device_name}`"
                        actual_interface_vlans = None
                        try:
                            actual_interface_vlans = get_interface_by_name(interface_name, actual_vlans)
                            check_result = SuccessfulCheck(description)
                            results.append(check_result)
                        except IndexError:
                            check_result = FailedCheck(
                                description,
                                f"No VLAN found for for `{interface_name}` on `{device_name}`",
                            )
                            results.append(check_result)

                        if actual_interface_vlans:
                            if "vlan_tags" in interface_conf:
                                check_result = self.check_vlan_tags(
                                    device_name, interface_name, interface_conf, actual_interface_vlans
                                )
                                results.append(check_result)

                            if "pvid" in interface_conf:
                                check_result = self.check_vlan_pvid(
                                    device_name,
                                    interface_name,
                                    interface_conf["pvid"],
                                    actual_interface_vlans,
                                )
                                results.append(check_result)

                    if "vxlan" in bridge_conf:
                        for vni, vni_info in bridge_conf["vxlan"].items():
                            check_result = self.check_vxlan_pvid(
                                device_name, vni, vni_info["pvid"], actual_interfaces, actual_vlans
                            )
                            results.append(check_result)
        return results

    def run_from_configuration(self, configuration: dict) -> list[CheckResult]:
        self.logger.info("Verifying bridges configuration inside devices...")
        if key_exists(["test", "bridges"], configuration):
            return self.run(configuration["test"]["bridges"])
        else:
            return []
