"""
XLS parsers: each returns a list of dicts (dynamic fields only) for use with
J2 templates or JSON output. No resource_models dependency.
"""

from typing import List, Optional
import re
import pandas as pd

from config import get_software_version


def _parse_labels(labels: Optional[str]) -> dict:
    """Parse labels from Labels column (newline or comma-separated key=value or key:value)."""
    out = {}
    if not labels or pd.isna(labels):
        return out
    for line in str(labels).replace("\n", ",").split(","):
        line = line.strip()
        if not line:
            continue
        if "=" in line:
            key, value = line.split("=", 1)
        elif ":" in line:
            key, value = line.split(":", 1)
        else:
            continue
        out[key.strip()] = value.strip()
    return out


def _parse_selectors(selectors_str: str) -> List[str]:
    """Parse newline and/or comma-separated selectors into list."""
    if not selectors_str or pd.isna(selectors_str):
        return []
    out = []
    for line in str(selectors_str).split("\n"):
        for part in line.split(","):
            part = part.strip()
            if part:
                out.append(part)
    return out


# ---------------------------------------------------------------------------
# ISL Interface Parser
# ---------------------------------------------------------------------------


class ISLInterfaceParser:
    def __init__(self, namespace: str):
        self.namespace = namespace

    def parse(self, df: pd.DataFrame) -> List[dict]:
        out = []
        for _, row in df.iterrows():
            out.append(self._interface_dict(row["Network Element A"], row["Port on Device A"], row["Ethernet Speed"], row.get("Encapsulation"), row.get("Labels")))
            out.append(self._interface_dict(row["Network Element B"], row["Port on Device B"], row["Ethernet Speed"], row.get("Encapsulation"), row.get("Labels")))
        return out

    def _interface_dict(
        self,
        node: str,
        port: str,
        speed: str,
        encapsulation: Optional[str],
        labels: Optional[str],
    ) -> dict:
        name = f"{node}-{port}".replace("/", "-")
        encap = None
        if encapsulation and pd.notna(encapsulation):
            enc = str(encapsulation).lower()
            if enc == "dot1q":
                encap = "dot1q"
            elif enc == "null":
                encap = "null"
        return {
            "name": name,
            "namespace": self.namespace,
            "labels": _parse_labels(labels),
            "members": [{"node": node, "interface": port, "enabled": True, "lacpPortPriority": 32768}],
            "type": "interface",
            "ethernet": {"speed": speed},
            "encapType": encap,
            "lldp": True,
        }


# ---------------------------------------------------------------------------
# Edge Interface Parser
# ---------------------------------------------------------------------------


class EdgeInterfaceParser:
    def __init__(self, namespace: str):
        self.namespace = namespace

    def parse(self, df: pd.DataFrame) -> List[dict]:
        out = []
        lag_df = df[df["Type"].str.contains("lag", case=False, na=False)]
        non_lag_df = df[~df["Type"].str.contains("lag", case=False, na=False)]

        for _lag_name, group in lag_df.groupby("Type"):
            out.append(self._lag_dict(group))

        for _, row in non_lag_df.iterrows():
            out.append(self._physical_dict(row))
        return out

    def _physical_dict(self, row: pd.Series) -> dict:
        node = row["Network Element"]
        port = row["Port on Device"]
        name = f"{node}-{port}".replace("/", "-")
        encap = self._encap(row.get("Encapsulation"))
        return {
            "name": name,
            "namespace": self.namespace,
            "labels": _parse_labels(row.get("Labels")),
            "members": [{"node": node, "interface": port, "enabled": True, "lacpPortPriority": 32768}],
            "type": "interface",
            "ethernet": {"speed": row["Ethernet Speed"]},
            "encapType": encap,
            "lldp": True,
        }

    def _lag_dict(self, group: pd.DataFrame) -> dict:
        first = group.iloc[0]
        encap = self._encap(first.get("Encapsulation"))
        members = [{"node": row["Network Element"], "interface": row["Port on Device"], "enabled": True, "lacpPortPriority": 32768} for _, row in group.iterrows()]
        lag_name = first["Type"]

        multihoming = None
        mh = first.get("Multihoming type")
        if mh and pd.notna(mh):
            mode_str = str(mh).strip().lower()
            if mode_str == "active/active":
                mode = "all-active"
            elif mode_str == "active/standby":
                mode = "single-active"
            else:
                mode = mode_str
            multihoming = {"mode": mode}

        lag = {"type": "lacp", "multihoming": multihoming} if multihoming else {"type": "lacp"}

        return {
            "name": lag_name,
            "namespace": self.namespace,
            "labels": _parse_labels(first.get("Labels")),
            "members": members,
            "type": "lag",
            "ethernet": {"speed": first["Ethernet Speed"]},
            "encapType": encap,
            "lldp": True,
            "lag": lag,
        }

    def _encap(self, encapsulation: Optional[str]) -> Optional[str]:
        if not encapsulation or pd.isna(encapsulation):
            return None
        enc = str(encapsulation).lower()
        if enc == "dot1q":
            return "dot1q"
        if enc == "null":
            return "null"
        return None


# ---------------------------------------------------------------------------
# Fabric Parser
# ---------------------------------------------------------------------------


class FabricParser:
    def __init__(self, namespace: str, tier: Optional[str] = None):
        self.namespace = namespace
        self.tier = tier
        self.tier_num = self._tier_number(tier) if tier else 0

    def _tier_number(self, tier: str) -> int:
        m = re.match(r"(\d+)-tier", str(tier).lower())
        return int(m.group(1)) if m else 0

    def parse(self, df: pd.DataFrame) -> List[dict]:
        return [self._fabric_dict(row) for _, row in df.iterrows()]

    def _fabric_dict(self, row: pd.Series) -> dict:
        fabric_name = row["Fabric"]
        fabric_selectors_str = row.get("Fabric Selectors", "")
        is_superspine = (
            bool(fabric_selectors_str)
            and pd.notna(fabric_selectors_str)
            and str(fabric_selectors_str).strip().upper() not in ("N.A", "N.A.")
        )
        fabric_selectors = _parse_selectors(fabric_selectors_str) if is_superspine else None

        is_pod_tier4 = self.tier_num == 4 and fabric_name.lower().startswith("pod")

        if self.tier_num in (1, 2, 3):
            labels = {"fabric": "pod1"}
        else:
            labels = {"eda.nokia.com/role": fabric_name}

        underlay_str = str(row.get("underlay protocol", "ebgp")).lower()
        overlay_str = str(row.get("overlay protocol", "ibgp")).lower()
        link_selector_str = row.get("Link Selector", "")

        if is_pod_tier4:
            underlay = self._underlay(underlay_str)
            overlay = self._overlay_tier4_pod(overlay_str, fabric_name)
            isl = self._isl_tier4_pod(link_selector_str, fabric_name)
            leafs = self._leafs_tier4(fabric_name)
            spines = self._spines_tier4(fabric_name)
            border_leafs = super_spines = None
        elif is_superspine and self.tier_num == 4:
            underlay = self._underlay(underlay_str)
            overlay = self._overlay_tier4_superspine(overlay_str, row.get("RR selector"), row.get("RR client selector"), fabric_selectors)
            isl = self._isl(link_selector_str)
            node_sel = row.get("Node Selectors", "")
            border_leafs, super_spines = self._nodes_tier4_superspine(node_sel)
            leafs = spines = None
        elif is_superspine:
            underlay = self._underlay(underlay_str)
            overlay = self._overlay(overlay_str, row.get("RR selector"), row.get("RR client selector"))
            isl = self._isl(link_selector_str)
            border_leafs = self._border_leafs()
            super_spines = self._super_spines()
            leafs = spines = None
        else:
            underlay = self._underlay(underlay_str)
            overlay = self._overlay_tier123(overlay_str, row.get("RR selector"), row.get("RR client selector"))
            isl = self._isl_tier123(link_selector_str)
            leafs, spines, border_leafs = self._nodes_tier123(row.get("Node Selectors", ""))
            super_spines = None

        d = {
            "name": fabric_name,
            "namespace": self.namespace,
            "labels": labels,
            "interSwitchLinks": isl,
            "overlayProtocol": overlay,
            "underlayProtocol": underlay,
            "systemPoolIPV4": "systemipv4-pool",
        }
        if fabric_selectors:
            d["fabricSelector"] = fabric_selectors
        if leafs:
            d["leafs"] = leafs
        if spines:
            d["spines"] = spines
        if border_leafs:
            d["borderLeafs"] = border_leafs
        if super_spines:
            d["superSpines"] = super_spines
        return d

    def _underlay(self, protocol: str) -> dict:
        return {
            "protocol": [protocol.upper()],
            "bfd": {
                "desiredMinTransmitInt": 1000000,
                "detectionMultiplier": 3,
                "enabled": True,
                "minEchoReceiveInterval": 1000000,
                "requiredMinReceive": 1000000,
            },
            "bgp": {"asnPool": "asn-pool", "timers": {}},
        }

    def _overlay(self, protocol: str, rr_sel: Optional[str], rr_client_sel: Optional[str]) -> dict:
        rr = _parse_selectors(rr_sel) if rr_sel else None
        rr_client = _parse_selectors(rr_client_sel) if rr_client_sel else None
        return {
            "protocol": protocol.upper(),
            "bfd": {
                "desiredMinTransmitInt": 1000000,
                "detectionMultiplier": 3,
                "enabled": True,
                "minEchoReceiveInterval": 1000000,
                "requiredMinReceive": 1000000,
            },
            "bgp": {
                "autonomousSystem": 65501,
                "clusterID": "1.1.1.2",
                "rrNodeSelector": rr,
                "rrClientNodeSelector": rr_client,
                "timers": {},
            },
        }

    def _overlay_tier123(self, protocol: str, rr_sel: Optional[str], rr_client_sel: Optional[str]) -> dict:
        rr = _parse_selectors(rr_sel) if rr_sel else []
        rr_client = _parse_selectors(rr_client_sel) if rr_client_sel else []
        rr_pod = [f"{s},eda.nokia.com/pod=pod1" for s in rr] if rr else None
        rr_client_pod = [f"{s},eda.nokia.com/pod=pod1" for s in rr_client] if rr_client else None
        bgp = None
        if protocol.upper() == "IBGP":
            bgp = {
                "autonomousSystem": 65501,
                "clusterID": "1.1.1.1",
                "rrNodeSelector": rr_pod,
                "rrClientNodeSelector": rr_client_pod,
                "timers": {},
            }
        return {
            "protocol": protocol.upper(),
            "bfd": {
                "desiredMinTransmitInt": 1000000,
                "detectionMultiplier": 3,
                "enabled": True,
                "minEchoReceiveInterval": 1000000,
                "requiredMinReceive": 1000000,
            },
            "bgp": bgp,
        }

    def _overlay_tier4_pod(self, protocol: str, fabric_name: str) -> dict:
        m = re.search(r"pod(\d+)", fabric_name.lower())
        pod_num = int(m.group(1)) if m else 1
        cluster_id = f"1.1.1.{pod_num}"
        return {
            "protocol": protocol.upper(),
            "bfd": {
                "desiredMinTransmitInt": 1000000,
                "detectionMultiplier": 3,
                "enabled": True,
                "minEchoReceiveInterval": 1000000,
                "requiredMinReceive": 1000000,
            },
            "bgp": {
                "autonomousSystem": 65501,
                "clusterID": cluster_id,
                "rrNodeSelector": None,
                "rrClientNodeSelector": None,
                "timers": {},
            },
        }

    def _overlay_tier4_superspine(
        self,
        protocol: str,
        rr_sel: Optional[str],
        rr_client_sel: Optional[str],
        fabric_selectors: Optional[List[str]],
    ) -> dict:
        rr = _parse_selectors(rr_sel) if rr_sel else []
        rr_client = _parse_selectors(rr_client_sel) if rr_client_sel else []
        pod_names = []
        if fabric_selectors:
            for s in fabric_selectors:
                m = re.search(r"pod\d+", s)
                if m:
                    pod_names.append(m.group(0))
        expanded = []
        for s in rr_client:
            if "role=leaf" in s and "pod=" not in s:
                for p in pod_names:
                    expanded.append(f"{s},eda.nokia.com/pod={p}")
            else:
                expanded.append(s)
        return {
            "protocol": protocol.upper(),
            "bfd": {
                "desiredMinTransmitInt": 1000000,
                "detectionMultiplier": 3,
                "enabled": True,
                "minEchoReceiveInterval": 1000000,
                "requiredMinReceive": 1000000,
            },
            "bgp": {
                "autonomousSystem": 65501,
                "clusterID": "1.1.1.2",
                "rrNodeSelector": rr if rr else None,
                "rrClientNodeSelector": expanded if expanded else None,
                "timers": {},
            },
        }

    def _isl(self, link_selector: str) -> dict:
        sel = _parse_selectors(link_selector)
        return {
            "linkSelector": sel if sel else None,
            "poolIPV4": "ipv4-pool",
            "qos": {},
        }

    def _isl_tier123(self, link_selector: str) -> dict:
        sel = _parse_selectors(link_selector)
        with_pod = [f"{s}, eda.nokia.com/pod=pod1" for s in sel] if sel else None
        return {
            "linkSelector": with_pod,
            "poolIPV4": "ipv4-pool",
        }

    def _isl_tier4_pod(self, link_selector: str, fabric_name: str) -> dict:
        sel = _parse_selectors(link_selector)
        has_pod = any("eda.nokia.com/pod=" in s for s in sel) if sel else False
        if not has_pod and sel:
            sel = [f"{sel[0]}, eda.nokia.com/pod={fabric_name}"] + sel[1:]
        elif not sel:
            sel = [f"eda.nokia.com/role=interSwitch,eda.nokia.com/pod={fabric_name}"]
        return {
            "linkSelector": sel,
            "poolIPV4": "ipv4-pool",
            "qos": {},
        }

    def _leafs_tier4(self, fabric_name: str) -> dict:
        return {
            "asnPool": "asn-pool",
            "leafNodeSelector": [f"eda.nokia.com/role=leaf,eda.nokia.com/pod={fabric_name}"],
        }

    def _spines_tier4(self, fabric_name: str) -> dict:
        return {
            "asnPool": "asn-pool",
            "spineNodeSelector": [f"eda.nokia.com/role=spine,eda.nokia.com/pod={fabric_name}"],
        }

    def _border_leafs(self) -> dict:
        return {"asnPool": "asn-pool", "borderLeafNodeSelector": ["eda.nokia.com/role=borderleaf"]}

    def _super_spines(self) -> dict:
        return {"asnPool": "asn-pool", "superSpineNodeSelector": ["eda.nokia.com/role=superspine"]}

    def _nodes_tier4_superspine(self, node_selectors_str: str) -> tuple:
        sel = _parse_selectors(node_selectors_str)
        border_leafs = super_spines = None
        bl = [s for s in sel if "borderleaf" in s.lower()]
        if bl:
            border_leafs = {"asnPool": "asn-pool", "borderLeafNodeSelector": bl}
        ss = [s for s in sel if "superspine" in s.lower()]
        if ss:
            super_spines = {"asnPool": "asn-pool", "superSpineNodeSelector": ss}
        return border_leafs, super_spines

    def _nodes_tier123(self, node_selectors_str: str) -> tuple:
        sel = _parse_selectors(node_selectors_str)
        leafs = spines = border_leafs = None
        leaf_sel = [s for s in sel if "leaf" in s.lower() and "border" not in s.lower()]
        if leaf_sel:
            leafs = {"asnPool": "asn-pool", "leafNodeSelector": [f"{s},eda.nokia.com/pod=pod1" for s in leaf_sel]}
        spine_sel = [s for s in sel if "spine" in s.lower() and "super" not in s.lower()]
        if spine_sel:
            spines = {"asnPool": "asn-pool", "spineNodeSelector": [f"{s},eda.nokia.com/pod=pod1" for s in spine_sel]}
        bl_sel = [s for s in sel if "borderleaf" in s.lower()]
        if bl_sel:
            border_leafs = {"asnPool": "asn-pool", "borderLeafNodeSelector": [f"{s},eda.nokia.com/pod=pod1" for s in bl_sel]}
        return leafs, spines, border_leafs


# ---------------------------------------------------------------------------
# TopoLink Parser
# ---------------------------------------------------------------------------


class TopoLinkParser:
    def __init__(self, namespace: str):
        self.namespace = namespace

    def parse(self, df: pd.DataFrame) -> List[dict]:
        return [self._link_dict(row) for _, row in df.iterrows()]

    def _link_dict(self, row: pd.Series) -> dict:
        node_a = row["Network Element A"]
        port_a = row["Port on Device A"]
        node_b = row["Network Element B"]
        port_b = row["Port on Device B"]
        pa = port_a.replace("/", "-").replace("ethernet-", "e")
        pb = port_b.replace("/", "-").replace("ethernet-", "e")
        name = f"{node_a}-{pa}-{node_b}-{pb}"

        labels = _parse_labels(row.get("Labels"))
        if "eda.nokia.com/role" not in labels:
            labels["eda.nokia.com/role"] = "interSwitch"

        if_a = f"{node_a}-{port_a}".replace("/", "-")
        if_b = f"{node_b}-{port_b}".replace("/", "-")
        link = {
            "local": {"node": node_a, "interface": port_a, "interfaceResource": if_a},
            "remote": {"node": node_b, "interface": port_b, "interfaceResource": if_b},
            "speed": row["Ethernet Speed"],
            "type": "interSwitch",
        }
        return {
            "name": name,
            "namespace": self.namespace,
            "labels": labels,
            "links": [link],
        }


# ---------------------------------------------------------------------------
# Toponodes Parser
# ---------------------------------------------------------------------------


class ToponodesParser:
    def __init__(self, namespace: str):
        self.namespace = namespace

    def parse(self, df: pd.DataFrame) -> List[dict]:
        return [self._node_dict(row) for _, row in df.iterrows()]

    def _node_dict(self, row: pd.Series) -> dict:
        name = row["Network Element"]
        platform = row.get("HW Type") if pd.notna(row.get("HW Type")) else "unknown"
        version = (
            row.get("Software Version") if pd.notna(row.get("Software Version")) else None
        ) or get_software_version()
        node_profile = f"node-profile-srlinux-{version}"
        return {
            "name": name,
            "namespace": self.namespace,
            "labels": _parse_labels(row.get("Labels")),
            "nodeProfile": node_profile,
            "operatingSystem": "srl",
            "platform": platform,
            "version": version,
            "npp": {"mode": "normal"},
        }


# ---------------------------------------------------------------------------
# VirtualNetwork Parser
# ---------------------------------------------------------------------------


class VirtualNetworkParser:
    def __init__(self, namespace: str):
        self.namespace = namespace

    def parse(self, df: pd.DataFrame) -> List[dict]:
        out = []
        for vnet_id, group in df.groupby("_vnet_id"):
            out.append(self._vnet_dict(vnet_id, group))
        return out

    def _vnet_dict(self, vnet_id: str, group: pd.DataFrame) -> dict:
        vnet_type = group.iloc[0].get("_vnet_type", "layer3")
        bridge_names = group["Bridge Domain"].unique()
        bridge_domains = [{"name": bd, "spec": {"eviPool": "evi-pool", "macLearning": True, "tunnelIndexPool": "tunnel-index-pool", "type": "EVPNVXLAN", "vniPool": "vni-pool"}} for bd in bridge_names]

        routers = []
        if vnet_type == "layer3":
            for rn in group["Router"].dropna().unique():
                routers.append({"name": rn, "spec": {"configuredName": rn, "eviPool": "evi-pool", "tunnelIndexPool": "tunnel-index-pool", "type": "EVPNVXLAN", "vniPool": "vni-pool"}})

        vlans = []
        irb_interfaces = []
        for _, row in group.iterrows():
            vlan_id = str(int(row["Vlan id"]))
            if_sel = _parse_selectors(row.get("interfaceSelector", ""))
            vlans.append({
                "name": f"vlan{vlan_id}",
                "spec": {
                    "bridgeDomain": row["Bridge Domain"],
                    "interfaceSelector": if_sel if if_sel else None,
                    "vlanID": vlan_id,
                },
            })
            if vnet_type == "layer3":
                irb_ipv4 = row.get("IRB ipv4")
                if irb_ipv4 and pd.notna(irb_ipv4) and str(irb_ipv4).upper() != "N.A.":
                    irb_interfaces.append({
                        "name": f"irb{vlan_id}",
                        "spec": {
                            "arpTimeout": 14400,
                            "bridgeDomain": row["Bridge Domain"],
                            "ipAddresses": [{"ipv4Address": {"ipPrefix": str(irb_ipv4), "primary": True}}],
                            "ipMTU": 1500,
                            "learnUnsolicited": "NONE",
                            "router": row["Router"],
                        },
                    })

        return {
            "name": vnet_id,
            "namespace": self.namespace,
            "bridgeDomains": bridge_domains if bridge_domains else None,
            "irbInterfaces": irb_interfaces if irb_interfaces else None,
            "routers": routers if routers else None,
            "vlans": vlans if vlans else None,
        }
