from dataclasses import dataclass
import ipaddress
from .common_val_functions import validate_enum_constraint
import pandas as pd


@dataclass
class VNetGroupContext:
    """Context object for VNet grouping validation handlers"""

    group: pd.DataFrame
    vnet_id: str
    vnet_type: str
    excel_rows: list
    config: dict


class VNETValidator:
    def __init__(self, vnet_schema: dict):
        self.vnet_schema = vnet_schema

    def validate_virtual_networks(self, df: pd.DataFrame) -> list:
        """Validate Virtual Networks section by processing constraints and grouping constraints"""
        if df is None or df.empty:
            return []
        errors = []

        validation_rules = self.vnet_schema.get("validation_rules", {})
        config = validation_rules.get("config", {})

        # Define constraint handlers
        constraint_handlers = {
            "enum_validation": validate_enum_constraint,
        }

        # Define grouping constraint handlers
        grouping_handlers = {
            "vnet_grouping": self._validate_vnet_grouping,
        }

        # Process non-grouping constraints
        constraints = validation_rules.get("constraints", [])
        for constraint in constraints:
            constraint_type = constraint.get("type")
            handler = constraint_handlers.get(constraint_type)
            if handler:
                errors.extend(
                    handler(df=df, constraint=constraint, config=config) or []
                )

        # Process grouping constraints
        grouping_constraints = validation_rules.get("grouping_constraints", [])
        for strategy in grouping_constraints:
            # Each strategy should specify which grouping handler to use
            # For backwards compatibility, default to vnet_grouping
            grouping_type = strategy.get("type", "vnet_grouping")
            handler = grouping_handlers.get(grouping_type)
            if handler:
                errors.extend(handler(df=df, strategy=strategy, config=config) or [])

        return errors

    def _validate_vnet_grouping(
        self,
        df: pd.DataFrame,
        strategy: dict,
        config: dict,
    ) -> list:
        """Handler for vnet_grouping - validates Virtual Networks by grouping"""
        errors = []
        if df is None or df.empty:
            return errors

        # Extract strategy-specific parameters
        groupby_column = strategy.get("groupby_column")
        constraints = strategy.get("constraints", [])

        vnet_context = config.get("vnet_context", {})
        vnet_id_col = groupby_column
        vnet_type_col = vnet_context.get("vnet_type_column", "_vnet_type")

        if not vnet_id_col or vnet_id_col not in df.columns:
            return errors

        handlers = {
            "required_for_type": self._handle_vnet_required_for_type,
            "unique_per_group": self._handle_vnet_unique_per_group,
            "ip_address": self._handle_vnet_ip_address,
        }

        for vnet_id, group in df.groupby(vnet_id_col):
            vnet_type = group[vnet_type_col].iloc[0]
            excel_rows = group["_excel_row"].tolist()

            for constraint in constraints:
                constraint_type = constraint.get("type")
                handler = handlers.get(constraint_type)
                if not handler:
                    continue

                # Create context object with all fields populated
                context = VNetGroupContext(
                    group=group,
                    vnet_id=vnet_id,
                    vnet_type=vnet_type,
                    excel_rows=excel_rows,
                    config=constraint.get("config", {}),
                )

                constraint_errors = handler(context)
                errors.extend(constraint_errors or [])

        return errors

    def _handle_vnet_required_for_type(
        self,
        context: VNetGroupContext,
    ) -> list:
        """Handle required-for-type constraint for VNet grouping"""
        errors = []
        required_type = context.config.get("vnet_type")
        required_cols = context.config.get("required_columns", [])
        allowed_values_map = context.config.get("allowed_values_for_columns", {})

        if context.vnet_type != required_type:
            return errors

        for col in required_cols:

            if col in allowed_values_map:
                allowed_vals = [v.lower() for v in allowed_values_map.get(col, [])]
                for idx, cell in context.group[col].items():
                    if pd.isna(cell) or str(cell).strip().lower() not in allowed_vals:
                        excel_row = context.group.loc[idx, "_excel_row"]
                        errors.append(
                            f"VNet {context.vnet_id} (row {excel_row}) column '{col}' has invalid or missing value '{cell}'; allowed values for {required_type} are {allowed_values_map.get(col)}"
                        )

        return errors

    def _handle_vnet_unique_per_group(
        self,
        context: VNetGroupContext,
    ) -> list:
        """Handle uniqueness constraint for VNet grouping"""
        errors = []
        unique_cols = context.config.get("unique_columns", [])
        for col in unique_cols:
            if col not in context.group.columns:
                continue
            dupes = context.group[col][context.group[col].duplicated()]
            if not dupes.empty:
                errors.append(
                    f"VNet {context.vnet_id} has duplicate {col} values: {dupes.tolist()}"
                )

        return errors

    def _handle_vnet_ip_address(
        self,
        context: VNetGroupContext,
    ) -> list:
        """Handle IP address constraint for VNet grouping"""
        errors = []
        constraint_vnet_type = context.config.get("vnet_type")
        column = context.config.get("column")
        required = context.config.get("required", False)
        unique_per_group = context.config.get("unique_per_group", False)

        if (
            context.vnet_type != constraint_vnet_type
            or column not in context.group.columns
        ):
            return errors

        # Validate IPv4 format for non-null values (accepts both plain IP and CIDR notation)
        for idx, value in context.group[column].items():
            if pd.notna(value):
                try:
                    s = str(value).strip()
                    # Use ip_interface to accept both '192.168.1.1' and '192.168.1.1/24'
                    ip_obj = ipaddress.ip_interface(s)
                    # Update with canonical representation (preserves CIDR if present)
                    context.group.loc[idx, column] = str(ip_obj)
                except ValueError:
                    excel_row = context.group.loc[idx, "_excel_row"]
                    errors.append(
                        f"VNet {context.vnet_id} (row {excel_row}) column '{column}' has invalid IPv4 address or CIDR: '{value}'"
                    )

        if required:
            missing_mask = context.group[column].isna()
            if missing_mask.any():
                missing_rows = context.group.loc[missing_mask, "_excel_row"].tolist()
                errors.append(
                    f"VNet {context.vnet_id} (rows {missing_rows}) is {constraint_vnet_type} but column '{column}' is missing"
                )

        if unique_per_group:
            non_null_vals = context.group.loc[~context.group[column].isna(), column]
            dupes = non_null_vals[non_null_vals.duplicated()]
            if not dupes.empty:
                dup_rows = context.group.loc[
                    context.group[column].isin(dupes) & ~context.group[column].isna(),
                    "_excel_row",
                ].tolist()
                errors.append(
                    f"VNet {context.vnet_id} (rows {dup_rows}) has duplicate {column} values: {dupes.tolist()}"
                )

        return errors
