# -*- coding: utf-8 -*-
"""
Biblium Configuration System

Provides centralized configuration management with sensible defaults
that can be overridden at initialization, runtime, or via GUI.

Configuration priority (highest to lowest):
1. Explicit parameter in function call
2. Instance attribute (set at initialization)
3. Runtime config (config.set())
4. Config file (biblium_config.yaml)
5. Built-in defaults

@author: Claude (Anthropic) for Lan.Umek
"""

from __future__ import annotations

import os
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from dataclasses import dataclass, field, asdict
import warnings


# =============================================================================
# DEFAULT CONFIGURATION
# =============================================================================

@dataclass
class BibliumConfig:
    """
    Central configuration for Biblium.
    
    All settings have sensible defaults that work out of the box.
    """
    
    # --- Color Settings ---
    color_scheme: str = "primary"  # "primary", "categorical", "colorblind", "custom"
    custom_colors: List[str] = field(default_factory=list)
    sequential_cmap: str = "YlOrRd"
    diverging_cmap: str = "RdYlBu"
    
    # --- Database Defaults ---
    default_database: str = "scopus"
    
    # --- Separators by Database ---
    separators: Dict[str, str] = field(default_factory=lambda: {
        "scopus": "; ",
        "open alex": "|",
        "oa": "|",
        "wos": "; ",
        "pubmed": "; ",
        "dimensions": "; ",
        "lens": "; ",
        "ieee": "; ",
        "dblp": "; ",
        "ris": "; ",
        "bibtex": " and ",
        "default": "; ",
    })
    
    # --- Figure Settings ---
    figure_dpi: int = 300
    figure_format: str = "png"  # "png", "svg", "pdf"
    figure_size: tuple = (10, 6)
    show_grid: bool = False
    
    # --- Plot Style ---
    font_family: str = "sans-serif"
    font_size: int = 10
    title_size: int = 14
    label_size: int = 12
    
    # --- Processing Settings ---
    default_chunk_size: int = 10000
    show_progress: bool = True
    verbose: bool = True


@dataclass
class PlotConfig:
    """
    Global plot configuration with sensible defaults.
    
    This configuration affects all plots generated by Biblium unless
    explicitly overridden in the function call.
    
    Examples
    --------
    >>> from biblium.config import plot_config, set_plot_config
    >>> 
    >>> # View current settings
    >>> print(plot_config.show_grid)
    False
    >>> 
    >>> # Change settings
    >>> plot_config.dpi = 600
    >>> plot_config.default_color = "steelblue"
    """
    
    # --- Resolution and Size ---
    dpi: int = 600
    figure_size: tuple = (10, 6)
    save_formats: List[str] = field(default_factory=lambda: ["png", "svg", "pdf"])
    
    # --- Grid and Axes ---
    show_grid: bool = False
    grid_alpha: float = 0.3
    grid_style: str = "--"
    
    # --- Colors ---
    default_color: str = "lightblue"
    cmap: str = "viridis"
    categorical_palette: List[str] = field(default_factory=lambda: [
        "#3498db", "#e74c3c", "#2ecc71", "#f39c12", "#9b59b6",
        "#1abc9c", "#e67e22", "#34495e", "#95a5a6", "#d35400"
    ])
    
    # --- Fonts ---
    font_family: str = "sans-serif"
    title_fontsize: int = 14
    label_fontsize: int = 12
    tick_fontsize: int = 10
    legend_fontsize: int = 10
    
    # --- Labels ---
    clean_underscore: bool = True  # Replace underscores with spaces
    max_label_length: int = 50  # For references, affiliations
    label_wrap_width: int = 40  # Characters before wrapping
    
    # --- Bar Charts ---
    bar_value_labels: bool = True
    bar_value_fontsize: int = 8
    
    # --- Network Plots ---
    node_size_range: tuple = (100, 2000)
    edge_alpha: float = 0.5
    
    def apply_to_matplotlib(self) -> None:
        """Apply these settings to matplotlib rcParams."""
        import matplotlib.pyplot as plt
        
        plt.rcParams['figure.dpi'] = self.dpi
        plt.rcParams['figure.figsize'] = self.figure_size
        plt.rcParams['font.family'] = self.font_family
        plt.rcParams['font.size'] = self.tick_fontsize
        plt.rcParams['axes.titlesize'] = self.title_fontsize
        plt.rcParams['axes.labelsize'] = self.label_fontsize
        plt.rcParams['xtick.labelsize'] = self.tick_fontsize
        plt.rcParams['ytick.labelsize'] = self.tick_fontsize
        plt.rcParams['legend.fontsize'] = self.legend_fontsize
        plt.rcParams['axes.grid'] = self.show_grid
        plt.rcParams['grid.alpha'] = self.grid_alpha
        plt.rcParams['grid.linestyle'] = self.grid_style

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "PlotConfig":
        """Create from dictionary."""
        valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in data.items() if k in valid_fields}
        return cls(**filtered)


@dataclass
class ReportConfig:
    """
    Configuration for report generation.
    
    Examples
    --------
    >>> from biblium.config import report_config
    >>> 
    >>> # Change default report level
    >>> report_config.default_level = "standard"
    """
    
    # --- Defaults ---
    default_level: str = "basic"  # "basic", "standard", "extended", "full"
    default_formats: List[str] = field(default_factory=lambda: ["xlsx", "docx"])
    
    # --- Table Settings ---
    top_n_tables: int = 20  # Default rows for tables in reports
    round_decimals: int = 3
    
    # --- Styling ---
    modern_style: bool = True
    zebra_tables: bool = True
    include_cover: bool = True
    include_toc: bool = True
    enumerate_sections: bool = True
    enumerate_tables: bool = True
    enumerate_figures: bool = True
    
    # --- Title and Metadata ---
    default_title: str = "Bibliometric Analysis Report"
    
    # --- Colors ---
    heading_color: str = "primary"  # "primary", "blue", "green", etc.
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return asdict(self)


# Global instances
plot_config: PlotConfig = PlotConfig()
report_config: ReportConfig = ReportConfig()


def get_plot_config() -> PlotConfig:
    """Get the current plot configuration."""
    return plot_config


def set_plot_config(new_config: PlotConfig) -> None:
    """Set the plot configuration."""
    global plot_config
    plot_config = new_config


def get_report_config() -> ReportConfig:
    """Get the current report configuration."""
    return report_config


def set_report_config(new_config: ReportConfig) -> None:
    """Set the report configuration."""
    global report_config
    report_config = new_config


def apply_plot_style() -> None:
    """Apply the current plot configuration to matplotlib."""
    plot_config.apply_to_matplotlib()
    
    # --- Paths ---
    output_folder: str = "results"
    temp_folder: str = "tmp"
    
    # --- SDG Settings ---
    sdg_method: str = "regex"  # "regex", "ml"
    
    # --- Analysis Defaults ---
    top_n: int = 20
    min_frequency: int = 2
    
    def get_separator(self, db: str) -> str:
        """Get separator for a database."""
        db_lower = db.lower().strip()
        return self.separators.get(db_lower, self.separators.get("default", "; "))
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert config to dictionary."""
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "BibliumConfig":
        """Create config from dictionary."""
        # Filter only valid fields
        valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in data.items() if k in valid_fields}
        return cls(**filtered)


# =============================================================================
# GLOBAL CONFIG INSTANCE
# =============================================================================

# The global config instance - this is what gets used throughout the package
_config: BibliumConfig = BibliumConfig()


def get_config() -> BibliumConfig:
    """Get the current configuration."""
    return _config


def set_config(new_config: BibliumConfig) -> None:
    """Replace the entire configuration."""
    global _config
    _config = new_config


def reset_config() -> None:
    """Reset configuration to defaults."""
    global _config
    _config = BibliumConfig()


# =============================================================================
# CONVENIENT GETTERS AND SETTERS
# =============================================================================

def get(key: str, default: Any = None) -> Any:
    """
    Get a configuration value.
    
    Parameters
    ----------
    key : str
        Configuration key (e.g., "color_scheme", "figure_dpi").
    default : Any
        Default value if key not found.
    
    Returns
    -------
    Any
        Configuration value.
    
    Example
    -------
    >>> from biblium import config
    >>> config.get("color_scheme")
    'primary'
    >>> config.get("nonexistent", "fallback")
    'fallback'
    """
    return getattr(_config, key, default)


def set(key: str, value: Any) -> None:
    """
    Set a configuration value.
    
    Parameters
    ----------
    key : str
        Configuration key.
    value : Any
        New value.
    
    Example
    -------
    >>> from biblium import config
    >>> config.set("color_scheme", "colorblind")
    >>> config.set("figure_dpi", 600)
    """
    if hasattr(_config, key):
        setattr(_config, key, value)
    else:
        warnings.warn(f"Unknown config key: {key}")


def get_separator(db: str = None) -> str:
    """
    Get separator for a database.
    
    Parameters
    ----------
    db : str
        Database name. If None, uses default_database.
    
    Returns
    -------
    str
        Separator string.
    """
    if db is None:
        db = _config.default_database
    return _config.get_separator(db)


def get_colors(n: int = None) -> List[str]:
    """
    Get colors based on current color scheme.
    
    Parameters
    ----------
    n : int
        Number of colors needed. If None, returns all.
    
    Returns
    -------
    list
        List of hex color codes.
    """
    from biblium.colors import (
        PRIMARY_PALETTE, CATEGORICAL_PALETTE, COLORBLIND_PALETTE
    )
    
    scheme = _config.color_scheme
    
    if scheme == "primary":
        colors = PRIMARY_PALETTE
    elif scheme == "categorical":
        colors = CATEGORICAL_PALETTE
    elif scheme == "colorblind":
        colors = COLORBLIND_PALETTE
    elif scheme == "custom" and _config.custom_colors:
        colors = _config.custom_colors
    else:
        colors = PRIMARY_PALETTE
    
    if n is None:
        return list(colors)
    elif n <= len(colors):
        return list(colors[:n])
    else:
        # Cycle colors if needed
        return [colors[i % len(colors)] for i in range(n)]


# =============================================================================
# CONFIG FILE I/O
# =============================================================================

def get_config_path() -> Path:
    """Get default config file path."""
    # Check multiple locations in order
    locations = [
        Path.cwd() / "biblium_config.json",
        Path.cwd() / ".biblium" / "config.json",
        Path.home() / ".biblium" / "config.json",
    ]
    
    for loc in locations:
        if loc.exists():
            return loc
    
    # Default to current directory
    return Path.cwd() / "biblium_config.json"


def save_config(filepath: str = None) -> str:
    """
    Save current configuration to file.
    
    Parameters
    ----------
    filepath : str
        Output path. If None, uses default location.
    
    Returns
    -------
    str
        Path where config was saved.
    
    Example
    -------
    >>> from biblium import config
    >>> config.set("color_scheme", "colorblind")
    >>> config.save_config()
    'biblium_config.json'
    """
    if filepath is None:
        filepath = get_config_path()
    
    filepath = Path(filepath)
    filepath.parent.mkdir(parents=True, exist_ok=True)
    
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(_config.to_dict(), f, indent=2, default=str)
    
    return str(filepath)


def load_config(filepath: str = None) -> BibliumConfig:
    """
    Load configuration from file.
    
    Parameters
    ----------
    filepath : str
        Config file path. If None, searches default locations.
    
    Returns
    -------
    BibliumConfig
        Loaded configuration (also sets as current).
    
    Example
    -------
    >>> from biblium import config
    >>> config.load_config("my_config.json")
    """
    global _config
    
    if filepath is None:
        filepath = get_config_path()
    
    filepath = Path(filepath)
    
    if not filepath.exists():
        warnings.warn(f"Config file not found: {filepath}")
        return _config
    
    with open(filepath, 'r', encoding='utf-8') as f:
        data = json.load(f)
    
    _config = BibliumConfig.from_dict(data)
    return _config


def load_config_if_exists() -> bool:
    """
    Load config from default location if it exists.
    
    Returns
    -------
    bool
        True if config was loaded, False otherwise.
    """
    config_path = get_config_path()
    if config_path.exists():
        load_config(str(config_path))
        return True
    return False


# =============================================================================
# CONTEXT MANAGER FOR TEMPORARY CONFIG CHANGES
# =============================================================================

class config_context:
    """
    Context manager for temporary configuration changes.
    
    Example
    -------
    >>> from biblium.config import config_context
    >>> 
    >>> # Temporarily use different settings
    >>> with config_context(color_scheme="colorblind", figure_dpi=600):
    ...     # Code here uses colorblind colors and 600 DPI
    ...     make_plot()
    >>> 
    >>> # Back to original settings
    """
    
    def __init__(self, **kwargs):
        self.overrides = kwargs
        self.original_values = {}
    
    def __enter__(self):
        # Save original values
        for key in self.overrides:
            self.original_values[key] = get(key)
        
        # Apply overrides
        for key, value in self.overrides.items():
            set(key, value)
        
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        # Restore original values
        for key, value in self.original_values.items():
            set(key, value)
        return False


# =============================================================================
# PRESET CONFIGURATIONS
# =============================================================================

PRESETS: Dict[str, Dict[str, Any]] = {
    "default": {},  # Use built-in defaults
    
    "publication": {
        "figure_dpi": 600,
        "figure_format": "pdf",
        "font_family": "serif",
        "title_size": 12,
        "label_size": 10,
    },
    
    "presentation": {
        "figure_dpi": 300,
        "figure_format": "png",
        "figure_size": (12, 8),
        "font_size": 14,
        "title_size": 18,
        "label_size": 14,
    },
    
    "colorblind": {
        "color_scheme": "colorblind",
    },
    
    "dark": {
        "color_scheme": "primary",
        "sequential_cmap": "viridis",
    },
    
    "minimal": {
        "show_grid": False,
        "verbose": False,
        "show_progress": False,
    },
    
    "large_dataset": {
        "default_chunk_size": 50000,
        "show_progress": True,
        "verbose": False,
    },
}


def apply_preset(preset_name: str) -> None:
    """
    Apply a preset configuration.
    
    Parameters
    ----------
    preset_name : str
        Name of preset: "default", "publication", "presentation",
        "colorblind", "dark", "minimal", "large_dataset".
    
    Example
    -------
    >>> from biblium import config
    >>> config.apply_preset("publication")  # High DPI, PDF output
    >>> config.apply_preset("colorblind")   # Colorblind-safe colors
    """
    if preset_name not in PRESETS:
        available = ", ".join(PRESETS.keys())
        raise ValueError(f"Unknown preset: {preset_name}. Available: {available}")
    
    if preset_name == "default":
        reset_config()
    else:
        for key, value in PRESETS[preset_name].items():
            set(key, value)


def list_presets() -> List[str]:
    """List available preset names."""
    return list(PRESETS.keys())


# =============================================================================
# HELPER FOR CLASSES
# =============================================================================

def get_init_kwargs(
    db: str = None,
    color_scheme: str = None,
    separator: str = None,
    **kwargs
) -> Dict[str, Any]:
    """
    Merge explicit kwargs with config defaults.
    
    Use this in __init__ methods to apply config defaults
    while allowing explicit overrides.
    
    Parameters
    ----------
    db : str
        Database name (explicit override).
    color_scheme : str
        Color scheme (explicit override).
    separator : str
        Separator (explicit override).
    **kwargs
        Additional explicit overrides.
    
    Returns
    -------
    dict
        Merged kwargs with config defaults.
    
    Example
    -------
    >>> # In BiblioAnalysis.__init__:
    >>> from biblium import config
    >>> 
    >>> def __init__(self, df, db=None, color_scheme=None, **kwargs):
    ...     settings = config.get_init_kwargs(db=db, color_scheme=color_scheme, **kwargs)
    ...     self.db = settings["db"]
    ...     self.color_scheme = settings["color_scheme"]
    ...     self.separator = settings["separator"]
    """
    result = {
        "db": db if db is not None else _config.default_database,
        "color_scheme": color_scheme if color_scheme is not None else _config.color_scheme,
        "separator": separator if separator is not None else get_separator(db),
        "figure_dpi": kwargs.get("figure_dpi", _config.figure_dpi),
        "figure_size": kwargs.get("figure_size", _config.figure_size),
        "show_grid": kwargs.get("show_grid", _config.show_grid),
        "verbose": kwargs.get("verbose", _config.verbose),
        "show_progress": kwargs.get("show_progress", _config.show_progress),
        "top_n": kwargs.get("top_n", _config.top_n),
    }
    
    # Add any extra kwargs
    for key, value in kwargs.items():
        if key not in result:
            result[key] = value
    
    return result


# =============================================================================
# DISPLAY FUNCTIONS
# =============================================================================

def show() -> None:
    """Print current configuration."""
    print("="*50)
    print("BIBLIUM CONFIGURATION")
    print("="*50)
    
    config_dict = _config.to_dict()
    
    # Group by category
    categories = {
        "Colors": ["color_scheme", "custom_colors", "sequential_cmap", "diverging_cmap"],
        "Database": ["default_database", "separators"],
        "Figures": ["figure_dpi", "figure_format", "figure_size", "show_grid"],
        "Fonts": ["font_family", "font_size", "title_size", "label_size"],
        "Processing": ["default_chunk_size", "show_progress", "verbose"],
        "Paths": ["output_folder", "temp_folder"],
        "Analysis": ["sdg_method", "top_n", "min_frequency"],
    }
    
    for category, keys in categories.items():
        print(f"\n{category}:")
        for key in keys:
            if key in config_dict:
                value = config_dict[key]
                if isinstance(value, dict) and len(str(value)) > 50:
                    print(f"  {key}: <{len(value)} items>")
                else:
                    print(f"  {key}: {value}")


def __repr__() -> str:
    return f"BibliumConfig(color_scheme={_config.color_scheme!r}, db={_config.default_database!r})"


# =============================================================================
# AUTO-LOAD CONFIG ON IMPORT
# =============================================================================

# Try to load config from default location
load_config_if_exists()
