"""Attribute key canonicalization — normalize, alias resolve, dotted recovery.

Pipeline: exact match → alias → dotted variant → suffix → open catalog.
"""

from __future__ import annotations

import logging
import re
from typing import Any, Literal

from sqlalchemy.ext.asyncio import AsyncSession

from arandu.config import MemoryConfig
from arandu.constants import (
    ALLOWED_ATTRIBUTE_KEYS,
    ATTRIBUTE_ALIASES,
    KEY_MAPPER_ALLOWED_NAMESPACES,
    KEY_MAPPER_MAX_KEY_LENGTH,
)

logger = logging.getLogger(__name__)

__all__ = [
    "canonicalize_attribute_key",
    "normalize_key",
    "validate_proposed_key",
]

# ---------------------------------------------------------------------------
# Pure helpers
# ---------------------------------------------------------------------------


def normalize_key(raw: str) -> str:
    """Normalize a raw attribute key: lowercase, strip, spaces/hyphens to dots.

    Underscores are preserved as legitimate separators.

    Args:
        raw: Raw attribute key string.

    Returns:
        Normalized key string.
    """
    if not raw:
        return ""
    key = raw.strip().lower()
    key = re.sub(r"[\s\-]+", ".", key)
    key = re.sub(r"\.+", ".", key)
    key = key.strip(".")
    return key


def validate_proposed_key(key: str, extra_namespaces: set[str] | None = None) -> bool:
    """Validate a proposed key meets naming rules.

    Args:
        key: Normalized key to validate.
        extra_namespaces: Optional deployer-provided namespaces to accept.

    Returns:
        True if key is well-formed and in allowed namespace.
    """
    if not key or len(key) > KEY_MAPPER_MAX_KEY_LENGTH:
        return False
    namespace = key.split(".")[0] if "." in key else key
    effective_ns = KEY_MAPPER_ALLOWED_NAMESPACES | (extra_namespaces or set())
    if namespace not in effective_ns:
        return False
    if not re.match(r"^[a-z][a-z0-9_.]*[a-z0-9]$", key):
        return False
    return True


def _build_dotted_variant_map(keys: set[str]) -> dict[str, str]:
    """Build map from dotted variants to canonical catalog keys."""
    mapping: dict[str, str] = {}
    for key in keys:
        if "_" in key:
            dotted = key.replace("_", ".")
            if dotted != key:
                mapping[dotted] = key
    return mapping


def _get_catalog_namespaces(keys: set[str]) -> set[str]:
    """Get set of all top-level namespaces from catalog keys."""
    return {key.split(".")[0] for key in keys if "." in key}


def _build_catalog_suffix_map(keys: set[str]) -> dict[str, str]:
    """Reverse suffix map for unambiguous single-part suffixes."""
    suffix_to_keys: dict[str, list[str]] = {}
    for key in keys:
        parts = key.split(".")
        if len(parts) > 1:
            suffix_to_keys.setdefault(parts[-1], []).append(key)
    return {suffix: ks[0] for suffix, ks in suffix_to_keys.items() if len(ks) == 1}


def _ns_safe_for_suffix_recovery(source_parts: list[str], target_key: str, effective_keys: set[str]) -> bool:
    """Check if suffix recovery is safe (same namespace or unrecognized source)."""
    source_ns = source_parts[0]
    target_ns = target_key.split(".")[0]
    if source_ns == target_ns:
        return True
    return source_ns not in _get_catalog_namespaces(effective_keys)


# ---------------------------------------------------------------------------
# Main canonicalization
# ---------------------------------------------------------------------------


async def canonicalize_attribute_key(
    session: AsyncSession,
    user_id: str,
    raw_key: str,
    config: MemoryConfig,
) -> tuple[str | None, Literal["allow", "map", "propose", "drop"], dict[str, Any]]:
    """Canonicalize an attribute key via catalog, alias, and recovery strategies.

    Pipeline: exact match → alias → dotted variant → suffix → open catalog → drop.

    Args:
        session: Database session.
        user_id: User identifier.
        raw_key: Raw attribute key from extraction.
        config: Memory configuration.

    Returns:
        Tuple of (canonical_key, action, metadata).
    """
    normalized = normalize_key(raw_key)

    # Merge deployer extensions with SDK defaults
    effective_keys = ALLOWED_ATTRIBUTE_KEYS | config.extra_attribute_keys
    effective_aliases = {**ATTRIBUTE_ALIASES, **config.attribute_aliases}

    # 1. Direct match in catalog
    if normalized in effective_keys:
        return normalized, "allow", {"source": "catalog"}

    # 2. Alias match
    if normalized in effective_aliases:
        canonical = effective_aliases[normalized]
        return canonical, "map", {"source": "alias", "raw": normalized}

    # 2b. Dotted-variant recovery
    dotted_map = _build_dotted_variant_map(effective_keys)
    if normalized in dotted_map:
        canonical = dotted_map[normalized]
        return canonical, "map", {"source": "dotted_variant_recovery", "raw": normalized}

    # 2c. Suffix-based recovery
    parts = normalized.split(".")
    if len(parts) > 2:
        suffix2 = ".".join(parts[-2:])
        if suffix2 in effective_aliases:
            canonical = effective_aliases[suffix2]
            if _ns_safe_for_suffix_recovery(parts, canonical, effective_keys):
                return canonical, "map", {"source": "suffix_recovery", "raw": normalized}
        if suffix2 in effective_keys:
            if _ns_safe_for_suffix_recovery(parts, suffix2, effective_keys):
                return suffix2, "map", {"source": "suffix_recovery_catalog", "raw": normalized}

    if len(parts) > 1:
        suffix1 = parts[-1]
        if suffix1 in effective_aliases:
            canonical = effective_aliases[suffix1]
            if _ns_safe_for_suffix_recovery(parts, canonical, effective_keys):
                return canonical, "map", {"source": "suffix_recovery", "raw": normalized}
        catalog_suffix = _build_catalog_suffix_map(effective_keys)
        if suffix1 in catalog_suffix:
            canonical = catalog_suffix[suffix1]
            if _ns_safe_for_suffix_recovery(parts, canonical, effective_keys):
                return canonical, "map", {"source": "suffix_recovery_catalog", "raw": normalized}

    # 3. Open catalog: accept well-formed key
    if validate_proposed_key(normalized, extra_namespaces=config.extra_namespaces):
        return normalized, "allow", {"source": "open_catalog"}

    # 4. Drop
    return None, "drop", {"reason": "unknown_key", "raw": normalized}
