import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import yaml

try:
    import tomllib
except ImportError:
    # Fallback for older python versions if needed, though 3.11+ has tomllib
    import tomli as tomllib

from veildata.compose import Compose
from veildata.core import Module
from veildata.exceptions import ConfigMissingError
from veildata.revealers import TokenStore

REDACTOR_REGISTRY: Dict[str, str] = {
    "regex": "veildata.redactors.regex.RegexRedactor",
    "ner_spacy": "veildata.redactors.ner_spacy.SpacyNERRedactor",
    "ner_bert": "veildata.redactors.ner_bert.BERTNERRedactor",
}


def list_available_redactors() -> List[str]:
    """Return available redaction engines."""
    return list(REDACTOR_REGISTRY.keys()) + ["all"]


def list_engines():
    return [
        ("regex", "Pattern-based redaction (fast, deterministic)."),
        ("spacy", "NER-based entity detection."),
        ("hybrid", "Regex + NER combined."),
    ]


def load_config(config_path: Optional[str], verbose: bool = False) -> dict:
    # If no config path provided, check default location
    if not config_path:
        default_path = Path.home() / ".veildata" / "config.toml"
        if default_path.exists():
            if verbose:
                print(f"[veildata] Loaded default config from {default_path}")
            config_path = str(default_path)
        else:
            return {}

    path = Path(config_path)
    try:
        text = path.read_text()
        if verbose:
            print(f"[veildata] Loaded config from {path.absolute()}")
    except FileNotFoundError:
        raise ConfigMissingError(f"Configuration file not found: {config_path}")

    if config_path.endswith(".json"):
        return json.loads(text)
    if config_path.endswith(".toml"):
        return tomllib.loads(text)
    return yaml.safe_load(text)


def _lazy_import(dotted_path: str):
    """
    Import a class by dotted string path:
    e.g. "veildata.redactors.regex.RegexRedactor"
    """
    module_path, cls_name = dotted_path.rsplit(".", 1)
    module = __import__(module_path, fromlist=[cls_name])
    return getattr(module, cls_name)


def build_redactor(
    method: str = "regex",
    detect_mode: str = "rules",
    config_path: Optional[str] = None,
    ml_config_path: Optional[str] = None,
    verbose: bool = False,
    config_dict: Optional[Dict] = None,
) -> Tuple[Module, TokenStore]:
    """
    Factory function to build a redactor based on configuration.
    """
    # Load main config
    if config_dict is not None:
        config = config_dict
    else:
        config = load_config(config_path, verbose=verbose)

    ml_config = load_config(ml_config_path, verbose=verbose)
    method = method.lower()
    store = TokenStore()

    def vprint(msg: str):
        if verbose:
            print(f"[veildata] {msg}")

    # Check if config has patterns - if so, use new detector-based approach even for rules mode
    if config.get("pattern") or config.get("patterns"):
        from veildata.detectors import (
            BertDetector,
            HybridDetector,
            RegexDetector,
            SpacyDetector,
        )
        from veildata.pipeline import DetectionPipeline

        # Support both "pattern" and "patterns" key names
        patterns = config.get("pattern") or config.get("patterns")

        if detect_mode == "rules":
            # Rules mode with patterns from config
            vprint(f"Loading RegexDetector with {len(patterns)} patterns...")
            detector = RegexDetector(patterns)
            return DetectionPipeline(detector, store=store), store

        elif detect_mode in ["ml", "hybrid"]:
            # ML/Hybrid mode
            detectors = []

            # 1. Add ML Detectors
            # ml_config can have settings at root level or nested under 'ml' key
            ml_settings = ml_config.get("ml", ml_config) if ml_config else {}
            spacy_conf = ml_settings.get("spacy", {})
            bert_conf = ml_settings.get("bert", {})

            # If no config provided, enable Spacy by default for 'ml' mode as a sane default
            if not ml_config:
                spacy_conf = {"enabled": True}

            if spacy_conf.get("enabled", False) or (
                not ml_config and detect_mode == "ml"
            ):
                vprint("Loading SpacyDetector...")
                detectors.append(
                    SpacyDetector(
                        model=spacy_conf.get("model", "en_core_web_lg"),
                        pii_labels=spacy_conf.get("pii_labels"),
                    )
                )

            if bert_conf.get("enabled", False):
                vprint("Loading BertDetector...")
                detectors.append(
                    BertDetector(
                        model_name=bert_conf.get("model_path", "dslim/bert-base-NER"),
                        threshold=bert_conf.get("threshold", 0.5),
                        label_mapping=bert_conf.get("label_mapping"),
                    )
                )

            # 2. Add Regex Detector for Hybrid mode
            if detect_mode == "hybrid":
                vprint("Loading RegexDetector for Hybrid mode...")
                detectors.append(RegexDetector(patterns))

            if not detectors:
                raise ValueError("No detectors enabled for ML/Hybrid mode.")

            if len(detectors) > 1:
                vprint(f"Combining {len(detectors)} detectors in Hybrid mode...")
                hybrid_conf = config.get("options", {}).get("hybrid", {})
                detector = HybridDetector(
                    detectors,
                    strategy=hybrid_conf.get("strategy", "union"),
                    prefer=hybrid_conf.get("prefer", "ml"),
                )
            else:
                detector = detectors[0]

            return DetectionPipeline(detector, store=store), store

    # Legacy / Rules Mode (no patterns in config)
    # Filter out top-level keys that are not for redactor constructors
    redactor_config = {k: v for k, v in config.items() if k not in ["method", "ml"]}

    if method == "all":
        redactors = []
        for key, dotted_path in REDACTOR_REGISTRY.items():
            cls = _lazy_import(dotted_path)
            vprint(f"Loading redactor: {key}")
            redactors.append(cls(store=store, **redactor_config))
        return Compose(redactors), store

    if method not in REDACTOR_REGISTRY:
        raise ValueError(
            f"Unknown redaction method '{method}'. "
            f"Available: {', '.join(list_available_redactors())}"
        )

    cls_path = REDACTOR_REGISTRY[method]
    cls = _lazy_import(cls_path)
    vprint(f"Loading redactor: {method}")

    redactor = cls(store=store, **redactor_config)
    return Compose([redactor]), store


def build_revealer(store_path: str):
    """
    Build a callable revealer from a saved TokenStore mapping.

    Returns:
        callable(text: str) -> str
    """
    store = TokenStore.load(store_path)
    return store.reveal
