"""Emotional Intelligence — trend analysis and insights.

Materializes emotional trends from memory events and provides
formatted summaries for injection into retrieval context.
"""

from __future__ import annotations

import logging
import re
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from typing import Any

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from arandu.config import MemoryConfig
from arandu.models import MemoryEvent, MemoryMetaObservation

logger = logging.getLogger(__name__)

__all__ = [
    "EmotionalTrendsResult",
    "get_emotional_summary_for_context",
    "materialize_emotional_trends",
]

# ---------------------------------------------------------------------------
# Common stopwords (EN + PT + ES — language-agnostic keyword extraction)
# ---------------------------------------------------------------------------

COMMON_STOPWORDS: frozenset[str] = frozenset(
    {
        # English
        "the",
        "a",
        "an",
        "is",
        "are",
        "was",
        "and",
        "or",
        "but",
        "in",
        "on",
        "at",
        "to",
        "for",
        "of",
        "with",
        "not",
        "this",
        "that",
        "it",
        "he",
        "she",
        "we",
        "you",
        "they",
        "my",
        "his",
        "her",
        "our",
        # Portuguese
        "que",
        "de",
        "para",
        "com",
        "não",
        "uma",
        "um",
        "os",
        "as",
        "do",
        "da",
        "no",
        "na",
        "em",
        "por",
        "se",
        "eu",
        "meu",
        "minha",
        "ele",
        "ela",
        "nos",
        "você",
        "isso",
        "esse",
        "essa",
        "aqui",
        "também",
        "mas",
        "ou",
        # Spanish
        "el",
        "la",
        "los",
        "las",
        "en",
        "con",
        "del",
        "una",
        "su",
        "es",
        "al",
        "lo",
        "como",
        "más",
        "pero",
        "sus",
        "entre",
    }
)


# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------


@dataclass
class EmotionalTrendsResult:
    """Result of emotional trend materialization.

    Attributes:
        emotion_counts: Mapping of emotion → occurrence count.
        trend_direction: One of ``"increasing"``, ``"decreasing"``, ``"stable"``.
        dominant_emotion: Most frequent emotion, or None if insufficient data.
        trigger_keywords: Top keywords from high-intensity events.
        avg_intensity: Average emotion intensity across events.
        dominant_intensity: Average intensity of the dominant emotion.
        dominant_energy: Predominant energy level (high/medium/low).
        events_analyzed: Number of events analyzed.
        observation_created: Whether a meta-observation was created/updated.
        observation_id: ID of the created/updated observation, if any.
    """

    emotion_counts: dict[str, int] = field(default_factory=dict)
    trend_direction: str = "stable"
    dominant_emotion: str | None = None
    trigger_keywords: list[str] = field(default_factory=list)
    avg_intensity: float = 0.0
    dominant_intensity: float = 0.0
    dominant_energy: str = "medium"
    events_analyzed: int = 0
    observation_created: bool = False
    observation_id: str | None = None


# ---------------------------------------------------------------------------
# Keyword extraction
# ---------------------------------------------------------------------------


def _extract_keywords(text: str, top_n: int = 5) -> list[str]:
    """Extract top N most frequent words (excluding common stopwords)."""
    if not text or not text.strip():
        return []
    text_lower = text.lower()
    words = re.findall(r"\b[a-zàâãáéêíóôõúç]{3,}\b", text_lower)
    counts: dict[str, int] = {}
    for w in words:
        if w not in COMMON_STOPWORDS:
            counts[w] = counts.get(w, 0) + 1
    sorted_words = sorted(counts.items(), key=lambda x: (-x[1], x[0]))
    return [w for w, _ in sorted_words[:top_n]]


# ---------------------------------------------------------------------------
# Core materialization
# ---------------------------------------------------------------------------


async def materialize_emotional_trends(
    session: AsyncSession,
    user_id: str,
    config: MemoryConfig,
) -> EmotionalTrendsResult:
    """Aggregate emotion data from events, detect trends, materialize as meta-observation.

    Queries ``MemoryEvent`` records within the configured time window,
    aggregates emotion counts, detects trend direction, extracts trigger
    keywords, and creates/updates a ``MemoryMetaObservation``.

    Args:
        session: Database session.
        user_id: User identifier.
        config: Memory configuration with trend window and min events.

    Returns:
        EmotionalTrendsResult with aggregated trend data.
    """
    window_days = config.emotional_trend_window_days
    min_events = config.emotional_trend_min_events
    since = datetime.now(UTC) - timedelta(days=window_days)

    stmt = (
        select(MemoryEvent)
        .where(
            MemoryEvent.user_id == user_id,
            MemoryEvent.occurred_at >= since,
            MemoryEvent.emotion_primary.isnot(None),
        )
        .order_by(MemoryEvent.occurred_at.asc())
    )
    result = await session.execute(stmt)
    events = list(result.scalars().all())

    if len(events) < min_events:
        return EmotionalTrendsResult(
            events_analyzed=len(events),
        )

    # Aggregate by emotion
    emotion_counts: dict[str, int] = {}
    emotion_intensities: dict[str, list[float]] = {}
    energy_counts: dict[str, int] = {"high": 0, "medium": 0, "low": 0}

    for e in events:
        emo = (e.emotion_primary or "neutral").strip().lower()
        emotion_counts[emo] = emotion_counts.get(emo, 0) + 1
        if e.emotion_intensity is not None:
            emotion_intensities.setdefault(emo, []).append(e.emotion_intensity)
        if e.energy_level and e.energy_level.lower() in energy_counts:
            energy_counts[e.energy_level.lower()] += 1

    avg_intensity_per_emotion = {emo: sum(vals) / len(vals) for emo, vals in emotion_intensities.items() if vals}
    dominant_emotion = (
        max(
            emotion_counts,
            key=lambda em: (emotion_counts[em], avg_intensity_per_emotion.get(em, 0.0)),
        )
        if emotion_counts
        else "neutral"
    )

    all_intensities = [e.emotion_intensity for e in events if e.emotion_intensity is not None]
    avg_intensity = sum(all_intensities) / len(all_intensities) if all_intensities else 0.0
    dominant_intensity = (
        sum(emotion_intensities.get(dominant_emotion, [])) / len(emotion_intensities[dominant_emotion])
        if emotion_intensities.get(dominant_emotion)
        else avg_intensity
    )

    dominant_energy = max(energy_counts, key=lambda k: energy_counts[k]) if energy_counts else "medium"

    # Trend direction: 1st half vs 2nd half
    mid = len(events) // 2
    first_half = events[:mid] if mid > 0 else []
    second_half = events[mid:]

    def count_dominant(evs: list[Any]) -> int:
        return sum(1 for ev in evs if (ev.emotion_primary or "").strip().lower() == dominant_emotion)

    count_first = count_dominant(first_half)
    count_second = count_dominant(second_half)

    if count_second > count_first:
        trend_direction = "increasing"
    elif count_second < count_first:
        trend_direction = "decreasing"
    else:
        trend_direction = "stable"

    # Trigger keywords from high-intensity events (>= 0.7)
    high_intensity = [e for e in events if (e.emotion_intensity or 0) >= 0.7 and (e.text or "").strip()]
    keyword_counts: dict[str, int] = {}
    for e in high_intensity:
        for kw in _extract_keywords(e.text or "", top_n=5):
            keyword_counts[kw] = keyword_counts.get(kw, 0) + 1
    trigger_keywords = [w for w, _ in sorted(keyword_counts.items(), key=lambda x: (-x[1], x[0]))[:3]]

    # Idempotency: check existing observation
    obs_stmt = select(MemoryMetaObservation).where(
        MemoryMetaObservation.user_id == user_id,
        MemoryMetaObservation.observation_type == "emotional_trend",
        MemoryMetaObservation.is_active.is_(True),
    )
    obs_result = await session.execute(obs_stmt)
    existing = obs_result.scalar_one_or_none()

    now = datetime.now(UTC)
    twenty_four_h = timedelta(hours=24)

    text = (
        f'Emotional trend ({window_days} days): dominant emotion "{dominant_emotion}" '
        f"({emotion_counts.get(dominant_emotion, 0)}x, avg intensity {dominant_intensity:.1f}). "
        f"Direction: {trend_direction}. Predominant energy: {dominant_energy}. "
        f"Associated keywords: {', '.join(trigger_keywords) if trigger_keywords else '—'}."
    )

    # Confidence based on coverage
    total_in_window_stmt = (
        select(func.count())
        .select_from(MemoryEvent)
        .where(
            MemoryEvent.user_id == user_id,
            MemoryEvent.occurred_at >= since,
        )
    )
    total_result = await session.execute(total_in_window_stmt)
    total_events_in_window = total_result.scalar_one()
    confidence = min(0.9, len(events) / max(1, total_events_in_window))

    supporting_ids = [str(e.id) for e in events if (e.emotion_primary or "").strip().lower() == dominant_emotion][:10]

    observation_created = False
    observation_id: str | None = None

    if existing:
        observation_id = str(existing.id)
        reinforced_at = existing.last_reinforced_at
        if reinforced_at is not None:
            if reinforced_at.tzinfo is None:
                reinforced_at = reinforced_at.replace(tzinfo=UTC)
            if (now - reinforced_at) < twenty_four_h:
                # Reinforce existing
                existing.text = text
                existing.confidence = confidence
                existing.last_reinforced_at = now
                existing.times_reinforced = (existing.times_reinforced or 1) + 1
                existing.supporting_event_ids = supporting_ids
                observation_created = True
            else:
                # Deactivate old, create new
                existing.is_active = False
                await session.flush()
                new_obs = _create_observation(user_id, dominant_emotion, text, supporting_ids, confidence)
                session.add(new_obs)
                await session.flush()
                observation_id = str(new_obs.id)
                observation_created = True
        else:
            # No reinforcement timestamp, create new
            existing.is_active = False
            await session.flush()
            new_obs = _create_observation(user_id, dominant_emotion, text, supporting_ids, confidence)
            session.add(new_obs)
            await session.flush()
            observation_id = str(new_obs.id)
            observation_created = True
    else:
        new_obs = _create_observation(user_id, dominant_emotion, text, supporting_ids, confidence)
        session.add(new_obs)
        await session.flush()
        observation_id = str(new_obs.id)
        observation_created = True

    logger.info(
        "emotional_trends: user=%s, events=%d, dominant=%s, direction=%s",
        user_id,
        len(events),
        dominant_emotion,
        trend_direction,
    )

    return EmotionalTrendsResult(
        emotion_counts=emotion_counts,
        trend_direction=trend_direction,
        dominant_emotion=dominant_emotion,
        trigger_keywords=trigger_keywords,
        avg_intensity=avg_intensity,
        dominant_intensity=dominant_intensity,
        dominant_energy=dominant_energy,
        events_analyzed=len(events),
        observation_created=observation_created,
        observation_id=observation_id,
    )


def _create_observation(
    user_id: str,
    dominant_emotion: str,
    text: str,
    supporting_ids: list[str],
    confidence: float,
) -> MemoryMetaObservation:
    """Create a new emotional trend meta-observation."""
    return MemoryMetaObservation(
        id=uuid.uuid4(),
        user_id=user_id,
        observation_type="emotional_trend",
        title=f"Emotional trend: {dominant_emotion}",
        text=text,
        supporting_event_ids=supporting_ids,
        supporting_fact_ids=[],
        confidence=confidence,
        importance=0.6,
        is_active=True,
    )


# ---------------------------------------------------------------------------
# Parsing helpers
# ---------------------------------------------------------------------------


def _parse_trend_meta(obs: Any) -> tuple[str, str]:
    """Extract dominant_emotion and trend_direction from observation title/text."""
    dominant = ""
    direction = ""
    title = getattr(obs, "title", "") or ""
    text = getattr(obs, "text", "") or ""
    if ":" in title:
        dominant = title.split(":", 1)[-1].strip()
    if "Direction:" in text:
        m = re.search(r"Direction:\s*(\w+)", text)
        if m:
            direction = m.group(1).strip()
    return dominant, direction


# ---------------------------------------------------------------------------
# Context injection
# ---------------------------------------------------------------------------


async def get_emotional_summary_for_context(
    session: AsyncSession,
    user_id: str,
) -> str | None:
    """Return formatted emotional summary for injection into retrieval context.

    Returns None if no recent (7-day) active emotional trend exists.

    Args:
        session: Database session.
        user_id: User identifier.

    Returns:
        Formatted summary string, or None.
    """
    stmt = select(MemoryMetaObservation).where(
        MemoryMetaObservation.user_id == user_id,
        MemoryMetaObservation.observation_type == "emotional_trend",
        MemoryMetaObservation.is_active.is_(True),
    )
    result = await session.execute(stmt)
    obs = result.scalar_one_or_none()
    if not obs or not obs.last_reinforced_at:
        return None

    obs_time = obs.last_reinforced_at
    if obs_time.tzinfo is None:
        obs_time = obs_time.replace(tzinfo=UTC)
    if datetime.now(UTC) - obs_time > timedelta(days=7):
        return None

    return obs.text
