"""Datasource configuration and vector settings for pipeline DB access."""

from __future__ import annotations

import importlib
import logging
import os
from typing import Any, Dict

from dotenv import load_dotenv
from qdrant_client.http.models import Distance

# Load environment variables
load_dotenv()

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Qdrant Configuration
QDRANT_HOST = os.getenv("QDRANT_HOST", "http://qdrant:6333")

# Default data source for collection naming (collections become documents_{source}, chunks_{source})
DEFAULT_DATA_SOURCE = os.getenv("DEFAULT_DATA_SOURCE", "uneg")

# Legacy collection name support (for backwards compatibility)
DOCUMENTS_COLLECTION = os.getenv(
    "DOCUMENTS_COLLECTION", f"documents_{DEFAULT_DATA_SOURCE}"
)
CHUNKS_COLLECTION = os.getenv("CHUNKS_COLLECTION", f"chunks_{DEFAULT_DATA_SOURCE}")

DEFAULT_SEGMENT_NUMBER = int(os.getenv("DEFAULT_SEGMENT_NUMBER", "0"))
MAX_SEGMENT_SIZE = int(os.getenv("MAX_SEGMENT_SIZE", "200000"))

# Write-performance tuning: delay HNSW indexing and mmap conversion
# until segments reach this many points.  Higher values speed up bulk
# ingestion at the cost of temporarily un-indexed (brute-force) search.
INDEXING_THRESHOLD = int(os.getenv("INDEXING_THRESHOLD", "20000"))
MEMMAP_THRESHOLD = int(os.getenv("MEMMAP_THRESHOLD", "20000"))

_datasources_config: Dict[str, Any] = {}


def load_datasources_config() -> Dict[str, Any]:
    """Load datasources configuration from JSON."""
    global _datasources_config
    db_module = importlib.import_module("pipeline.db")
    Path_cls = db_module.Path
    open_func = db_module.open
    json_module = db_module.json

    path_to_check = Path_cls(__file__).resolve().parents[2] / "config.json"
    config_path = path_to_check
    if not config_path.exists():
        # Fallback to old name if new doesn't exist? Or just fail?
        # Let's try legacy name if new one missing for smooth dev transition
        legacy_path = Path_cls("datasources.config.json")
        if legacy_path.exists():
            config_path = legacy_path
        else:
            logger.warning("Config file %s not found. Using defaults.", config_path)
            _datasources_config = {}
            return _datasources_config

    with open_func(config_path, encoding="utf-8") as handle:
        _datasources_config = json_module.load(handle)
    return _datasources_config


DB_VECTORS: Dict[str, Dict[str, Any]] = {}
SUPPORTED_LLMS: Dict[str, Dict[str, Any]] = {}
SUPPORTED_RERANK_MODELS: Dict[str, Dict[str, Any]] = {}
UI_MODEL_COMBOS: Dict[str, Dict[str, Any]] = {}
_config: Dict[str, Any] = {}
_app_config: Dict[str, Any] = {}
_search_config: Dict[str, Any] = {}

# Legacy constants for backward compatibility
# DB_VECTORS = "vectors"  # Removed to prevent collision with config dict
# Default Vector Name (Must match a key in supported_embedding_models)
DENSE_VECTOR_NAME = "e5_large"

# Default to "Cosine" if not defined elsewhere
VECTOR_DISTANCE_METRIC = "COSINE"
DENSE_VECTOR_SIZE = 1024
SPARSE_VECTOR_NAME = "bm25"  # Just in case


def _load_embedding_models(config: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    """Build DB_VECTORS dict from supported_embedding_models config."""
    result: Dict[str, Dict[str, Any]] = {}
    for model_name, model_info in config.get("supported_embedding_models", {}).items():
        if model_info.get("type") != "dense":
            continue
        vec_entry: Dict[str, Any] = {
            "size": model_info["size"],
            "enabled": True,
            "model_id": model_info["model_id"],
            "source": model_info.get("source", "huggingface"),
        }
        if "max_tokens" in model_info:
            vec_entry["max_tokens"] = model_info["max_tokens"]
        if "output_dimensionality" in model_info:
            vec_entry["output_dimensionality"] = model_info["output_dimensionality"]
        result[model_name] = vec_entry
    return result


def _load_llms(config: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    """Build SUPPORTED_LLMS dict from supported_llms config."""
    result: Dict[str, Dict[str, Any]] = {}
    for model_name, model_info in config.get("supported_llms", {}).items():
        llm_config: Dict[str, Any] = {
            "model": model_info.get("model"),
            "provider": model_info.get("provider"),
        }
        if "inference_provider" in model_info:
            llm_config["inference_provider"] = model_info.get("inference_provider")
        result[model_name] = llm_config
    return result


def _load_rerank_models(config: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    """Build SUPPORTED_RERANK_MODELS dict from supported_rerank_models config."""
    result: Dict[str, Dict[str, Any]] = {}
    for model_name, model_info in config.get("supported_rerank_models", {}).items():
        result[model_name] = {
            "api_version": model_info.get("api_version"),
            "endpoint": model_info.get("endpoint"),
            "endpoint_url": model_info.get("endpoint_url"),
            "model_id": model_info.get("model_id", model_name),
            "source": model_info.get("source", "huggingface"),
            "provider": model_info.get("provider"),
        }
    return result


def _resolve_default_dense_model(
    config: Dict[str, Any],
    search_config: Dict[str, Any],
    db_vectors: Dict[str, Any],
) -> str:
    """Derive the default dense model from the first datasource's index config."""
    # 1. First dense_model from the first datasource's pipeline.index.dense_models
    for ds_cfg in config.get("datasources", {}).values():
        if not isinstance(ds_cfg, dict):
            continue
        dense_models = (
            ds_cfg.get("pipeline", {}).get("index", {}).get("dense_models", [])
        )
        for model_name in dense_models:
            if model_name in db_vectors:
                return model_name

    # 2. Fallback to application.search config
    explicit = search_config.get("default_dense_model")
    if explicit and explicit in db_vectors:
        return explicit

    # 3. Hardcoded fallback
    return "e5_large"


def get_default_model_combo() -> str:
    """Return the name of the default ui_model_combo.

    Prefers whichever combo has ``"default": true`` in config.json;
    falls back to the first defined combo.
    """
    for name, combo in UI_MODEL_COMBOS.items():
        if combo.get("default"):
            return name
    return next(iter(UI_MODEL_COMBOS), "")


def refresh_config() -> None:
    """Reload config-driven settings for vectors and LLMs."""
    global _config, _app_config, _search_config, _datasources_config
    global DB_VECTORS, SUPPORTED_LLMS, SUPPORTED_RERANK_MODELS, UI_MODEL_COMBOS
    global DENSE_VECTOR_NAME, DENSE_VECTOR_SIZE

    _datasources_config = {}
    _config = load_datasources_config()

    DB_VECTORS = _load_embedding_models(_config)
    SUPPORTED_LLMS = _load_llms(_config)
    SUPPORTED_RERANK_MODELS = _load_rerank_models(_config)
    UI_MODEL_COMBOS = _config.get("ui_model_combos", {})

    _app_config = _config.get("application", {})
    _search_config = _app_config.get("search", {})
    DENSE_VECTOR_NAME = _resolve_default_dense_model(
        _config, _search_config, DB_VECTORS
    )
    DENSE_VECTOR_SIZE = DB_VECTORS.get(DENSE_VECTOR_NAME, {}).get("size", 1024)

    if "ui_model_combos" in _config:
        from utils.config_validator import validate_ui_model_combos  # noqa: PLC0415

        combo_errors = validate_ui_model_combos(_config)
        if combo_errors:
            raise ValueError(
                "Invalid ui_model_combos configuration: " + "; ".join(combo_errors)
            )


refresh_config()


def get_field_mapping(data_source: str) -> Dict[str, str]:
    """Get field mapping for a data source (core field -> source field)."""
    config = load_datasources_config()
    # Handle new config structure (datasources key) or legacy structure
    datasources = config.get("datasources", config)

    # If datasources is not a dict (e.g. if config has other top level keys but no datasources),
    # we might be iterating over the wrong thing, so be careful.
    if not isinstance(datasources, dict):
        return {}

    for domain_config in datasources.values():
        if (
            isinstance(domain_config, dict)
            and domain_config.get("data_subdir") == data_source
        ):
            return domain_config.get("field_mapping", {})
    return {}


def get_default_filter_fields(data_source: str) -> Dict[str, str]:
    """Get default filter fields for a data source (core field -> display label)."""
    config = load_datasources_config()
    # Handle new config structure (datasources key) or legacy structure
    datasources = config.get("datasources", config)

    if not isinstance(datasources, dict):
        return {}

    for domain_config in datasources.values():
        if (
            isinstance(domain_config, dict)
            and domain_config.get("data_subdir") == data_source
        ):
            return domain_config.get("default_filter_fields", {})
    return {}


def get_src_field_mapping(data_source: str) -> Dict[str, str]:
    """Return mapping from ``src_<sanitized>`` field names to raw metadata keys.

    Used by the facet endpoint to query ``src_*`` filter values directly from
    the ``src_doc_raw_metadata`` JSONB column when the field is not indexed in
    Qdrant. The mapping inverts the sanitization that
    ``normalize_document_payload`` applies at search time, e.g.
    ``{"src_evaluation_category": "Evaluation category"}``.
    """
    config = load_datasources_config()
    datasources = config.get("datasources", config)
    if not isinstance(datasources, dict):
        return {}

    for domain_config in datasources.values():
        if (
            isinstance(domain_config, dict)
            and domain_config.get("data_subdir") == data_source
        ):
            mapping = domain_config.get("src_field_mapping", {})
            if isinstance(mapping, dict):
                return {
                    str(k): str(v) for k, v in mapping.items() if isinstance(v, str)
                }
            return {}
    return {}


def get_taxonomy_filter_fields(data_source: str) -> Dict[str, str]:
    """Return taxonomy fields configured for a datasource as {tag_key: label}.

    Reads the ``pipeline.tag.taxonomies`` section of config.json and
    produces entries like ``{"tag_sdg": "SDG", "tag_cross_cutting_theme": "Cross-cutting Theme"}``.
    """
    config = load_datasources_config()
    datasources = config.get("datasources", config)
    if not isinstance(datasources, dict):
        return {}

    for domain_config in datasources.values():
        if (
            isinstance(domain_config, dict)
            and domain_config.get("data_subdir") == data_source
        ):
            taxonomies = (
                domain_config.get("pipeline", {}).get("tag", {}).get("taxonomies", {})
            )
            result: Dict[str, str] = {}
            for tax_key, tax_def in taxonomies.items():
                tag_field = f"tag_{tax_key}"
                label = tax_def.get("name", tax_key)
                result[tag_field] = label
            return result
    return {}


def core_to_source_field(data_source: str, core_field: str) -> str:
    """Map a core field name to source field name."""
    mapping = get_field_mapping(data_source)
    mapping_value = mapping.get(core_field, core_field)
    # If the mapping is a fixed_value, there's no source field, so return the core field name
    if isinstance(mapping_value, str) and mapping_value.startswith("fixed_value:"):
        return core_field
    return mapping_value


def source_to_core_field(data_source: str, source_field: str) -> str:
    """Map source field name to core field name."""
    mapping = get_field_mapping(data_source)
    reverse_mapping = {v: k for k, v in mapping.items()}
    return reverse_mapping.get(source_field, source_field)


def get_application_config() -> Dict[str, Any]:
    """Get application config from datasources config."""
    config = load_datasources_config()
    return config.get("application", {})


VALID_METRICS = {
    "COSINE": Distance.COSINE,
    "EUCLID": Distance.EUCLID,
    "DOT": Distance.DOT,
}

# HNSW Configuration
HNSW_M = int(os.getenv("HNSW_M", "16"))
HNSW_EF_CONSTRUCT = int(os.getenv("HNSW_EF_CONSTRUCT", "100"))

# Storage & Memory Configuration
VECTORS_ON_DISK = os.getenv("VECTORS_ON_DISK", "false").lower() == "true"
HNSW_ON_DISK = os.getenv("HNSW_ON_DISK", "false").lower() == "true"

# Quantization Configuration
ENABLE_QUANTIZATION = os.getenv("ENABLE_QUANTIZATION", "false").lower() == "true"
QUANTIZATION_TYPE = os.getenv("QUANTIZATION_TYPE", "int8")
QUANTIZATION_ALWAYS_RAM = os.getenv("QUANTIZATION_ALWAYS_RAM", "true").lower() == "true"
QUANTIZATION_RESCORE = os.getenv("QUANTIZATION_RESCORE", "true").lower() == "true"


def clean_model_name(model_id: str) -> str:
    """Generate a clean vector name from model ID."""
    name = model_id.split("/")[-1]
    name = name.replace("-en-v1.5", "").replace("-v2", "").replace("-v1", "")
    name = name.replace(".", "_").replace("-", "_")
    return name


def _clean_model_name(model_id: str) -> str:
    """Backward-compatible alias for clean_model_name."""
    return clean_model_name(model_id)
