"""Configuration profiles: save/load deduplication settings as JSON."""

from __future__ import annotations

import json
from dataclasses import dataclass, field, fields, asdict
from pathlib import Path
from typing import Optional

from .dedup import (
    Algorithm,
    ColumnMatchStrategy,
    MatchStrategy,
    NormalizerType,
    SurvivorRule,
)


@dataclass
class ColumnStrategyConfig:
    """JSON-serializable mirror of ColumnMatchStrategy."""
    column: str
    algorithm: str = "exact"
    threshold: float = 100.0
    normalizer: Optional[str] = None


@dataclass
class StrategyConfig:
    """JSON-serializable mirror of MatchStrategy."""
    columns: list[ColumnStrategyConfig] = field(default_factory=list)


@dataclass
class DeduplicationConfig:
    """All deduplication settings as a flat JSON-serializable structure."""

    strategies: list[StrategyConfig] = field(default_factory=list)
    survivor_rule: str = "first"
    date_column: Optional[str] = None
    merge: bool = False
    subset_columns: Optional[list[str]] = None
    fuzzy_columns: Optional[list[str]] = None
    default_algorithm: str = "jaro_winkler"
    default_threshold: float = 85.0
    normalize_map: Optional[dict[str, str]] = None  # column -> normalizer type

    # -----------------------------------------------------------------------
    # Serialisation
    # -----------------------------------------------------------------------

    def to_dict(self) -> dict:
        return asdict(self)

    def to_file(self, path: str | Path) -> Path:
        """Save configuration to a JSON file."""
        out = Path(path)
        out.write_text(json.dumps(self.to_dict(), indent=2))
        return out

    @classmethod
    def from_dict(cls, data: dict) -> DeduplicationConfig:
        # Filter unknown fields silently — keeps loading forward-compatible
        # when older code reads a config written by a newer version that
        # added fields to ColumnStrategyConfig.
        col_known = {f.name for f in fields(ColumnStrategyConfig)}
        strategies = []
        for s in data.get("strategies", []):
            cols = [
                ColumnStrategyConfig(**{k: v for k, v in c.items() if k in col_known})
                for c in s.get("columns", [])
            ]
            strategies.append(StrategyConfig(columns=cols))
        return cls(
            strategies=strategies,
            survivor_rule=data.get("survivor_rule", "first"),
            date_column=data.get("date_column"),
            merge=data.get("merge", False),
            subset_columns=data.get("subset_columns"),
            fuzzy_columns=data.get("fuzzy_columns"),
            default_algorithm=data.get("default_algorithm", "jaro_winkler"),
            default_threshold=data.get("default_threshold", 85.0),
            normalize_map=data.get("normalize_map"),
        )

    @classmethod
    def from_file(cls, path: str | Path) -> DeduplicationConfig:
        """Load configuration from a JSON file."""
        from .errors import ConfigError, wrap_file_read
        path = Path(path)
        try:
            text = path.read_text()
        except OSError as e:
            raise wrap_file_read(path, "DeduplicationConfig.from_file", e) from e
        try:
            data = json.loads(text)
        except json.JSONDecodeError as e:
            raise ConfigError(
                "Invalid JSON in DeduplicationConfig file",
                path=path,
                operation="DeduplicationConfig.from_file",
                cause=e,
                suggestion=(
                    f"JSON parser failed at line {e.lineno}, column {e.colno}. "
                    "Validate with `python -m json.tool < file.json`."
                ),
            ) from e
        return cls.from_dict(data)

    @classmethod
    def default(cls) -> DeduplicationConfig:
        """Return sensible defaults (auto-detect strategies at runtime)."""
        return cls()

    # -----------------------------------------------------------------------
    # Convert to engine objects
    # -----------------------------------------------------------------------

    def to_strategies(self) -> Optional[list[MatchStrategy]]:
        """Convert the config back to MatchStrategy objects.

        Returns None if no explicit strategies are configured
        (the engine will auto-detect).
        """
        if not self.strategies:
            return None

        from .errors import ConfigError
        result: list[MatchStrategy] = []
        for s_idx, sc in enumerate(self.strategies):
            col_strats = []
            for cc in sc.columns:
                try:
                    algorithm = Algorithm(cc.algorithm)
                except ValueError as e:
                    raise ConfigError(
                        f"Invalid algorithm {cc.algorithm!r}",
                        column=cc.column,
                        operation=f"strategy[{s_idx}]",
                        cause=e,
                        suggestion=f"Valid: {sorted(a.value for a in Algorithm)}",
                    ) from e
                try:
                    normalizer = (
                        NormalizerType(cc.normalizer) if cc.normalizer else None
                    )
                except ValueError as e:
                    raise ConfigError(
                        f"Invalid normalizer {cc.normalizer!r}",
                        column=cc.column,
                        operation=f"strategy[{s_idx}]",
                        cause=e,
                        suggestion=f"Valid: {sorted(n.value for n in NormalizerType)}",
                    ) from e
                col_strats.append(ColumnMatchStrategy(
                    column=cc.column,
                    algorithm=algorithm,
                    threshold=cc.threshold,
                    normalizer=normalizer,
                ))
            result.append(MatchStrategy(column_strategies=col_strats))
        return result

    def to_survivor_rule(self) -> SurvivorRule:
        from .errors import ConfigError
        try:
            return SurvivorRule(self.survivor_rule)
        except ValueError as e:
            raise ConfigError(
                f"Invalid survivor_rule {self.survivor_rule!r}",
                operation="DeduplicationConfig.to_survivor_rule",
                cause=e,
                suggestion=f"Valid: {sorted(r.value for r in SurvivorRule)}",
            ) from e
