"""
Configuration schema for g-gremlin sync command.

Supports both YAML and JSON config files with auto-detection by extension.
"""

from __future__ import annotations

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

try:
    import yaml
    HAS_YAML = True
except ImportError:
    HAS_YAML = False

from g_gremlin.a1 import parse_a1_range
from g_gremlin.errors import config_file_not_found_error, missing_pyyaml_error


@dataclass
class TabConfig:
    """Configuration for a single tab to write."""
    csv: str
    anchor: str = "A1"
    formulas: bool = False

    def resolve_csv_path(self, config_dir: Path) -> Path:
        """Resolve CSV path relative to config file directory."""
        csv_path = Path(self.csv)
        if csv_path.is_absolute():
            return csv_path
        return (config_dir / csv_path).resolve()


@dataclass
class StyleSpec:
    """Style specification for a range."""
    range: str  # A1 notation or "body" for auto-expand
    header: bool = False
    bold: bool = False
    background: Optional[str] = None  # Hex color like "#4285F4"
    font_color: Optional[str] = None  # Hex color like "#FFFFFF"
    zebra: Optional[str] = None  # Stripe color for even rows
    font_size: Optional[int] = None
    freeze_rows: Optional[int] = None
    freeze_cols: Optional[int] = None


@dataclass
class Settings:
    """Safety and behavior settings."""
    max_cells: int = 100000
    require_backup: bool = False
    validate_live_refs: bool = False


@dataclass
class SyncConfig:
    """
    Complete configuration for g-gremlin sync command.

    Example config (YAML):
        version: 1
        sheet_id: "1abc123..."

        tabs:
          Quick Calculator:
            csv: ./build/quick_calculator.csv
            formulas: true
          Pricing Engine:
            csv: ./build/pricing.csv

        variables:
          VERSION: "2.1.0"

        named_ranges:
          INPUT_VOLUME: "'Quick Calculator'!C10"

        styles:
          - range: "A1:Z1"
            header: true
            bold: true
            background: "#4285F4"

        settings:
          max_cells: 100000
    """
    version: int
    sheet_id: str
    tabs: Dict[str, TabConfig]
    variables: Dict[str, str] = field(default_factory=dict)
    named_ranges: Dict[str, str] = field(default_factory=dict)
    styles: List[StyleSpec] = field(default_factory=list)
    settings: Settings = field(default_factory=Settings)

    # Internal: path to config file for relative path resolution
    _config_path: Optional[Path] = field(default=None, repr=False)

    def __post_init__(self):
        """Validate configuration after initialization."""
        if self.version != 1:
            raise ValueError(f"Unsupported config version: {self.version}. Only version 1 is supported.")
        if not self.sheet_id:
            raise ValueError("sheet_id is required")
        if not self.tabs:
            raise ValueError("At least one tab must be configured")

    @property
    def config_dir(self) -> Path:
        """Directory containing the config file, for relative path resolution."""
        if self._config_path:
            return self._config_path.parent
        return Path.cwd()


class ConfigValidationError(Exception):
    """Raised when config validation fails."""
    pass


def load_config(config_path: Union[str, Path], sheet_id_override: Optional[str] = None) -> SyncConfig:
    """
    Load and validate a sync config from YAML or JSON file.

    Args:
        config_path: Path to config file (.yaml, .yml, or .json)
        sheet_id_override: Optional sheet_id to override config value

    Returns:
        Validated SyncConfig object

    Raises:
        ConfigValidationError: If config is invalid
        FileNotFoundError: If config file doesn't exist
    """
    path = Path(config_path)
    if not path.exists():
        raise config_file_not_found_error(path)

    # Determine format from extension
    ext = path.suffix.lower()
    if ext in (".yaml", ".yml"):
        if not HAS_YAML:
            raise missing_pyyaml_error()
        with path.open("r", encoding="utf-8") as f:
            raw_config = yaml.safe_load(f)
    elif ext == ".json":
        with path.open("r", encoding="utf-8") as f:
            raw_config = json.load(f)
    else:
        raise ConfigValidationError(
            f"Unsupported config file extension: {ext}. Use .yaml, .yml, or .json"
        )

    if not isinstance(raw_config, dict):
        raise ConfigValidationError("Config must be a dictionary/object")

    # Parse and validate
    try:
        config = _parse_config(raw_config, path)
    except (KeyError, TypeError, ValueError) as e:
        raise ConfigValidationError(f"Invalid config: {e}") from e

    # Apply overrides
    if sheet_id_override:
        config.sheet_id = sheet_id_override

    # Expand environment variables in sheet_id
    if config.sheet_id.startswith("$"):
        env_var = config.sheet_id[1:]
        config.sheet_id = os.environ.get(env_var, config.sheet_id)

    return config


def _parse_config(raw: Dict[str, Any], config_path: Path) -> SyncConfig:
    """Parse raw config dict into SyncConfig object."""
    # Required fields
    version = raw.get("version")
    if version is None:
        raise ValueError("'version' is required (set to 1)")

    sheet_id = raw.get("sheet_id", "")

    # Parse tabs
    raw_tabs = raw.get("tabs", {})
    if not isinstance(raw_tabs, dict):
        raise ValueError("'tabs' must be a dictionary")

    tabs: Dict[str, TabConfig] = {}
    for tab_name, tab_data in raw_tabs.items():
        if isinstance(tab_data, str):
            # Shorthand: just CSV path
            tabs[tab_name] = TabConfig(csv=tab_data)
        elif isinstance(tab_data, dict):
            tabs[tab_name] = TabConfig(
                csv=tab_data.get("csv", ""),
                anchor=tab_data.get("anchor", "A1"),
                formulas=tab_data.get("formulas", False),
            )
        else:
            raise ValueError(f"Invalid tab config for '{tab_name}'")

    # Parse variables
    variables = raw.get("variables", {})
    if not isinstance(variables, dict):
        raise ValueError("'variables' must be a dictionary")
    # Convert all values to strings
    variables = {k: str(v) for k, v in variables.items()}

    # Parse named ranges
    named_ranges = raw.get("named_ranges", {})
    if not isinstance(named_ranges, dict):
        raise ValueError("'named_ranges' must be a dictionary")

    # Parse styles
    raw_styles = raw.get("styles", [])
    if not isinstance(raw_styles, list):
        raise ValueError("'styles' must be a list")

    styles: List[StyleSpec] = []
    for style_data in raw_styles:
        if not isinstance(style_data, dict):
            raise ValueError("Each style must be a dictionary")
        styles.append(StyleSpec(
            range=style_data.get("range", ""),
            header=style_data.get("header", False),
            bold=style_data.get("bold", False),
            background=style_data.get("background"),
            font_color=style_data.get("font_color"),
            zebra=style_data.get("zebra"),
            font_size=style_data.get("font_size"),
            freeze_rows=style_data.get("freeze_rows"),
            freeze_cols=style_data.get("freeze_cols"),
        ))

    # Parse settings
    raw_settings = raw.get("settings", {})
    if not isinstance(raw_settings, dict):
        raise ValueError("'settings' must be a dictionary")

    settings = Settings(
        max_cells=raw_settings.get("max_cells", 100000),
        require_backup=raw_settings.get("require_backup", False),
        validate_live_refs=raw_settings.get("validate_live_refs", False),
    )

    return SyncConfig(
        version=version,
        sheet_id=sheet_id,
        tabs=tabs,
        variables=variables,
        named_ranges=named_ranges,
        styles=styles,
        settings=settings,
        _config_path=config_path,
    )


def validate_config(config: SyncConfig) -> List[str]:
    """
    Validate a config and return list of warnings/issues.

    Returns:
        List of warning messages (empty if no issues)
    """
    warnings: List[str] = []

    # Check that CSV files exist
    for tab_name, tab_config in config.tabs.items():
        csv_path = tab_config.resolve_csv_path(config.config_dir)
        if not csv_path.exists():
            warnings.append(f"CSV file not found for tab '{tab_name}': {csv_path}")
        # Validate anchor early to fail-fast in CLI
        try:
            parse_a1_range(tab_config.anchor if "!" in tab_config.anchor else f"Sheet!{tab_config.anchor}")
        except Exception as exc:
            warnings.append(f"Invalid anchor for tab '{tab_name}': {exc}")

    # Check style ranges
    for i, style in enumerate(config.styles):
        if not style.range:
            warnings.append(f"Style {i+1} has empty range")

    return warnings
