from collections import defaultdict
from dataclasses import dataclass
import ipaddress
import logging
import os
from typing import Optional
import numpy as np
import pandas as pd

from .schema_constants import get_schema_by_tier, TIER_SCHEMA_MAPPING

logger = logging.getLogger(__name__)


class TopologyLoader:
    def __init__(
        self,
        xls_path: str,
        schema_dir: str = None,
    ):
        """
        Initialize TopologyLoader.

        Args:
            xls_path: Path to Excel file
            schema_dir: (DEPRECATED - now ignored) Schema directory path.
                       Schemas are now hardcoded in schema_constants.py
        """
        self.xls_path = xls_path
        self.schema_dir = (
            schema_dir  # Keep for backward compatibility with pool copying
        )

        # Use hardcoded tier mapping configuration
        self.tier_mapping_config = TIER_SCHEMA_MAPPING

        # Load fabric topology using configuration
        self.tier, self.namespace, self.num_pods = self.load_fabric_topology_metadata()

        # Get schema from hardcoded constants
        self.schema = get_schema_by_tier(self.tier)
        self.sheet_name = self.schema["sheet_name"]
        self.allowed_resources = set(self.schema["resource_list"])
        self.resource_columns = self.schema["resources"]

    def load_fabric_topology_metadata(self):
        """Load fabric topology parameters from Excel using configuration"""
        # Get fabric topology configuration
        topo_config = self.tier_mapping_config.get("fabric_topology_config", {})
        params_marker = topo_config.get("parameters_marker", "Parameter")
        parameters = topo_config.get("parameters", {})

        df = pd.read_excel(self.xls_path)
        df = df.replace(r"^\s*$", np.nan, regex=True)

        # Find the parameters table marker
        mask = df.eq(params_marker)
        if not mask.any().any():
            raise ValueError(f"'{params_marker}' not found in DataFrame")
        start_row, start_col = np.where(mask.values)[0][0], np.where(mask.values)[1][0]

        # Find the bounds of the parameters table
        end_row = df.iloc[start_row:, start_col].isna().idxmax() or df.index[-1]
        end_col = (
            df.iloc[start_row, start_col:].reset_index().isna().idxmax().iloc[1]
            + start_col
        ) or df.columns[-1]

        # Extract and process the parameters table
        cropped_df = df.iloc[start_row:end_row, start_col:end_col]
        cropped_df.columns = cropped_df.iloc[0]
        cropped_df = cropped_df.iloc[1:]
        cropped_df = cropped_df.set_index(cropped_df.columns[0])
        cropped_df.index.name = None
        cropped_df.columns.name = None

        # Extract tier, namespace, and num_pods using configuration
        tier_config = parameters.get("tier", {})

        tier = cropped_df.loc[tier_config.get("row_name", "Tier")][
            tier_config.get("column_name", "site#1")
        ]

        ns_config = parameters.get("namespace", {})
        namespace = cropped_df.loc[ns_config.get("row_name", "namespace")][
            ns_config.get("column_name", "namespace")
        ]

        pods_config = parameters.get("num_pods", {})
        num_pods = cropped_df.loc[pods_config.get("row_name", "Number of POD")][
            pods_config.get("column_name", "site#1")
        ]

        return tier, namespace, num_pods

    def load(self):
        df = pd.read_excel(self.xls_path)
        sections = self._split_sections(df)

        # Get resource names from schema
        resource_names = self.schema.get("resource_list", [])

        # Assign VNet context
        vnet_resource_name = next(
            (r for r in resource_names if "Virtual" in r and "Network" in r), None
        )
        if vnet_resource_name and vnet_resource_name in sections:
            sections[vnet_resource_name] = self._assign_vnet_context(
                sections[vnet_resource_name], resource_type=vnet_resource_name
            )

        # Assign node type context for Toponodes
        toponodes_resource_name = "Toponodes"
        if toponodes_resource_name in sections:
            sections[toponodes_resource_name] = self._assign_node_type_context(
                sections[toponodes_resource_name], resource_type=toponodes_resource_name
            )

        # Assign pod labels for tiers 1, 2, or 3
        tier_num = self._extract_tier_number(self.tier)
        if tier_num in [1, 2, 3]:
            # Assign pod labels to Toponodes
            if toponodes_resource_name in sections:
                sections[toponodes_resource_name] = self._assign_pod_labels(
                    sections[toponodes_resource_name],
                    resource_type=toponodes_resource_name,
                )

            # Assign pod labels to ISL (inter-switch links)
            isl_resource_name = next((r for r in resource_names if "ISL" in r), None)
            if isl_resource_name and isl_resource_name in sections:
                sections[isl_resource_name] = self._assign_pod_labels(
                    sections[isl_resource_name], resource_type=isl_resource_name
                )

        # For tier 4, assign pod labels to leaf-spine ISL links based on toponode labels
        elif tier_num == 4:
            isl_resource_name = next((r for r in resource_names if "ISL" in r), None)
            if isl_resource_name and isl_resource_name in sections:
                if toponodes_resource_name in sections:
                    sections[isl_resource_name] = self._assign_tier4_isl_pod_labels(
                        sections[isl_resource_name], sections[toponodes_resource_name]
                    )

        return sections

    def _split_sections(self, df):
        section_col = df.columns[0]
        section_rows = df[df[section_col].notna()][section_col]
        section_indices = list(section_rows.index)
        sections = {}

        for i, start_idx in enumerate(section_indices):
            section_name = section_rows.loc[start_idx]
            found = False
            for res in self.allowed_resources:
                if res.lower() == section_name.lower():
                    section_name = res
                    found = True
                    break
            if not found:
                logger.warning("%s not found in allowed resources", section_name)
                continue

            end_idx = (
                section_indices[i + 1] if i + 1 < len(section_indices) else len(df)
            )

            data_start_row = start_idx + 2
            data_start_col = df.columns.get_loc(section_col) + 1

            block = df.iloc[data_start_row:end_idx, data_start_col:]

            # Header row
            block.columns = block.iloc[0]
            block = block.iloc[1:]
            block = block.loc[:, block.columns.notna()]
            block.columns = block.columns.astype(str).str.strip()
            block = block.dropna(how="all")

            # ---- Filter columns using schema (case-insensitive comparison) ----
            schema_key = section_name
            required_cols = self.resource_columns[schema_key]["required_columns"].keys()
            col_mapping = {}
            for c in block.columns:
                for req_col in required_cols:
                    if c.lower() == req_col.lower():
                        col_mapping[c] = req_col
            block = block.loc[:, col_mapping.keys()]
            block = block.rename(columns=col_mapping)
            sections[section_name] = block.reset_index(names="_excel_row")
            sections[section_name]["_excel_row"] = (
                sections[section_name]["_excel_row"] + 2
            )

        return sections

    def _assign_vnet_context(
        self, df: pd.DataFrame, resource_type: str = "Virtual Networks"
    ) -> pd.DataFrame:
        """Assign virtual network context to rows based on schema configuration"""
        # Get column names from schema
        vnet_schema = self.resource_columns.get(resource_type, {})
        vnet_rules = vnet_schema.get("vnet_context", {})
        vnet_col = vnet_rules.get("vnet_column", "VNET")
        vnet_id_col = vnet_rules.get("vnet_id_column", "_vnet_id")
        vnet_type_col = vnet_rules.get("vnet_type_column", "_vnet_type")

        if vnet_col not in df.columns:
            return df

        vnet_counter = defaultdict(int)
        current_vnet_id = None
        current_vnet_type = None

        vnet_ids = []
        vnet_types = []
        vnet_col_values = []

        for idx, row in df.iterrows():
            if pd.notna(row[vnet_col]):
                current_vnet_type = str(row[vnet_col]).strip().lower()
                vnet_counter[current_vnet_type] += 1
                current_vnet_id = (
                    f"{current_vnet_type}-{vnet_counter[current_vnet_type]}"
                )
                vnet_col_values.append(current_vnet_type)
            elif current_vnet_id is None:
                raise ValueError(
                    f"{resource_type}: Row {row['_excel_row']} has no {vnet_col} and no previous {vnet_col} context"
                )
            else:
                # Fill NaN entries with current vnet type
                vnet_col_values.append(current_vnet_type)

            vnet_ids.append(current_vnet_id)
            vnet_types.append(current_vnet_type)

        df[vnet_col] = vnet_col_values
        df[vnet_id_col] = vnet_ids
        df[vnet_type_col] = vnet_types

        # Fill NaN values with "N.A"
        df[vnet_id_col] = df[vnet_id_col].fillna("N.A")
        df[vnet_type_col] = df[vnet_type_col].fillna("N.A")

        # Add these columns to schema if not present
        if vnet_id_col not in self.resource_columns[resource_type]["required_columns"]:
            self.resource_columns[resource_type]["required_columns"][vnet_id_col] = {
                "type": "string",
                "required": True,
            }
        if (
            vnet_type_col
            not in self.resource_columns[resource_type]["required_columns"]
        ):
            self.resource_columns[resource_type]["required_columns"][vnet_type_col] = {
                "type": "string",
                "required": True,
            }

        return df

    def _assign_node_type_context(
        self, df: pd.DataFrame, resource_type: str = "Toponodes"
    ) -> pd.DataFrame:
        """Extract node type from Network Element and add as column"""
        toponodes_schema = self.resource_columns.get(resource_type, {})
        validation_rules = toponodes_schema.get("validation_rules", {})
        config = validation_rules.get("config", {})
        regex = config.get("node_type_regex", r"^(leaf|spine|superspine|borderleaf)")
        ne_column = "Network Element"
        node_type_col = "_node_type"

        if ne_column not in df.columns:
            return df

        # Extract node type from Network Element column
        df[node_type_col] = df[ne_column].str.extract(regex, expand=False)
        df[node_type_col] = df[node_type_col].fillna("N.A.")
        # Add column to schema if not present
        if (
            node_type_col
            not in self.resource_columns[resource_type]["required_columns"]
        ):
            self.resource_columns[resource_type]["required_columns"][node_type_col] = {
                "type": "string",
                "required": True,
            }

        return df

    def _extract_pod_label_from_labels(self, labels_str: str) -> str:
        """
        Extract pod label value from a labels string.

        Args:
            labels_str: Comma-separated labels string (e.g., "eda.nokia.com/role=leaf,eda.nokia.com/pod=pod1")

        Returns:
            Pod label value (e.g., "eda.nokia.com/pod=pod1") or None if not found
        """
        import re

        if pd.isna(labels_str) or not labels_str:
            return None

        # Look for eda.nokia.com/pod=<value> pattern
        match = re.search(r"eda\.nokia\.com/pod=[^,\s]+", str(labels_str))
        if match:
            return match.group(0)
        return None

    def _assign_tier4_isl_pod_labels(
        self, isl_df: pd.DataFrame, toponodes_df: pd.DataFrame
    ) -> pd.DataFrame:
        """
        Assign pod labels to ISL links for tier 4 topologies.
        Only applies to leaf-to-spine connections.
        Derives pod label from Network Element A's toponode labels.

        Args:
            isl_df: DataFrame containing ISL (inter-switch link) data
            toponodes_df: DataFrame containing Toponode data with pod labels

        Returns:
            DataFrame with pod labels added to leaf-spine ISL connections
        """
        if "Labels" not in isl_df.columns:
            return isl_df

        if (
            "Network Element A" not in isl_df.columns
            or "Network Element B" not in isl_df.columns
        ):
            return isl_df

        # Create lookup dictionary: network_element -> pod_label
        node_to_pod = {}
        if (
            "Network Element" in toponodes_df.columns
            and "Labels" in toponodes_df.columns
        ):
            for _, row in toponodes_df.iterrows():
                node_name = row.get("Network Element")
                labels = row.get("Labels", "")
                pod_label = self._extract_pod_label_from_labels(labels)
                if node_name and pod_label:
                    node_to_pod[str(node_name).strip()] = pod_label

        # Check if we have _node_type column to identify node types
        # If not available, extract from network element name
        if (
            "Network Element" in toponodes_df.columns
            and "_node_type" in toponodes_df.columns
        ):
            node_to_type = {}
            for _, row in toponodes_df.iterrows():
                node_name = row.get("Network Element")
                node_type = row.get("_node_type")
                if node_name and node_type:
                    node_to_type[str(node_name).strip()] = str(node_type).strip()
        else:
            # Fallback: extract from node name
            import re

            node_to_type = {}
            for node_name in node_to_pod.keys():
                match = re.match(r"^(leaf|spine|superspine|borderleaf)", node_name)
                if match:
                    node_to_type[node_name] = match.group(1)

        # Process each ISL row
        for idx, row in isl_df.iterrows():
            element_a = str(row.get("Network Element A", "")).strip()
            element_b = str(row.get("Network Element B", "")).strip()
            current_labels = row.get("Labels", "")

            # Check if this is a leaf-spine connection
            type_a = node_to_type.get(element_a, "")
            type_b = node_to_type.get(element_b, "")

            # Only process leaf-to-spine connections
            is_leaf_spine = (type_a == "leaf" and type_b == "spine") or (
                type_a == "spine" and type_b == "leaf"
            )

            if not is_leaf_spine:
                continue

            # Check if pod label already exists
            existing_pod_label = self._extract_pod_label_from_labels(current_labels)
            if existing_pod_label:
                continue  # Already has pod label, skip

            # Get pod label from Network Element A
            pod_label = node_to_pod.get(element_a)
            if not pod_label:
                continue  # No pod label found for this element

            # Add pod label to ISL
            if pd.isna(current_labels) or str(current_labels).strip() == "":
                isl_df.at[idx, "Labels"] = pod_label
            else:
                labels_str = str(current_labels).strip()
                isl_df.at[idx, "Labels"] = f"{labels_str},{pod_label}"

        return isl_df

    def _extract_tier_number(self, tier: str) -> int:
        """
        Extract numeric tier value from tier string.

        Args:
            tier: Tier string (e.g., "2-tier", "3-tier")

        Returns:
            Integer tier number (e.g., 2, 3)
        """
        import re

        match = re.match(r"(\d+)-tier", str(tier).lower())
        if match:
            return int(match.group(1))
        return 0  # Unknown tier

    def _assign_pod_labels(self, df: pd.DataFrame, resource_type: str) -> pd.DataFrame:
        """
        Assign pod labels to resources for multi-pod topologies (tiers 1, 2, 3).

        For Toponodes: Adds 'eda.nokia.com/pod=pod1' to Labels column if not present
        For ISL links: Adds 'eda.nokia.com/pod=pod1' to Labels column if not present

        Args:
            df: DataFrame containing resource data
            resource_type: Type of resource ("Toponodes" or "Interfaces (ISL)")

        Returns:
            DataFrame with pod labels added
        """
        # Check if Labels column exists
        if "Labels" not in df.columns:
            return df

        # Determine pod identifier based on num_pods
        # For now, we'll use pod1 as default for single pod or first pod
        # This can be extended to handle multiple pods dynamically
        pod_label = "eda.nokia.com/pod=pod1"

        # Process each row
        for idx, row in df.iterrows():
            current_labels = row.get("Labels", "")

            # Handle NaN or empty labels
            if pd.isna(current_labels) or str(current_labels).strip() == "":
                df.at[idx, "Labels"] = pod_label
            else:
                # Check if pod label already exists
                labels_str = str(current_labels).strip()
                if "eda.nokia.com/pod=" not in labels_str:
                    # Add pod label
                    if labels_str:
                        df.at[idx, "Labels"] = f"{labels_str},{pod_label}"
                    else:
                        df.at[idx, "Labels"] = pod_label

        return df
