#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
#/**
#* Digital Voice Modem - Host Configuration Generator
#* GPLv2 Open Source. Use is subject to license terms.
#* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#*
#*/
#/*
#*   Copyright (C) 2026 by Bryan Biedenkapp N2PLL
#*
#*   This program is free software; you can redistribute it and/or modify
#*   it under the terms of the GNU General Public License as published by
#*   the Free Software Foundation; either version 2 of the License, or
#*   (at your option) any later version.
#*
#*   This program is distributed in the hope that it will be useful,
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#*   GNU General Public License for more details.
#*
#*   You should have received a copy of the GNU General Public License
#*   along with this program; if not, write to the Free Software
#*   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#*/
"""
dvmcfggen - DVMHost Configuration Generator
Core module for managing DVMHost configuration files
"""

import yaml
import copy
from pathlib import Path
from typing import Dict, Any, Optional, List
import re


class HexInt(int):
    """Marker type for integers that should be emitted in hexadecimal."""


class DVMConfigDumper(yaml.SafeDumper):
    """Custom YAML dumper with consistent list indentation and NAC hex output."""

    def increase_indent(self, flow=False, indentless=False):
        # Force sequence items to be indented under mapping keys.
        return super().increase_indent(flow, False)


def _represent_hex_int(dumper, data):
    """Represent marked integers as YAML int scalars in hex format."""
    hex_value = f'{int(data):X}'
    # YAML only accepts decimal/octal/0x-prefixed ints; bare A-F text must be a string.
    if any(ch in 'ABCDEF' for ch in hex_value):
        return dumper.represent_scalar('tag:yaml.org,2002:str', hex_value)
    return dumper.represent_scalar('tag:yaml.org,2002:int', hex_value)


DVMConfigDumper.add_representer(HexInt, _represent_hex_int)


def _mark_hex_fields(value: Any, key: Optional[str] = None) -> Any:
    """Recursively mark NAC-like fields for hexadecimal YAML serialization."""
    hex_keys = {
        'nac',
        'txNAC',
        'p25NAC',
        'netId',
        'p25NetId',
        'sysId',
        'rfssId',
        'siteId',
        'dmrNetId',
        'channelNo',
        'rxChannelNo',
    }

    if isinstance(value, dict):
        return {k: _mark_hex_fields(v, k) for k, v in value.items()}
    if isinstance(value, list):
        return [_mark_hex_fields(item) for item in value]
    if key in hex_keys and isinstance(value, int):
        return HexInt(value)
    return value


class ConfigValidator:
    """Validates DVMHost configuration values"""
    
    @staticmethod
    def validate_ip_address(ip: str) -> bool:
        """Validate IP address format"""
        if ip in ['0.0.0.0', 'localhost']:
            return True
        pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
        if not re.match(pattern, ip):
            return False
        parts = ip.split('.')
        return all(0 <= int(part) <= 255 for part in parts)
    
    @staticmethod
    def validate_port(port: int) -> bool:
        """Validate port number"""
        return 1 <= port <= 65535
    
    @staticmethod
    def validate_hex_key(key: str, required_length: int) -> bool:
        """Validate hexadecimal key"""
        if len(key) != required_length:
            return False
        return all(c in '0123456789ABCDEFabcdef' for c in key)
    
    @staticmethod
    def validate_range(value: int, min_val: int, max_val: int) -> bool:
        """Validate value is within range"""
        return min_val <= value <= max_val
    
    @staticmethod
    def validate_identity(identity: str) -> bool:
        """Validate system identity format"""
        return len(identity) > 0 and len(identity) <= 32
    
    @staticmethod
    def validate_callsign(callsign: str) -> bool:
        """Validate callsign format"""
        return len(callsign) > 0 and len(callsign) <= 16


class DVMConfig:
    """DVMHost configuration manager"""
    
    def __init__(self, config_path: Optional[Path] = None):
        """
        Initialize configuration manager
        
        Args:
            config_path: Path to existing config file to load
        """
        self.config_path = config_path
        self.config: Dict[str, Any] = {}
        self.validator = ConfigValidator()
        
        if config_path and config_path.exists():
            self.load(config_path)
    
    def load(self, config_path: Path) -> None:
        """Load configuration from YAML file"""
        with open(config_path, 'r') as f:
            self.config = yaml.safe_load(f)
        self.config_path = config_path
    
    def save(self, output_path: Optional[Path] = None) -> None:
        """Save configuration to YAML file"""
        path = output_path or self.config_path
        if not path:
            raise ValueError("No output path specified")
        
        # Create backup if file exists
        if path.exists():
            backup_path = path.with_suffix(path.suffix + '.bak')
            path.rename(backup_path)
        
        config_to_write = _mark_hex_fields(self.config)

        with open(path, 'w') as f:
            yaml.dump(
                config_to_write,
                f,
                Dumper=DVMConfigDumper,
                default_flow_style=False,
                sort_keys=False,
            )
    
    def get(self, key_path: str, default: Any = None) -> Any:
        """
        Get configuration value using dot notation
        
        Args:
            key_path: Path to value (e.g., 'network.id')
            default: Default value if not found
        """
        keys = key_path.split('.')
        value = self.config
        
        for key in keys:
            if isinstance(value, dict) and key in value:
                value = value[key]
            else:
                return default
        
        return value
    
    def set(self, key_path: str, value: Any) -> None:
        """
        Set configuration value using dot notation
        
        Args:
            key_path: Path to value (e.g., 'network.id')
            value: Value to set
        """
        keys = key_path.split('.')
        config = self.config
        
        # Navigate to the parent dict
        for key in keys[:-1]:
            if key not in config:
                config[key] = {}
            config = config[key]
        
        # Set the value
        config[keys[-1]] = value
    
    def validate(self) -> List[str]:
        """
        Validate configuration
        
        Returns:
            List of error messages (empty if valid)
        """
        errors = []
        
        # Network validation
        if self.get('network.enable'):
            if not self.validator.validate_ip_address(self.get('network.address', '')):
                errors.append("Invalid network.address")
            
            if not self.validator.validate_port(self.get('network.port', 0)):
                errors.append("Invalid network.port")
            
            if self.get('network.encrypted'):
                psk = self.get('network.presharedKey', '')
                if not self.validator.validate_hex_key(psk, 64):
                    errors.append("Invalid network.presharedKey (must be 64 hex characters)")
        
        # RPC validation
        rpc_addr = self.get('network.rpcAddress', '')
        if rpc_addr and not self.validator.validate_ip_address(rpc_addr):
            errors.append("Invalid network.rpcAddress")
        
        rpc_port = self.get('network.rpcPort', 0)
        if rpc_port and not self.validator.validate_port(rpc_port):
            errors.append("Invalid network.rpcPort")
        
        # REST API validation
        if self.get('network.restEnable'):
            rest_addr = self.get('network.restAddress', '')
            if not self.validator.validate_ip_address(rest_addr):
                errors.append("Invalid network.restAddress")
            
            rest_port = self.get('network.restPort', 0)
            if not self.validator.validate_port(rest_port):
                errors.append("Invalid network.restPort")
        
        # System validation
        identity = self.get('system.identity', '')
        if not self.validator.validate_identity(identity):
            errors.append("Invalid system.identity")
        
        # Color code / NAC / RAN validation
        color_code = self.get('system.config.colorCode', 1)
        if not self.validator.validate_range(color_code, 0, 15):
            errors.append("Invalid system.config.colorCode (must be 0-15)")
        
        nac = self.get('system.config.nac', 0)
        if not self.validator.validate_range(nac, 0, 0xFFF):
            errors.append("Invalid system.config.nac (must be 0-4095)")
        
        ran = self.get('system.config.ran', 0)
        if not self.validator.validate_range(ran, 0, 63):
            errors.append("Invalid system.config.ran (must be 0-63)")
        
        # CW ID validation
        if self.get('system.cwId.enable'):
            callsign = self.get('system.cwId.callsign', '')
            if not self.validator.validate_callsign(callsign):
                errors.append("Invalid system.cwId.callsign")
        
        # Modem validation
        rx_level = self.get('system.modem.rxLevel', 0)
        if not self.validator.validate_range(rx_level, 0, 100):
            errors.append("Invalid system.modem.rxLevel (must be 0-100)")
        
        tx_level = self.get('system.modem.txLevel', 0)
        if not self.validator.validate_range(tx_level, 0, 100):
            errors.append("Invalid system.modem.txLevel (must be 0-100)")
        
        # LLA key validation
        lla_key = self.get('system.config.secure.key', '')
        if lla_key and not self.validator.validate_hex_key(lla_key, 32):
            errors.append("Invalid system.config.secure.key (must be 32 hex characters)")
        
        return errors
    
    def get_summary(self) -> Dict[str, Any]:
        """Get configuration summary"""
        return {
            'identity': self.get('system.identity', 'UNKNOWN'),
            'peer_id': self.get('network.id', 0),
            'fne_address': self.get('network.address', 'N/A'),
            'fne_port': self.get('network.port', 0),
            'rpc_password': self.get('network.rpcPassword', 'N/A'),
            'rest_enabled': self.get('network.restEnable', False),
            'rest_password': self.get('network.restPassword', 'N/A'),
            'protocols': {
                'dmr': self.get('protocols.dmr.enable', False),
                'p25': self.get('protocols.p25.enable', False),
                'nxdn': self.get('protocols.nxdn.enable', False),
            },
            'color_code': self.get('system.config.colorCode', 1),
            'nac': self.get('system.config.nac', 0x293),
            'site_id': self.get('system.config.siteId', 1),
            'is_control': self.get('protocols.p25.control.enable', False) or 
                         self.get('protocols.dmr.control.enable', False),
            'modem_type': self.get('system.modem.protocol.type', 'null'),
            'modem_mode': self.get('system.modem.protocol.mode', 'air'),
            'modem_port': self.get('system.modem.protocol.uart.port', 'N/A'),
            'mode': 'Duplex' if self.get('system.duplex', True) else 'Simplex',
            'channel_id': self.get('system.config.channelId', 0),
            'channel_no': self.get('system.config.channelNo', 0),
            'tx_frequency': self.get('system.config.txFrequency', 0),
            'rx_frequency': self.get('system.config.rxFrequency', 0),
        }


if __name__ == '__main__':
    # Quick test
    config = DVMConfig()
    config.set('system.identity', 'TEST001')
    config.set('network.id', 100001)
    print("Config manager initialized successfully")
