"""Core constants and data types for the memory system.

Extracted from the original memory_constants.py. Only includes types and constants
needed by the SDK — domain config YAMLs and hot-reload are excluded (v1+).
"""

from dataclasses import dataclass, field
from typing import Literal

from pydantic import BaseModel, Field

# =============================================================================
# CONFIDENCE
# =============================================================================

CONFIDENCE_LEVEL_MAP: dict[str, float] = {
    "explicit_statement": 0.95,
    "strong_inference": 0.80,
    "weak_inference": 0.60,
    "speculation": 0.40,
}
CONFIDENCE_DEFAULT: float = 0.60


def resolve_confidence(
    confidence_level: str | None = None,
    confidence_float: float | None = None,
) -> float:
    """Resolve a confidence score from categorical level or float.

    Priority: confidence_level (mapped) > confidence_float > CONFIDENCE_DEFAULT.
    """
    if confidence_level and confidence_level in CONFIDENCE_LEVEL_MAP:
        return CONFIDENCE_LEVEL_MAP[confidence_level]
    if confidence_float is not None:
        return max(0.0, min(1.0, confidence_float))
    return CONFIDENCE_DEFAULT


# =============================================================================
# CANONICAL RELATIONSHIP TYPES
#
# Reference set of common relationship types. Used as documentation and hint
# for consumers — NOT as a validation filter. The extraction pipeline accepts
# any rel_type; normalize_rel_type() sanitizes and maps aliases but passes
# unknown types through without rejection.
# =============================================================================

CANONICAL_REL_TYPES: set[str] = {
    "reports_to",
    "manages",
    "member_of",
    "works_with",
    "works_at",
    "family_of",
    "friend_of",
    "partner_of",
    "lives_in",
    "owns",
    "studies_at",
}

_REL_TYPE_ALIASES: dict[str, str] = {
    "direct_report": "manages",
    "boss": "reports_to",
    "manager": "reports_to",
    "subordinate": "manages",
    "led_by": "reports_to",
    "leads": "manages",
    "supervises": "manages",
    "supervised_by": "reports_to",
    "colleague": "works_with",
    "coworker": "works_with",
    "social_connection": "friend_of",
    "related_to": "family_of",
    "sibling": "family_of",
    "parent": "family_of",
    "child": "family_of",
    "spouse": "partner_of",
    "dating": "partner_of",
    "romantic_partner": "partner_of",
    "roommate": "lives_in",
    "employee_of": "works_at",
    "employed_at": "works_at",
    "enrolled_at": "studies_at",
    "attends": "studies_at",
    "pet_owner": "owns",
    "belongs_to": "member_of",
    "part_of": "member_of",
}


def normalize_rel_type(raw: str) -> str:
    """Normalize a relationship type string.

    Applies alias mapping for known synonyms. Unknown types pass through
    after sanitization (lowercase, underscores). CANONICAL_REL_TYPES is a
    reference set, not a validation filter — types outside the canonical
    set are accepted and returned as-is after cleaning.

    Args:
        raw: Raw relationship type string from LLM extraction.

    Returns:
        Normalized relationship type string (lowercase, snake_case).
    """
    cleaned = raw.strip().lower().replace(" ", "_").replace("-", "_")
    if cleaned in CANONICAL_REL_TYPES:
        return cleaned
    return _REL_TYPE_ALIASES.get(cleaned, cleaned)


# =============================================================================
# CONSOLIDATION VALIDATION SETS
# =============================================================================

VALID_EMOTIONS: frozenset[str] = frozenset(
    {
        "satisfied",
        "disappointed",
        "anxious",
        "excited",
        "overwhelmed",
        "confused",
        "hopeful",
        "neutral",
        "frustrated",
        "determined",
    }
)

VALID_ENERGIES: frozenset[str] = frozenset(
    {
        "high",
        "medium",
        "low",
    }
)

VALID_OBSERVATION_TYPES: frozenset[str] = frozenset(
    {
        "insight",
        "pattern",
        "contradiction",
        "trend",
        "cross_entity_theme",
        "behavioral_preference",
        "community_theme",
    }
)


# =============================================================================
# ATTRIBUTE KEY CANONICALIZATION
# =============================================================================

KEY_MAPPER_MAX_KEY_LENGTH: int = 64

KEY_MAPPER_ALLOWED_NAMESPACES: set[str] = {
    "user",
    "person",
    "pet",
    "place",
    "organization",
    "trip",
    "family",
    "work",
    "health",
    "goal",
}

ALLOWED_ATTRIBUTE_KEYS: set[str] = set()
# Empty by default — the open-catalog path in canonicalization.py accepts any
# well-formed namespace.path.key. Deployers can populate via config.extra_attribute_keys.

ATTRIBUTE_ALIASES: dict[str, str] = {}
# Empty by default — deployers can populate via config.attribute_aliases.


# =============================================================================
# DROP REASON CODES
# =============================================================================

DROP_REASON = Literal[
    "parse_error",
    "missing_facts_key",
    "facts_not_list",
    "unknown_attribute_key",
    "unknown_namespace",
    "below_confidence_threshold",
    "quarantined_proposed_key",
    "dedup_noop",
    "private_mode",
    "gated_out",
    "invalid_fact_structure",
]


# =============================================================================
# EXTRACTION RESULT TYPES
# =============================================================================


@dataclass
class ExtractionResult:
    """Instrumented extraction result for observability."""

    facts: list["FactCandidate"] = field(default_factory=list)
    mode: str = "full"
    input_snippet: str = ""
    raw_json_snippet: str = ""
    parse_ok: bool = True
    parse_error: str | None = None
    drop_reasons: dict[str, int] = field(default_factory=dict)
    completion_tokens: int = 0
    facts_from_llm_count: int = 0

    def add_drop(self, reason: str) -> None:
        self.drop_reasons[reason] = self.drop_reasons.get(reason, 0) + 1


# =============================================================================
# PYDANTIC MODELS — FACT EXTRACTION
# =============================================================================


class FactCandidate(BaseModel):
    entity_type: str = Field("user", description="Entity type: user, person, pet, place, etc.")
    entity_key: str = Field("user:self", description="Entity key like user:self, pet:primary")
    entity_name: str | None = Field(None, description="Display name if known")
    attribute_key: str = Field(..., description="Attribute key")
    value_json: dict = Field(..., description="Structured value")
    value_text: str | None = Field(None, description="Human readable value")
    confidence_level: str | None = Field(None, description="Categorical confidence level")
    confidence: float = Field(0.8, ge=0.0, le=1.0)
    importance: float = Field(0.5, ge=0.0, le=1.0)
    is_sensitive: bool = Field(False)
    needs_confirmation: bool = Field(False)
    evidence: str | None = Field(None, description="Evidence snippet from original message")
    subject: str | None = Field(None, description="Who this fact is about")
    fact_text: str | None = Field(None, description="Fact as natural language sentence")


# =============================================================================
# V2 EXTRACTION SCHEMAS
# =============================================================================


class ExtractedEntity(BaseModel):
    """An entity extracted from a user message."""

    name: str = Field(..., description="Entity name as mentioned")
    type: str = Field(
        ..., description="Entity type (free-form, e.g. person, organization, place, product, event, concept)"
    )
    aliases: list[str] = Field(default_factory=list, description="Alternative names/nicknames for this entity")


class ExtractedFact(BaseModel):
    """A fact extracted as natural language."""

    subject: str = Field(..., description="Who/what the fact is about")
    fact: str = Field(..., description="The fact in natural language")
    confidence: float = Field(0.8, ge=0.0, le=1.0, description="Extraction confidence")


class ExtractedRelation(BaseModel):
    """A relationship between two entities."""

    source: str = Field(..., description="Source entity name")
    target: str = Field(..., description="Target entity name")
    rel_type: str = Field(..., description="Relationship type")


class ExtractionOutput(BaseModel):
    """Complete extraction result from a user message."""

    entities: list[ExtractedEntity] = Field(default_factory=list)
    facts: list[ExtractedFact] = Field(default_factory=list)
    relations: list[ExtractedRelation] = Field(default_factory=list)


# is_high_signal_message() was removed — it's an app-layer concern, not SDK.
# The deployer's application should decide which messages to send to extraction.
