"""Modern configuration module using Pydantic for validation and type safety."""

import logging
import subprocess
import os
import time
import sys
from typing import List, Dict, Optional, Any
from pydantic import BaseModel, Field, field_validator
import yaml
from pathlib import Path
import datetime

LOGGER = logging.getLogger('sampacs')


class MysqlConfig(BaseModel):
    """MySQL database configuration."""
    host: str = Field(default="127.0.0.1")
    port: int = Field(default=3306)
    db: str = Field(default="pacs")
    user: str = Field(default="dbuser")
    passwd: str = Field(default="dbpassword")
    charset: str = Field(default="utf8")


class EquipmentInfo(BaseModel):
    """Информация об оборудовании."""
    commissioning_date: str = Field(
        description="Дата ввода в эксплуатацию"
    )
    warranty_date: Optional[str] = Field(
        default=None,
        description="Дата гарантийного обслуживания"
    )
    last_service_date: Optional[str] = Field(
        default=None,
        description="Дата последнего обслуживания"
    )
    note: str = Field(
        default="",
        description="Примечание"
    )


class ModalityConfig(BaseModel):
    """DICOM modality configuration."""
    name: str = Field(
        description="Уникальное название оборудования"
    )
    info: Optional[EquipmentInfo] = Field(
        default=None,
        description="Информация об оборудовании"
    )
    host: str = Field(
        default="",
        description="IP-адрес или имя хоста DICOM оборудования",
        examples=["192.168.1.100", "modality.example.com"]
    )
    port: int = Field(
        default=104,
        description="Порт для подключения по dicom протоколу к аппарату (по умолчанию: 104)"
    )
    aetitle: str = Field(
        description="DICOM Application Entity Title (AET) оборудования"
    )
    isSource: bool = Field(
        default=False,
        description="Является ли это оборудование источником исследований"
    )
    pacsaetitle: str = Field(
        default="",
        description="AET для замены оригинального AET при передаче исследований из этого оборудования в центральный архив"
    )
    encoding: str = Field(
        default="",
        description="Кодировка символов, используемая этим оборудованием"
    )
    wlEncoding: str = Field(
        default="",
        description="Целевая кодировка символов для ответов на запросы worklist"
    )
    translit: bool = Field(
        default=False,
        description="Включить транслитерацию для данных из этого оборудования"
    )
    translitLang: str = Field(
        default="ru",
        description="Код языка для обработки транслитерации"
    )
    uppercase: bool = Field(
        default=False,
        description="Преобразовать данные в верхний регистр в ответах на запросы worklist"
    )
    ignoreActionName: bool = Field(
        default=False,
        description="Исключить описание исследования из ответов на запросы worklist"
    )
    generateStudyUID: bool = Field(
        default=False,
        description="Генерировать Study Instance UID в ответах на запросы worklist"
    )
    modalityTag: str = Field(
        default="",
        description="Установить значение тега Modality в ответах на запросы worklist"
    )
    additionalTags: List[str] = Field(
        default_factory=list,
        description="Дополнительные теги для включения в ответы worklist. Формат: список объектов {tag: nn, vr: nn, value: nn}"
    )
    wlFullName: bool = Field(
        default=False,
        description="Включить полное имя пациента в ответы на запросы worklist"
    )
    wlShortName2: bool = Field(
        default=False,
        description="Включить имя пациента в сокращенном формате (одна строка без разделителей ^) в ответах worklist"
    )
    srTags: Dict[str, str] = Field(
        default_factory=dict,
        description="Сопоставление между разобранным содержимым структурированного отчета и именами полей МИС. Ключи генерируются из DICOM ContentSequence (например, 'Findings Finding'). Значения указывают целевые имена полей МИС для содержимого структурированного отчета, извлеченного из объектов DICOM SR. По умолчанию: пустой словарь."
    )
    saveTags: Dict[str, str] = Field(
        default_factory=dict,
        description="Прямое сопоставление между DICOM тегами и именами полей МИС. Ключи - идентификаторы DICOM тегов. Значения сохраняются напрямую из DICOM набора данных в указанные поля МИС без разбора структурированного содержимого. По умолчанию: пустой словарь."
    )
    createStepID: bool = Field(
        default=True,
        description="В настоящее время неиспользуемое поле"
    )
    fillEmptyStudyDate: bool = Field(
        default=False,
        description="Если StudyDate отсутствует в DICOM изображении, заполнить поле текущей датой"
    )
    autoroute: List[str] = Field(
        default_factory=list,
        description="Список AET для автоматической пересылки полученных DICOM исследований"
    )
    mergeByAccessionNumber: bool = Field(
        default=True,
        description="Включить объединение исследований по AccessionNumber: при получении новых изображений проверяет базу данных МИС на наличие существующих исследований с тем же AccessionNumber и заменяет StudyUID в новых изображениях на StudyUID из базы данных, позволяя PACS собирать изображения в одно исследование по AccessionNumber"
    )
    property_separator: str = Field(
        default="\n",
        description="Разделитель, используемый при добавлении новых значений к существующим значениям полей МИС. Если поле уже имеет значение, новое значение будет добавлено с использованием этого разделителя."
    )
    roundSRValues: int = Field(
        default=1,
        description="Количество знаков после запятой для округления числовых значений при импорте структурированных отчетов"
    )
    electronSR: bool = Field(
        default=False,
        description="Использовать схему структурированных отчетов компании Electron при импорте структурированных отчетов"
    )
    accessionWorkaround: str = Field(
        default="",
        description="Обходное решение для устройств, запрещающих редактирование поля AccessionNumber. При включении, ожидается, что поле PatientID содержит идентификатор пациента и номер исследования, разделенные дефисом, которые будут разделены и сохранены в соответствующие поля."
    )
    snilsAsId: bool = Field(
        default=False,
        description="При включении, сервис ожидает СНИЛС (российский номер социального страхования) в качестве идентификатора пациента"
    )



    @field_validator("property_separator", mode="before")
    @classmethod
    def replace_newline_escapes(cls, v):
        """Replace escaped newlines with actual newlines."""
        if isinstance(v, str):
            return v.replace("\\n", "\n")
        return v


class NodeConfig(BaseModel):
    """Node configuration for distributed systems."""
    title: str = Field(default="")
    code: str = Field(default="")
    address: str = Field(default="")


class KometaConfig(ModalityConfig):
    """Kometa-specific configuration extending ModalityConfig."""
    port: int = Field(default=2575)
    dicom_port: int = Field(default=11112)
    orgCode: str = Field(default="")


class Config(BaseModel):
    """Main configuration model for the PACS system."""
    mysql: MysqlConfig = Field(default_factory=MysqlConfig)
    kometa: Optional[KometaConfig] = Field(default=None)
    storageDir: str = Field(default="/storage")
    nodeCode: str = Field(default="")
    autoroute: List[str] = Field(default_factory=list)
    autorouteSelfNode: bool = Field(default=True)
    modalities: List[ModalityConfig] = Field(default_factory=list)
    nodes: Dict[str, NodeConfig] = Field(default_factory=dict)
    registrateOptions: List[str] = Field(
        default_factory=lambda: ["registrate", "updateClientInfo", "closeTicket"]
    )
    exportSystem: int = Field(default=0)
    pacs_aetitle: str = Field(default="SAMPACS")
    pacs_port: int = Field(default=104)
    require_called_aet: bool = Field(default=False)
    require_calling_aet: bool = Field(default=True)
    sendStartHour: int = Field(default=0)
    sendEndHour: int = Field(default=23)
    weblog_path: str = Field(default="/tmp/weblog")
    description_property_name: str = Field(
        default="Описание",
        description="Название свойства для описания исследования в ActionPropertyType"
    )
    conclusion_property_name: str = Field(
        default="Заключение",
        description="Название свойства для заключения по исследованию в ActionPropertyType"
    )

    @classmethod
    def from_file(cls, config_file: str = "config.yml") -> "Config":
        """Load configuration from YAML file."""
        config_path = Path(config_file)

        if not config_path.exists():
            return cls()

        try:
            with open(config_path, "r") as f:
                config_data = yaml.safe_load(f) or {}
        except Exception as e:
            raise ValueError(f"Failed to load config file {config_file}: {e}")

        # Handle nested configurations
        if "mysql" in config_data:
            config_data["mysql"] = config_data.pop("mysql")

        # Convert nodes list to dictionary
        if "nodes" in config_data and isinstance(config_data["nodes"], list):
            nodes_dict = {}
            for node_data in config_data["nodes"]:
                node = NodeConfig(**node_data)
                nodes_dict[node.code] = node
            config_data["nodes"] = nodes_dict

        # Convert modalities list
        if "modalities" in config_data and isinstance(config_data["modalities"], list):
            modalities_list = []
            for modality_data in config_data["modalities"]:
                modality = ModalityConfig(**modality_data)
                modalities_list.append(modality)
            config_data["modalities"] = modalities_list

        return cls(**config_data)

    def save(self, config_file: str = "config.yml") -> None:
        """Save configuration to YAML file."""
        config_path = Path(config_file)

        # Create backup of existing config file
        if config_path.exists():
            backup_path = config_path.with_suffix('.yml.backup')
            import shutil
            shutil.copy2(config_path, backup_path)

        # Convert configuration to dictionary (excluding default values) and save to YAML
        try:
            with open(config_path, "w") as f:
                config_dict = self.dict(exclude_defaults=True)

                # Convert nodes dictionary back to list for YAML serialization
                if "nodes" in config_dict and isinstance(config_dict["nodes"], dict):
                    nodes_list = []
                    for node_code, node_data in config_dict["nodes"].items():
                        node_dict = node_data.copy()
                        node_dict["code"] = node_code
                        nodes_list.append(node_dict)
                    config_dict["nodes"] = nodes_list

                # Replace newlines in property_separator with escaped version for YAML
                def replace_newlines(obj):
                    if isinstance(obj, dict):
                        for key, value in obj.items():
                            if key == "property_separator" and isinstance(value, str):
                                obj[key] = value.replace("\n", "\\n")
                            elif isinstance(value, (dict, list)):
                                replace_newlines(value)
                    elif isinstance(obj, list):
                        for item in obj:
                            if isinstance(item, (dict, list)):
                                replace_newlines(item)

                replace_newlines(config_dict)

                # Custom YAML dumper that adds empty lines between top-level entities and custom key order
                class CustomDumper(yaml.Dumper):
                    def write_line_break(self, data=None):
                        super().write_line_break(data)
                        if len(self.indents) == 1:  # Top level
                            super().write_line_break()

                    def represent_mapping(self, tag, mapping, flow_style=None):
                        # Define custom key order for top-level config
                        if len(self.indents) == 0:  # Root level
                            desired_order = [
                                'nodeCode', 'nodes', 'pacs_aetitle', 'mysql', 'exportSystem',
                                'storageDir', 'pacs_port',
                                'registrateOptions', 'autoroute',
                                'autorouteSelfNode', 'modalities', 'kometa'
                            ]
                            # Reorder keys according to desired order
                            ordered_items = []
                            for key in desired_order:
                                if key in mapping:
                                    ordered_items.append((key, mapping[key]))
                            # Add any remaining keys not in the desired order
                            for key, value in mapping.items():
                                if key not in [item[0] for item in ordered_items]:
                                    ordered_items.append((key, value))
                            return super().represent_mapping(tag, ordered_items, flow_style)
                        return super().represent_mapping(tag, mapping, flow_style)

                yaml.dump(config_dict, f, Dumper=CustomDumper, default_flow_style=False, allow_unicode=True, indent=2)
        except Exception as e:
            raise ValueError(f"Failed to save config file {config_file}: {e}")

        # Reload and validate the saved config
        try:
            reloaded_config = Config.from_file(config_file)
            warnings = reloaded_config.validate_config()
            if warnings:
                LOGGER.error(f"Warning: Configuration warnings after save: {warnings}")
        except Exception as e:
            LOGGER.error(f"Warning: Failed to validate saved config: {e}")

    def validate_config(self) -> List[str]:
        """Validate configuration and return list of warnings/errors."""
        warnings = []

        # Validate storage directory
        storage_path = Path(self.storageDir)
        if not storage_path.exists():
            warnings.append(f"Storage directory {self.storageDir} does not exist")

        # Validate modalities
        for i, modality in enumerate(self.modalities):
            if not modality.name:
                warnings.append(f"Modality at index {i} has no name")
            if not modality.aetitle:
                warnings.append(f"Modality at index {i} has no aetitle")

        return warnings

    def save_and_notify(self, config_file: str = "config.yml"):
        """Save config and notify other processes via SIGUSR1"""
        self.save(config_file)
        send_reload_signal()


def send_reload_signal():
    """Send SIGUSR1 to all processes via supervisorctl"""
    try:
        result = subprocess.run(
            ["supervisorctl", "signal", "USR1", "all"],
            capture_output=True,
            text=True,
            timeout=5
        )

        if result.returncode == 0:
            LOGGER.info("SIGUSR1 sent to all processes")
            return True
        else:
            LOGGER.warning(f"Failed to send SIGUSR1: {result.stderr}")
            return False

    except Exception as e:
        LOGGER.warning(f"Could not send SIGUSR1 via supervisorctl: {e}")
        return False


# Global configuration instance
config = Config.from_file()


if __name__ == "__main__":
    # Example usage and validation
    warnings = config.validate_config()
    if warnings:
        print("Configuration warnings:")
        for warning in warnings:
            print(f"  - {warning}")
    else:
        print("Configuration is valid!")
        print("\n" + "="*60)
        print("PACS Configuration Summary")
        print("="*60)
        print(f"Storage Directory: {config.storageDir}")
        print(f"Node Code: {config.nodeCode}")
        print(f"PACS AETitle: {config.pacs_aetitle}")
        print(f"PACS Port: {config.pacs_port}")
        print(f"Export System: {config.exportSystem}")
        print(f"Auto-route Self Node: {config.autorouteSelfNode}")
        print(f"Registration Options: {', '.join(config.registrateOptions)}")
        print(f"Web Log Path: {config.weblog_path}")
        print()

        print("Database Configuration")
        print("-" * 30)
        print(f"  Host: {config.mysql.host}")
        print(f"  Port: {config.mysql.port}")
        print(f"  Database: {config.mysql.db}")
        print(f"  Samson DB: {config.mysql.samsondb}")
        print(f"  User: {config.mysql.user}")
        print()

        print("Modalities Configuration")
        print("-" * 30)
        print(f"  Total modalities: {len(config.modalities)}")
        for i, modality in enumerate(config.modalities, 1):
            print(f"  {i}. {modality.name} ({modality.aetitle})")
            if modality.isSource:
                print(f"     Source modality -> {modality.pacsaetitle}")
            if modality.autoroute:
                print(f"     Auto-route to: {', '.join(modality.autoroute)}")
        print()

        print("Nodes Configuration")
        print("-" * 30)
        print(f"  Total nodes: {len(config.nodes)}")
        for code, node in config.nodes.items():
            print(f"  {node.title} ({code}) -> {node.address}")
        print()

        print("Configuration loaded successfully!")
        print("="*60 + "\n")
