"""Flash profile management for multi-target systems.

This module provides classes to manage flash profiles (targets) with
different SRIO endpoints, IP addresses, and flash configurations.

The structure follows a three-level hierarchy:
1. GlobalConfig: System-wide settings (IP, port, SRIO base, etc.)
2. FlashModel: Reusable FPGA model definitions (memory map, sectors, etc.)
3. FlashTarget: Specific targets referencing a model by ID

Reference: Vecchia_app FpgaFlashProfile system and targets.ini format.
"""
from __future__ import annotations

import configparser
import json
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import List, Optional, Dict


@dataclass
class GlobalConfig:
    """Global system configuration.
    
    Attributes:
        ip: Default IP address for SRIO communication.
        port: Default port for TFTP/SRIO communication.
        srio_base: SRIO base address (hex value).
        fpga_base: FPGA base address (hex value).
        fpga_sector: FPGA sector size (hex value).
        smart: Smart mode flag (0 or 1).
        section: Default flash section (e.g., "APP1").
        default_target: Default target ID to use.
    """
    
    ip: str = "192.168.2.102"
    port: int = 50069
    srio_base: int = 0x500000
    fpga_base: int = 0x0
    fpga_sector: int = 0x010000
    smart: int = 1
    section: str = "APP1"
    default_target: str = "AESA_RFIF"
    verify_after_write: bool = False  # Read-back verification after each chunk write
    
    # Retry configuration (at different levels)
    max_tftp_retries: int = 3           # TFTP transport level (SimpleTFTPClient)
    max_register_retries: int = 500     # Register write verification level (srio_flash.py)
    max_chunk_write_retries: int = 3    # Flash chunk write+verify level (core.py)

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @staticmethod
    def from_dict(data: dict) -> GlobalConfig:
        """Create GlobalConfig from dictionary."""
        return GlobalConfig(**data)


@dataclass
class FlashModel:
    """FPGA flash memory model definition.
    
    Attributes:
        id_model: Unique model identifier (e.g., 0, 1, 2).
        model: Model name (e.g., "xcku040", "rfif").
        description: Human-readable description.
        flash_type: Flash type identifier (0 or 1).
        is_4byte_addressing: True if 4-byte addressing, False for 3-byte.
        num_sectors: Total number of flash sectors.
        golden_start: Golden area start address.
        golden_stop: Golden area stop address.
        user_start: User area start address.
        user_stop: User area stop address.
        test_address: Optional test address for verification.
    """
    
    id_model: int
    model: str
    description: str
    flash_type: int
    is_4byte_addressing: bool
    num_sectors: int
    golden_start: int
    golden_stop: int
    user_start: int
    user_stop: int
    test_address: Optional[int] = None

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @staticmethod
    def from_dict(data: dict) -> FlashModel:
        """Create FlashModel from dictionary."""
        return FlashModel(**data)


@dataclass
class FlashTarget:
    """Individual flash target configuration.
    
    Attributes:
        id_target: Unique target identifier (e.g., "EIF_FPGA1").
        description: Human-readable description.
        slot_address: SRIO slot/endpoint address (hex value).
        architecture: Target architecture (e.g., "Xilinx", "RFIF").
        name: Target name (same as id_target usually).
        file_prefix: Prefix for firmware files.
        id_model: Reference to FlashModel ID.
        golden_binary_path: Optional path to golden area firmware binary.
        user_binary_path: Optional path to user area firmware binary.
    """
    
    id_target: str
    description: str
    slot_address: int
    architecture: str
    name: str
    file_prefix: str
    id_model: int
    golden_binary_path: Optional[str] = None
    user_binary_path: Optional[str] = None

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @staticmethod
    def from_dict(data: dict) -> FlashTarget:
        """Create FlashTarget from dictionary."""
        return FlashTarget(**data)


# Legacy class for backward compatibility
@dataclass
class FlashProfile:
    """Legacy flash target configuration (deprecated).
    
    This class is kept for backward compatibility with older code.
    New code should use FlashTarget + FlashModel instead.
    
    Attributes:
        name: Human-readable profile name (e.g., "Primary Flash", "Secondary Flash").
        slot_address: SRIO endpoint/slot address (hex string, e.g., "0x10").
        ip: Target IP address (e.g., "192.168.1.100").
        port: TFTP port on target (default 69).
        base_address: Flash base address (hex, e.g., 0x01000000).
        size: Total flash size in bytes (e.g., 16777216 for 16 MB).
        binary_path: Optional path to binary file associated with this profile.
    """
    
    name: str
    slot_address: str
    ip: str
    port: int = 69
    base_address: int = 0x01000000
    size: int = 16 * 1024 * 1024  # 16 MB default
    binary_path: Optional[str] = None

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @staticmethod
    def from_dict(data: dict) -> FlashProfile:
        """Create FlashProfile from dictionary."""
        return FlashProfile(**data)


class ProfileManager:
    """Manages flash targets, models, and global configuration.
    
    This manager handles the complete flash programming configuration including:
    - Global system settings (IP, port, SRIO addresses)
    - Flash models (FPGA types with memory maps)
    - Flash targets (specific FPGA instances)
    
    Supports both JSON and INI file formats for import/export.
    """

    def __init__(self, config_path: Optional[Path] = None) -> None:
        """Initialize ProfileManager.
        
        Args:
            config_path: Path to JSON configuration file. Defaults to "flash_profiles.json".
        """
        self.global_config = GlobalConfig()
        self.models: Dict[int, FlashModel] = {}
        self.targets: Dict[str, FlashTarget] = {}
        self.config_path = config_path or Path("flash_profiles.json")
        
        if self.config_path.exists():
            self.load()

    def add_model(self, model: FlashModel) -> None:
        """Add or update a flash model."""
        self.models[model.id_model] = model

    def add_target(self, target: FlashTarget) -> None:
        """Add or update a flash target."""
        self.targets[target.id_target] = target

    def get_model(self, id_model: int) -> Optional[FlashModel]:
        """Get model by ID."""
        return self.models.get(id_model)

    def get_target(self, id_target: str) -> Optional[FlashTarget]:
        """Get target by ID."""
        return self.targets.get(id_target)

    def get_target_with_model(self, id_target: str) -> Optional[tuple[FlashTarget, FlashModel]]:
        """Get target together with its associated model.
        
        Args:
            id_target: Target ID to retrieve.
            
        Returns:
            Tuple of (FlashTarget, FlashModel) if found, None otherwise.
        """
        target = self.get_target(id_target)
        if not target:
            return None
        model = self.get_model(target.id_model)
        if not model:
            return None
        return (target, model)

    def delete_model(self, id_model: int) -> bool:
        """Delete model by ID. Returns True if found and deleted."""
        if id_model in self.models:
            del self.models[id_model]
            return True
        return False

    def delete_target(self, id_target: str) -> bool:
        """Delete target by ID. Returns True if found and deleted."""
        if id_target in self.targets:
            del self.targets[id_target]
            return True
        return False

    def list_models(self) -> List[int]:
        """Return list of model IDs."""
        return list(self.models.keys())

    def list_targets(self) -> List[str]:
        """Return list of target IDs."""
        return list(self.targets.keys())

    def save(self) -> None:
        """Save configuration to JSON file."""
        data = {
            "global_config": self.global_config.to_dict(),
            "models": {k: v.to_dict() for k, v in self.models.items()},
            "targets": {k: v.to_dict() for k, v in self.targets.items()},
        }
        with open(self.config_path, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)

    def load(self) -> None:
        """Load configuration from JSON file."""
        with open(self.config_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        
        # Load global config
        if "global_config" in data:
            self.global_config = GlobalConfig.from_dict(data["global_config"])
        
        # Load models
        if "models" in data:
            self.models = {
                int(k): FlashModel.from_dict(v) 
                for k, v in data["models"].items()
            }
        
        # Load targets
        if "targets" in data:
            self.targets = {
                k: FlashTarget.from_dict(v) 
                for k, v in data["targets"].items()
            }

    def load_from_ini(self, ini_path: Path) -> None:
        """Load configuration from INI file (targets.ini format).
        
        Args:
            ini_path: Path to INI configuration file.
        """
        parser = configparser.ConfigParser()
        parser.read(ini_path, encoding="utf-8")
        
        # Parse global config from [default] section
        if "default" in parser:
            section = parser["default"]
            self.global_config.ip = section.get("ip", self.global_config.ip).strip('"')
            self.global_config.port = section.getint("port", self.global_config.port)
            self.global_config.smart = section.getint("smart", self.global_config.smart)
            self.global_config.section = section.get("section", self.global_config.section)
            
            # Parse hex values
            if "srio_base" in section:
                self.global_config.srio_base = int(section["srio_base"].replace("$", ""), 16)
            if "fpga_base" in section:
                self.global_config.fpga_base = int(section["fpga_base"].replace("$", ""), 16)
            if "fpga_sector" in section:
                self.global_config.fpga_sector = int(section["fpga_sector"].replace("$", ""), 16)
            if "defTarget" in section:
                self.global_config.default_target = section["defTarget"]
        
        # Parse models from [MODEL-x] sections
        for section_name in parser.sections():
            if section_name.startswith("MODEL-"):
                section = parser[section_name]
                id_model = section.getint("idModel")
                
                model = FlashModel(
                    id_model=id_model,
                    model=section.get("model", ""),
                    description=section.get("description", ""),
                    flash_type=section.getint("type", 0),
                    is_4byte_addressing=bool(section.getint("3_4Byte", 0)),
                    num_sectors=section.getint("numSector", 0),
                    golden_start=int(section.get("goldenAddressStartArea", "0x0"), 16),
                    golden_stop=int(section.get("goldenAddressStopArea", "0x0"), 16),
                    user_start=int(section.get("userAddressStartArea", "0x0"), 16),
                    user_stop=int(section.get("userAddresStopArea", "0x0"), 16),
                )
                
                if "testAddress" in section:
                    model.test_address = int(section["testAddress"], 16)
                
                self.add_model(model)
        
        # Parse targets from [TGT-x] sections
        for section_name in parser.sections():
            if section_name.startswith("TGT-"):
                section = parser[section_name]
                id_target = section.get("idTgt", "")
                
                target = FlashTarget(
                    id_target=id_target,
                    description=section.get("description", ""),
                    slot_address=int(section.get("slotAddress", "0x0"), 16),
                    architecture=section.get("arch", ""),
                    name=section.get("name", id_target),
                    file_prefix=section.get("filePrefix", id_target),
                    id_model=section.getint("idModel", 0),
                )
                
                self.add_target(target)

    def export_to_ini(self, ini_path: Path) -> None:
        """Export configuration to INI file (targets.ini format).
        
        Args:
            ini_path: Path where to save INI file.
        """
        parser = configparser.ConfigParser()
        
        # Write [default] section
        parser["default"] = {
            "port": str(self.global_config.port),
            "ip": f'"{self.global_config.ip}"',
            "smart": str(self.global_config.smart),
            "section": self.global_config.section,
            "srio_base": f"$0x{self.global_config.srio_base:X}",
            "fpga_base": f"$0x{self.global_config.fpga_base:X}",
            "fpga_sector": f"0x{self.global_config.fpga_sector:06X}",
            "defTarget": self.global_config.default_target,
        }
        
        # Write [MODEL-x] sections
        for id_model, model in sorted(self.models.items()):
            section_name = f"MODEL-{id_model}"
            parser[section_name] = {
                "idModel": str(model.id_model),
                "model": model.model,
                "description": model.description,
                "type": str(model.flash_type),
                "3_4Byte": "1" if model.is_4byte_addressing else "0",
                "numSector": str(model.num_sectors),
                "goldenAddressStartArea": f"0x{model.golden_start:08X}",
                "goldenAddressStopArea": f"0x{model.golden_stop:08X}",
                "userAddressStartArea": f"0x{model.user_start:08X}",
                "userAddresStopArea": f"0x{model.user_stop:08X}",
            }
            if model.test_address is not None:
                parser[section_name]["testAddress"] = f"0x{model.test_address:08X}"
        
        # Write [TGT-x] sections
        for idx, (id_target, target) in enumerate(sorted(self.targets.items())):
            section_name = f"TGT-{idx}"
            parser[section_name] = {
                "idTgt": target.id_target,
                "description": target.description,
                "slotAddress": f"0x{target.slot_address:02X}",
                "arch": target.architecture,
                "name": target.name,
                "filePrefix": target.file_prefix,
                "idModel": str(target.id_model),
            }
        
        with open(ini_path, "w", encoding="utf-8") as f:
            parser.write(f)


def create_default_profiles() -> List[FlashProfile]:
    """Create sample legacy flash profiles for testing (deprecated).
    
    This function is kept for backward compatibility.
    Use ProfileManager.load_from_ini() for new code.
    """
    return [
        FlashProfile(
            name="Primary Flash",
            slot_address="0x10",
            ip="192.168.1.100",
            port=69,
            base_address=0x01000000,
            size=16 * 1024 * 1024,
        ),
        FlashProfile(
            name="Secondary Flash",
            slot_address="0x11",
            ip="192.168.1.100",
            port=69,
            base_address=0x02000000,
            size=16 * 1024 * 1024,
        ),
        FlashProfile(
            name="Localhost Emulator",
            slot_address="0xFF",
            ip="127.0.0.1",
            port=6969,
            base_address=0x00000000,
            size=16 * 1024 * 1024,
        ),
    ]


def initialize_from_targets_ini(
    ini_path: Path = Path("_OLD/Vecchia_app/FpgaBeamMeUp/targets.ini"),
    json_path: Path = Path("flash_profiles.json")
) -> ProfileManager:
    """Initialize ProfileManager from legacy targets.ini file.
    
    This function reads the original targets.ini and creates a new JSON
    configuration file with all models and targets properly structured.
    
    Args:
        ini_path: Path to targets.ini file.
        json_path: Path where to save JSON configuration.
        
    Returns:
        Initialized ProfileManager with all data loaded.
    """
    manager = ProfileManager(config_path=json_path)
    manager.load_from_ini(ini_path)
    manager.save()
    return manager
