"""
cortex/peers/manager.py — PeerManager (M15).

Manages interaction partners (peers): auto-creation from entity mentions,
interaction counting, and building structured trait representations.

Design decisions:
    - get_or_create_peer is idempotent: looks up by entity_id first, then by
      display_name within the owner scope, then creates if not found.
    - build_representation extracts four dimensions from claim text:
        communication_style, topics_of_interest, preferences,
        relationship_dynamics.
    - No LLM dependency for the simple extractor path (keyword heuristics);
      if an LLM provider is supplied, richer extraction is available.
    - All mutations go through StorageAdapter — no direct SQL here.
"""
from __future__ import annotations

import json
import logging
import re
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional

from cortex.types import Peer

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="milliseconds")


# ---------------------------------------------------------------------------
# PeerManager
# ---------------------------------------------------------------------------


class PeerManager:
    """High-level API for peer lifecycle and representation building.

    Args:
        storage: A StorageAdapter instance (must support peer CRUD methods).
        entity_resolver: Optional EntityResolver — used to resolve entity IDs
            when auto-creating peers from entity mentions.
        llm_provider: Optional LLMProvider — enables richer representation
            building. Falls back to keyword heuristics when not supplied.
        auto_create_threshold: Minimum entity mention_count before a peer is
            automatically created. Default 3 (configurable via config).
    """

    def __init__(
        self,
        storage: Any,
        entity_resolver: Any = None,
        llm_provider: Any = None,
        auto_create_threshold: int = 3,
    ) -> None:
        self.storage = storage
        self.entity_resolver = entity_resolver
        self.llm_provider = llm_provider
        self.auto_create_threshold = auto_create_threshold

    # ------------------------------------------------------------------
    # Core CRUD
    # ------------------------------------------------------------------

    async def get_or_create_peer(
        self,
        owner_id: str,
        entity_name: str,
        entity_type: str = "person",
        relationship: str = "unknown",
    ) -> Peer:
        """Return an existing peer or create a new one (idempotent).

        Resolution order:
          1. If entity_resolver available: resolve entity → look up by entity_id.
          2. Fall back to name-based lookup within owner scope.
          3. Create if not found.

        Args:
            owner_id: Scoping owner identifier.
            entity_name: Canonical or display name of the person.
            entity_type: Entity type — only 'person' peers are auto-created.
            relationship: Initial relationship label.

        Returns:
            Peer dataclass (existing or newly created).
        """
        # Validate entity_name
        entity_name = entity_name.strip() if entity_name else ""
        if not entity_name:
            raise ValueError(
                "entity_name must not be empty or whitespace-only"
            )
        if len(entity_name) > 255:
            raise ValueError(
                f"entity_name must be 255 characters or fewer, got {len(entity_name)}"
            )

        entity_id: Optional[str] = None

        # 1. Try to resolve entity_id via EntityResolver
        if self.entity_resolver is not None:
            try:
                resolved = await self.entity_resolver.resolve(
                    mention=entity_name,
                    owner_id=owner_id,
                )
                if resolved and resolved.entity_id:
                    entity_id = resolved.entity_id
            except Exception as exc:
                logger.warning("Peer entity resolution failed for '%s': %s", entity_name, exc)

        # 2. Look up existing peer by entity_id
        if entity_id and hasattr(self.storage, "get_peer_by_entity"):
            try:
                existing = await self.storage.get_peer_by_entity(entity_id)
                if existing:
                    return existing
            except Exception as exc:
                logger.warning("get_peer_by_entity(%s) failed: %s", entity_id, exc)

        # 3. Look up existing peer by display_name within owner scope
        try:
            all_peers = await self.storage.list_peers(owner_id=owner_id)
            for peer in all_peers:
                if peer.display_name.lower() == entity_name.lower():
                    # Back-fill entity_id if we resolved one
                    if entity_id and not peer.entity_id:
                        await self.storage.update_peer(
                            peer.id, entity_id=entity_id, updated_at=_now()
                        )
                    return peer
        except Exception as exc:
            logger.warning("list_peers failed during get_or_create: %s", exc)

        # 4. Create new peer
        now = _now()
        peer = await self.storage.create_peer(
            owner_id=owner_id,
            display_name=entity_name,
            entity_id=entity_id,
            relationship=relationship,
            first_interaction=now,
            last_interaction=now,
            interaction_count=1,
            representation="{}",
            created_at=now,
            updated_at=now,
        )
        logger.info("Created new peer '%s' (id=%s, owner=%s)", entity_name, peer.id, owner_id)
        return peer

    async def get_peer(self, peer_id: str) -> Optional[Peer]:
        """Fetch a peer by its primary key."""
        return await self.storage.get_peer(peer_id)

    async def list_peers(self, owner_id: str) -> List[Peer]:
        """List all peers for an owner."""
        return await self.storage.list_peers(owner_id=owner_id)

    async def update_peer(self, peer_id: str, **kwargs: Any) -> Peer:
        """Update arbitrary peer fields. updated_at is auto-set."""
        kwargs.setdefault("updated_at", _now())
        return await self.storage.update_peer(peer_id, **kwargs)

    async def delete_peer(self, peer_id: str) -> None:
        """Delete a peer record."""
        await self.storage.delete_peer(peer_id)

    # ------------------------------------------------------------------
    # Active peer discovery
    # ------------------------------------------------------------------

    async def get_active_peers(
        self,
        owner_id: str,
        days: int = 7,
        limit: int = 20,
    ) -> List[Dict[str, Any]]:
        """Return peers that have recent claim activity.

        Delegates to storage.get_active_peer_ids() to find peers referenced
        in claims created within the last *days* days, then fetches full peer
        records and parses their representations.

        Args:
            owner_id: Scoping owner identifier.
            days: Look-back window in days (default 7).
            limit: Maximum number of peers to return (default 20).

        Returns:
            List of dicts with keys: peer_id, display_name, entity_id,
            relationship, representation (parsed dict), last_interaction.
        """
        since_dt = datetime.now(timezone.utc) - timedelta(days=days)

        # Use storage query if available, else fall back to listing all peers
        if hasattr(self.storage, "get_active_peer_ids"):
            peer_ids = await self.storage.get_active_peer_ids(
                owner_id=owner_id,
                since_dt=since_dt,
                limit=limit,
            )
        else:
            # Fallback: return all peers (less efficient but functional)
            all_peers = await self.storage.list_peers(owner_id=owner_id)
            peer_ids = [p.id for p in all_peers[:limit]]

        results: List[Dict[str, Any]] = []
        for pid in peer_ids:
            peer = await self.storage.get_peer(pid)
            if peer is None:
                continue
            try:
                rep = json.loads(peer.representation or "{}")
            except (json.JSONDecodeError, TypeError):
                rep = {}
            results.append({
                "peer_id": peer.id,
                "display_name": peer.display_name,
                "entity_id": peer.entity_id,
                "relationship": peer.relationship,
                "representation": rep,
                "last_interaction": peer.last_interaction,
            })

        return results

    # ------------------------------------------------------------------
    # Interaction tracking
    # ------------------------------------------------------------------

    async def update_interaction(
        self,
        peer_id: str,
        session_id: Optional[str] = None,
    ) -> Peer:
        """Bump interaction_count and last_interaction timestamp.

        Args:
            peer_id: Peer to update.
            session_id: Optional session context (stored for traceability).

        Returns:
            Updated Peer.
        """
        existing = await self.storage.get_peer(peer_id)
        if existing is None:
            raise ValueError(f"Peer not found: {peer_id}")

        now = _now()
        new_count = existing.interaction_count + 1
        updated = await self.storage.update_peer(
            peer_id,
            interaction_count=new_count,
            last_interaction=now,
            updated_at=now,
        )
        logger.debug(
            "Peer %s interaction updated: count=%d session=%s",
            peer_id,
            new_count,
            session_id,
        )
        return updated

    # ------------------------------------------------------------------
    # Representation building
    # ------------------------------------------------------------------

    async def build_representation(
        self,
        peer_id: str,
        claims: List[Any],
    ) -> Dict[str, Any]:
        """Build a structured trait representation from claims about this peer.

        Extracts four dimensions:
          - communication_style: tone, formality, directness cues
          - topics_of_interest: recurring subjects and domains
          - preferences: likes, dislikes, stated preferences
          - relationship_dynamics: interaction patterns, reciprocity, sentiment

        The representation is persisted to peer.representation (JSON) and
        returned as a dict.

        Args:
            peer_id: Target peer.
            claims: List of SemanticClaim (or duck-typed objects with a
                ``predicate`` and ``object_value`` or ``content`` attribute).

        Returns:
            Structured representation dict.
        """
        peer = await self.storage.get_peer(peer_id)
        if peer is None:
            raise ValueError(f"Peer not found: {peer_id}")

        # Merge with existing representation
        try:
            existing_rep: Dict[str, Any] = json.loads(peer.representation or "{}")
        except (json.JSONDecodeError, TypeError):
            existing_rep = {}

        rep = {
            "communication_style": existing_rep.get("communication_style", []),
            "topics_of_interest": existing_rep.get("topics_of_interest", []),
            "preferences": existing_rep.get("preferences", []),
            "relationship_dynamics": existing_rep.get("relationship_dynamics", []),
        }

        # Extract traits from claims
        new_traits = self._extract_traits_from_claims(claims)

        # Merge: deduplicate by lowercased value
        for dimension, new_values in new_traits.items():
            existing_lower = {v.lower() for v in rep[dimension]}
            for val in new_values:
                if val.lower() not in existing_lower:
                    rep[dimension].append(val)
                    existing_lower.add(val.lower())

        rep["last_rebuilt"] = _now()
        rep["source_claim_count"] = len(claims)

        # Persist
        await self.storage.update_peer(
            peer_id,
            representation=json.dumps(rep),
            updated_at=_now(),
        )
        logger.info("Built representation for peer %s: %d claims processed", peer_id, len(claims))
        return rep

    # ------------------------------------------------------------------
    # Auto-create pipeline integration
    # ------------------------------------------------------------------

    async def auto_create_from_entities(
        self,
        owner_id: str,
        entities: List[Any],
    ) -> List[Peer]:
        """Auto-create peers for person entities that exceed the mention threshold.

        Called by the extraction pipeline after entity resolution.

        Args:
            owner_id: Scoping owner.
            entities: List of Entity objects (must have entity_type, canonical_name,
                mention_count, id).

        Returns:
            List of newly created (or existing) Peers.
        """
        created: List[Peer] = []
        for entity in entities:
            if (
                getattr(entity, "entity_type", "") == "person"
                and getattr(entity, "mention_count", 0) >= self.auto_create_threshold
            ):
                try:
                    peer = await self.get_or_create_peer(
                        owner_id=owner_id,
                        entity_name=entity.canonical_name,
                        entity_type="person",
                    )
                    created.append(peer)
                except Exception as exc:
                    logger.warning(
                        "auto_create_from_entities: failed for '%s': %s",
                        getattr(entity, "canonical_name", "?"),
                        exc,
                    )
        return created

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _extract_traits_from_claims(self, claims: List[Any]) -> Dict[str, List[str]]:
        """Keyword-heuristic trait extractor.

        Inspects claim predicate + object_value text for trait signals.
        """
        result: Dict[str, List[str]] = {
            "communication_style": [],
            "topics_of_interest": [],
            "preferences": [],
            "relationship_dynamics": [],
        }

        _COMM_STYLE = re.compile(
            r"\b(formal|informal|casual|direct|indirect|verbose|concise|"
            r"technical|simple|humorous|serious|friendly|reserved|talkative|quiet)\b",
            re.I,
        )
        _PREF_VERBS = re.compile(
            r"\b(prefer|like|love|enjoy|dislike|hate|avoid|favour|favor|want|need)\b",
            re.I,
        )
        _RELATIONSHIP = re.compile(
            r"\b(collaborat|trust|respect|close|distant|mentor|peer|friend|"
            r"collea|partner|rival|acquaint)\w*\b",
            re.I,
        )

        for claim in claims:
            predicate = getattr(claim, "predicate", "") or ""
            obj = getattr(claim, "object_value", "") or getattr(claim, "content", "") or ""
            text = f"{predicate} {obj}".strip()
            if not text:
                continue

            # communication_style
            for m in _COMM_STYLE.findall(text):
                result["communication_style"].append(m.lower())

            # preferences (verb + object phrase)
            if _PREF_VERBS.search(text):
                snippet = re.sub(r"\s+", " ", obj)[:80].strip()
                if snippet:
                    result["preferences"].append(snippet)

            # topics_of_interest — predicate signals subject domain
            topic_match = re.search(
                r"\b(interest(?:ed)? in|talks? about|discuss|mention|knows? about|"
                r"studies|works? on|expert in|focus(?:es)? on)\b (.+)",
                text,
                re.I,
            )
            if topic_match:
                topic = re.sub(r"\s+", " ", topic_match.group(2))[:60].strip()
                if topic:
                    result["topics_of_interest"].append(topic)

            # relationship_dynamics
            for m in _RELATIONSHIP.findall(text):
                result["relationship_dynamics"].append(m.lower())

        # Deduplicate within each dimension
        for key in result:
            seen: set = set()
            deduped = []
            for v in result[key]:
                if v.lower() not in seen:
                    seen.add(v.lower())
                    deduped.append(v)
            result[key] = deduped

        return result
