from __future__ import annotations

import hashlib
import json
import os
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Dict, Optional, Sequence, Type
from urllib.parse import urlparse

from pydantic import BaseModel


def _validate_collection_name(name: str) -> None:
    """
    Validate that collection_name follows Python variable naming rules.

    Only allows letters, numbers, and underscores. Must not start with a number.

    Args:
        name: Collection name to validate

    Raises:
        ValueError: If name is invalid
    """
    if not name:
        raise ValueError("collection_name cannot be empty")

    # Python variable name pattern: starts with letter or underscore,
    # followed by letters, numbers, or underscores
    if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
        raise ValueError(
            f"Invalid collection_name '{name}'. Must contain only letters, numbers, "
            "and underscores, and cannot start with a number (Python variable naming rules)."
        )


@dataclass(frozen=True)
class ModelSpec:
    """Describes how a Pydantic model should be indexed."""

    model: Type[BaseModel]
    semantic_fields: Sequence[str]
    keyword_fields: Sequence[str]


@dataclass(frozen=True)
class CollectionEnvironment:
    """
    Configuration for a collection's query executor and embedding settings.

    This environment describes how to connect to a ChromaDB collection,
    what embedding model to use, and where to find the model registry.

    Secret Handling:
        Fields containing secrets (API keys, credentials) can use the special
        syntax "env:VAR_NAME" to reference environment variables:
            cloud_api_key="env:CHROMA_API_KEY"

        On deserialization, these are resolved to actual values from os.environ.

    Persistence:
        Use to_json() / from_json() to serialize/deserialize the environment.
        This allows persisting collection configurations alongside ChromaDB data.

    Attributes:
        collection_name: Unique identifier for the collection. Must follow Python
            variable naming rules (letters, numbers, underscores; cannot start with number).
        query_config_path: Path to the Chroma query configuration JSON.
        discriminator_field: Metadata field for model routing.
        model_registry_target: Import path for loading the model registry
            (e.g., "package.module:REGISTRY_VAR").
        embedding_model: Embedding model name (e.g., "text-embedding-3-small").
        chroma_client_type: One of "cloud", "http", "persistent", or "memory".
        cloud_api_key: ChromaDB cloud API key (or "env:VAR_NAME").
        cloud_tenant: ChromaDB cloud tenant ID.
        cloud_database: ChromaDB cloud database name.
        http_host: HTTP client host.
        http_port: HTTP client port.
        http_ssl: Whether to use SSL for HTTP client.
        local_persist_dir: Directory for persistent local ChromaDB.
        local_collection_name: Collection name for local ChromaDB
        (deprecated, use collection_name).
        is_local: Flag indicating this is a local user collection.
    """

    collection_name: str
    query_config_path: Path
    discriminator_field: str
    model_registry_target: str
    embedding_model: str
    chroma_client_type: str = "cloud"
    cloud_api_key: Optional[str] = None
    cloud_tenant: Optional[str] = None
    cloud_database: Optional[str] = None
    http_host: Optional[str] = None
    http_port: Optional[int] = None
    http_ssl: bool = False
    local_persist_dir: Optional[Path] = None
    local_collection_name: Optional[str] = None
    is_local: bool = False

    def __post_init__(self) -> None:
        """Validate collection_name after initialization."""
        _validate_collection_name(self.collection_name)

    @staticmethod
    def sanitize_string_to_collection_name(
        input_string: str,
        max_length: int = 63,
        hash_length: int = 8,
        add_hash: bool = True,
    ) -> str:
        """
        Convert any string into a valid collection name.

        Replaces invalid characters with underscores and ensures the result
        follows Python variable naming rules.

        Args:
            input_string: Any string to convert
            max_length: Maximum length of the output (default: 63)
            hash_length: Length of hash suffix to ensure uniqueness (default: 8)
            add_hash: Whether to add hash suffix for uniqueness (default: True)

        Returns:
            Valid collection name following Python variable rules

        Examples:
            >>> CollectionEnvironment.sanitize_string_to_collection_name("My Collection!")
            'My_Collection_a1b2c3d4'
            >>> CollectionEnvironment.sanitize_string_to_collection_name("123-test")
            '_123_test_e5f6g7h8'
            >>> CollectionEnvironment.sanitize_string_to_collection_name(\\
                "ECC 6.0", add_hash=False)
            'ECC_6_0'
        """
        if not input_string:
            raise ValueError("input_string cannot be empty")

        # Replace invalid characters with underscores
        # Keep only alphanumeric and underscore
        sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", input_string)

        # Remove consecutive underscores
        sanitized = re.sub(r"_+", "_", sanitized)

        # Strip leading/trailing underscores
        sanitized = sanitized.strip("_")

        # If starts with number, prefix with underscore
        if sanitized and sanitized[0].isdigit():
            sanitized = "_" + sanitized

        # If empty after sanitization, use a default prefix
        if not sanitized:
            sanitized = "collection"

        if add_hash:
            # Add hash for uniqueness
            hash_suffix = hashlib.md5(input_string.encode()).hexdigest()[:hash_length]

            # Truncate to fit max_length (accounting for hash and underscore)
            max_prefix_length = max_length - hash_length - 1
            if len(sanitized) > max_prefix_length:
                sanitized = sanitized[:max_prefix_length]

            result = f"{sanitized}_{hash_suffix}"
        else:
            # Truncate to max_length if needed
            if len(sanitized) > max_length:
                sanitized = sanitized[:max_length]
            result = sanitized

        # Final validation
        _validate_collection_name(result)

        return result

    @staticmethod
    def from_url(url: str, max_domain_length: int = 20, hash_length: int = 8) -> str:
        """
        Generate a collection name from a URL.

        Uses domain name and URL hash to create a readable, unique identifier.

        Args:
            url: URL to convert to collection name
            max_domain_length: Maximum length of the domain part (default: 20)
            hash_length: Length of hash suffix (default: 8)

        Returns:
            Valid collection name

        Examples:
            >>> CollectionEnvironment.from_url("https://example.com/doc.pdf")
            'example_com_a1b2c3d4'
            >>> CollectionEnvironment.from_url("https://my-site.org/path/to/file")
            'my_site_org_e5f6g7h8'
        """
        if not url:
            raise ValueError("url cannot be empty")

        parsed = urlparse(url)
        domain = parsed.netloc or "local"

        # Sanitize domain: replace dots and dashes with underscores
        domain_sanitized = domain.replace(".", "_").replace("-", "_")

        # Remove consecutive underscores
        domain_sanitized = re.sub(r"_+", "_", domain_sanitized)

        # Limit domain length
        domain_sanitized = domain_sanitized[:max_domain_length]

        # Generate hash from full URL for uniqueness
        url_hash = hashlib.md5(url.encode()).hexdigest()[:hash_length]

        result = f"{domain_sanitized}_{url_hash}"

        # Ensure valid name
        _validate_collection_name(result)

        return result

    @staticmethod
    def from_sap_version(sap_version: str) -> str:
        """
        Generate a collection name from SAP version string.

        Does not add hash suffix since SAP versions are already unique identifiers.

        Examples:
            >>> CollectionEnvironment.from_sap_version("ECC 6.0 Ehp 7")
            'ecc_6_0_ehp_7'
            >>> CollectionEnvironment.from_sap_version("S/4 HANA 2021")
            's_4_hana_2021'
        """
        return CollectionEnvironment.sanitize_string_to_collection_name(
            sap_version.lower(), add_hash=False
        )

    def to_dict(self) -> Dict[str, Any]:
        """
        Convert environment to a dictionary with Path objects converted to strings.

        Returns:
            Dictionary representation with serializable values
        """
        data = asdict(self)
        # Convert Path objects to strings for JSON serialization
        if data.get("query_config_path"):
            data["query_config_path"] = str(data["query_config_path"])
        if data.get("local_persist_dir"):
            data["local_persist_dir"] = str(data["local_persist_dir"])
        return data

    def to_json(self) -> str:
        """
        Serialize environment to JSON string.

        Secret values are preserved in their "env:VAR_NAME" form.

        Returns:
            JSON string representation
        """
        return json.dumps(self.to_dict(), indent=2)

    @classmethod
    def from_dict(
        cls, data: Dict[str, Any], resolve_secrets: bool = True
    ) -> CollectionEnvironment:
        """
        Create environment from dictionary, optionally resolving secrets.

        Args:
            data: Dictionary with environment configuration
            resolve_secrets: If True, resolve "env:VAR_NAME" references to actual values

        Returns:
            CollectionEnvironment instance

        Raises:
            ValueError: If a secret reference cannot be resolved
        """
        data = data.copy()

        # Convert string paths back to Path objects
        if data.get("query_config_path"):
            data["query_config_path"] = Path(data["query_config_path"])
        if data.get("local_persist_dir"):
            data["local_persist_dir"] = Path(data["local_persist_dir"])

        # Resolve secret references if requested
        if resolve_secrets:
            secret_fields = ["cloud_api_key"]
            for field in secret_fields:
                value = data.get(field)
                if value and isinstance(value, str) and value.startswith("env:"):
                    env_var = value[4:]  # Remove "env:" prefix
                    resolved = os.environ.get(env_var)
                    if resolved is None:
                        raise ValueError(
                            f"Secret reference '{value}' could not be resolved: "
                            f"environment variable '{env_var}' not found"
                        )
                    data[field] = resolved

        return cls(**data)

    @classmethod
    def from_json(
        cls, json_str: str, resolve_secrets: bool = True
    ) -> CollectionEnvironment:
        """
        Deserialize environment from JSON string.

        Args:
            json_str: JSON string representation
            resolve_secrets: If True, resolve "env:VAR_NAME" references

        Returns:
            CollectionEnvironment instance
        """
        data = json.loads(json_str)
        return cls.from_dict(data, resolve_secrets=resolve_secrets)

    @classmethod
    def from_file(
        cls, path: Path, resolve_secrets: bool = True
    ) -> CollectionEnvironment:
        """
        Load environment from JSON file.

        Args:
            path: Path to JSON file
            resolve_secrets: If True, resolve "env:VAR_NAME" references

        Returns:
            CollectionEnvironment instance
        """
        with open(path, "r") as f:
            return cls.from_json(f.read(), resolve_secrets=resolve_secrets)

    def to_file(self, path: Path) -> None:
        """
        Save environment to JSON file.

        Args:
            path: Path where JSON file should be written
        """
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w") as f:
            f.write(self.to_json())
