#!/usr/bin/env python3
"""
XLS to EDA YAML Converter

Converts Excel topology files to EDA-compliant YAML resource files.
"""

from __future__ import annotations

import logging
import shutil
import sys
from pathlib import Path

import pandas as pd

# Ensure src and repo root are on path (utils, xls_parser, and root paths)
_SCRIPT_DIR = Path(__file__).resolve().parent
_SRC_PATH = _SCRIPT_DIR.parent
_ROOT = _SRC_PATH.parent
for _p in (_SRC_PATH, _ROOT):
    if str(_p) not in sys.path:
        sys.path.insert(0, str(_p))

from xls_parser.src.topology_loader import TopologyLoader
from xls_parser.src.validators.syntactic_validator import SyntacticValidator
from xls_parser.src.validators.vnet_validator import VNETValidator
from xls_parser.src.validators.isl_validator import ISLValidator
from xls_parser.src.validators.edge_interface_validator import EdgeInterfaceValidator
from xls_parser.src.validators.toponode_validator import ToponodesValidator
from xls_parser.src.validators.fabric_validator import FabricValidator
from xls_parser.src.validators.interface_speed_hw_validator import validate_interface_speeds_by_hw
from xls_parser.src.xls_parsers import (
    ISLInterfaceParser,
    EdgeInterfaceParser,
    FabricParser,
    TopoLinkParser,
    ToponodesParser,
    VirtualNetworkParser,
)
from utils.json_handler import write_resources
from utils.constants import SUBSTEP_INDENT
from utils.boxes import format_box
from xls_parser.src.clab_generator import generate_clab_topology

logger = logging.getLogger(__name__)


def validate_sections(sections, schema):
    """Run all validations on loaded sections

    Args:
        sections: Dictionary of loaded DataFrame sections
        schema: Schema dictionary

    Returns:
        List of error messages (empty if validation passed)
    """
    errors = []

    # Toponodes validation
    toponodes_schema = schema["resources"]["Toponodes"]
    toponode_validator = ToponodesValidator(toponodes_schema=toponodes_schema)
    errors.extend(toponode_validator.validate_toponodes(df=sections["Toponodes"]))

    # Virtual Networks validation
    vnet_schema = schema["resources"]["Virtual Networks"]
    vnet_validator = VNETValidator(vnet_schema=vnet_schema)
    errors.extend(
        vnet_validator.validate_virtual_networks(df=sections["Virtual Networks"])
    )

    # ISL/TopoLinks validation
    isl_schema = schema["resources"]["Interfaces (ISL)"]
    isl_validator = ISLValidator(isl_schema=isl_schema)
    errors.extend(isl_validator.validate_topolinks(df=sections["Interfaces (ISL)"]))

    # Edge interfaces validation
    edge_schema = schema["resources"]["Interfaces (ISL)"]
    edge_validator = EdgeInterfaceValidator(edge_schema=edge_schema)
    errors.extend(
        edge_validator.validate_edge_interfaces(df=sections["Interfaces (Edge)"])
    )

    # Interface speed vs HW type (ISL + Edge): D2L/D3L allowed speeds, mixed link = common only
    toponodes_df = sections.get("Toponodes")
    isl_df = sections.get("Interfaces (ISL)")
    edge_df = sections.get("Interfaces (Edge)")
    if toponodes_df is not None and (isl_df is not None or edge_df is not None):
        errors.extend(
            validate_interface_speeds_by_hw(
                toponodes_df=toponodes_df,
                isl_df=isl_df if isl_df is not None else pd.DataFrame(),
                edge_df=edge_df if edge_df is not None else pd.DataFrame(),
            )
        )

    # Fabric validation
    fabric_schema = schema["resources"]["Fabric"]
    fabric_validator = FabricValidator(fabric_schema=fabric_schema)
    errors.extend(fabric_validator.validate_fabrics(df=sections["Fabric"]))

    return errors


def parse_resources(sections, namespace, tier=None):
    """Parse DataFrame sections into resource objects

    Args:
        sections: Dictionary of loaded DataFrame sections
        namespace: Kubernetes namespace for resources
        tier: Tier string (e.g., "2-tier", "4-tier") for tier-specific logic

    Returns:
        Dictionary containing parsed resources by type
    """
    # Initialize parsers
    isl_parser = ISLInterfaceParser(namespace=namespace)
    edge_parser = EdgeInterfaceParser(namespace=namespace)
    fabric_parser = FabricParser(namespace=namespace, tier=tier)
    topolink_parser = TopoLinkParser(namespace=namespace)
    toponodes_parser = ToponodesParser(namespace=namespace)
    vnet_parser = VirtualNetworkParser(namespace=namespace)

    # Parse each section
    resources = {
        "isl_interfaces": isl_parser.parse(sections["Interfaces (ISL)"]),
        "edge_interfaces": edge_parser.parse(sections["Interfaces (Edge)"]),
        "fabrics": fabric_parser.parse(sections["Fabric"]),
        "topolinks": topolink_parser.parse(sections["Interfaces (ISL)"]),
        "toponodes": toponodes_parser.parse(sections["Toponodes"]),
        "vnets": vnet_parser.parse(sections["Virtual Networks"]),
    }

    return resources


def render_resources(resources, output_dir, schema):
    """Write parsed resource dicts to JSON files under output_dir."""
    write_resources("interfaces_isl", resources["isl_interfaces"], output_dir)
    write_resources("interfaces_edge", resources["edge_interfaces"], output_dir)
    write_resources("fabric", resources["fabrics"], output_dir)
    write_resources("topolink", resources["topolinks"], output_dir)
    write_resources("toponode", resources["toponodes"], output_dir)
    write_resources("virtualnetwork", resources["vnets"], output_dir)


def convert_topology(
    xls_path, output_dir, namespace=None
):
    """Main orchestration function for topology conversion

    Args:
        xls_path: Path to Excel topology file
        output_dir: Output directory for YAML files
        namespace: Kubernetes namespace (optional)

    Returns:
        0 on success, 1 on validation failure
    """
    
    # Load topology data
    logger.info("Loading topology data...")
    top_loader = TopologyLoader(
        xls_path=xls_path,
    )
    sections = top_loader.load()

    # Use provided namespace or auto-detected one
    if namespace is None:
        namespace = top_loader.namespace

    # Run syntactic validation
    logger.info("Running syntactic validation...")
    syn_validator = SyntacticValidator(
        sections=sections, resouce_columns=top_loader.resource_columns
    )
    sections = syn_validator.validate_semantics()

    # Run semantic validations
    logger.info("Running semantic validations...")
    errors = validate_sections(sections, top_loader.schema)

    # Check validation results
    if errors:
        lines = [
            f"VALIDATION FAILED - {len(errors)} error(s) found:",
            "",
        ] + [f"{i}. {e}" for i, e in enumerate(errors, 1)] + [
            "",
            "Please fix the errors above and try again.",
        ]
        print("\n" + format_box(lines))
        return 1

    print("\n" + format_box(["VALIDATION PASSED - No errors found!"]))

    # Parse sections into resource objects
    logger.info("Parsing resources...")
    resources = parse_resources(sections, namespace, tier=top_loader.tier)

    # Log parsing summary with substep indent
    logger.info("%sParsed %s ISL interfaces", SUBSTEP_INDENT, len(resources["isl_interfaces"]))
    logger.info("%sParsed %s Edge interfaces", SUBSTEP_INDENT, len(resources["edge_interfaces"]))
    logger.info("%sParsed %s Fabric(s)", SUBSTEP_INDENT, len(resources["fabrics"]))
    logger.info("%sParsed %s TopoLinks", SUBSTEP_INDENT, len(resources["topolinks"]))
    logger.info("%sParsed %s TopoNodes", SUBSTEP_INDENT, len(resources["toponodes"]))
    logger.info("%sParsed %s VirtualNetworks", SUBSTEP_INDENT, len(resources["vnets"]))

    # Output directly under data/stage/
    base_dir = Path(output_dir)
    resources_dir = base_dir / "resources"
    clab_dir = base_dir / "clab"

    # Write JSON files to resources subdirectory
    logger.info("Writing JSON files...")
    render_resources(resources, str(resources_dir), top_loader.schema)

    # Generate ContainerLab topology to clab subdirectory
    # Name: topology-<xls filename without extension>
    topology_name = f"topology-{Path(xls_path).stem}"
    logger.info("Generating ContainerLab topology...")
    clab_dir.mkdir(parents=True, exist_ok=True)
    clab_output_path = clab_dir / "topology.clab.yaml"
    try:
        generate_clab_topology(
            toponodes_df=sections["Toponodes"],
            isl_df=sections["Interfaces (ISL)"],
            output_path=str(clab_output_path),
            topology_name=topology_name,
            tier=top_loader.tier,
            edge_df=sections.get("Interfaces (Edge)"),
        )
    except ValueError as e:
        logger.error("ContainerLab topology error: %s", e)
        print("\n" + format_box([f"ContainerLab topology error: {e}", "", "Fix the issue and re-run parse."]))
        return 1
    logger.info("%sContainerLab topology saved to %s", SUBSTEP_INDENT, clab_output_path)

    success_lines = [
        f"SUCCESS - JSON resources generated in '{base_dir}/' directory",
        f"  Resources: {resources_dir}/",
        f"  ContainerLab: {clab_dir}/",
    ]
    print("\n" + format_box(success_lines))

    return 0


def run_convert_xls_to_eda(
    xls_path: str | Path,
    namespace: str | None = None,
    *,
    output_dir: str | Path,
) -> int:
    """
    Entry point for converting an Excel topology file to EDA YAML resources.

    Validates the XLS path, clears the stage directory, then runs the full
    conversion (load, validate, parse, render, generate ContainerLab topology).
    Output is written to output_dir (stage dir passed from caller).

    Args:
        xls_path: Path to the Excel topology file.
        namespace: Optional Kubernetes namespace (default: auto-detected from schema).
        output_dir: Output directory (required; stage dir passed from caller).

    Returns:
        0 on success, 1 on validation failure or missing file.
    """
    xls_path = Path(xls_path)
    if not xls_path.exists():
        logger.error("Excel file not found: %s", xls_path)
        return 1

    _output_dir = Path(output_dir).resolve()

    if _output_dir.exists():
        for child in _output_dir.iterdir():
            shutil.rmtree(child) if child.is_dir() else child.unlink()
    _output_dir.mkdir(parents=True, exist_ok=True)

    return convert_topology(
        xls_path=str(xls_path),
        output_dir=str(_output_dir),
        namespace=namespace,
    )
