"""Journal closeout — nightly extraction of profile observations.

Runs once per user per day at day_rollover_hour. Reads yesterday's /journal
conversation, filters out assistant-authored auto-content (daily prep),
asks the background LLM to extract user-side patterns/habits, and appends
the bullets to user_profiles.observations_raw via append_observations.
"""
from __future__ import annotations

import datetime
import logging

from sqlalchemy import or_, select

from fabledassistant.config import Config
from fabledassistant.models import async_session
from fabledassistant.models.conversation import Conversation, Message
from fabledassistant.services.llm import generate_completion
from fabledassistant.services.settings import get_setting
from fabledassistant.services.user_profile import append_observations

logger = logging.getLogger(__name__)

# Message kinds whose content must NEVER be sent to the closeout LLM.
# These are assistant-authored auto-blocks that would otherwise dominate
# attention and leak back into "what the assistant has learned."
EXCLUDED_KINDS: set[str] = {"daily_prep"}


def _filter_messages(messages):
    """Drop messages whose msg_metadata.kind is in EXCLUDED_KINDS.

    Accepts any iterable of message-like objects with `role`, `content`,
    and `msg_metadata` attributes (real Message rows or SimpleNamespace).
    """
    kept = []
    for m in messages:
        meta = getattr(m, "msg_metadata", None) or {}
        if meta.get("kind") in EXCLUDED_KINDS:
            continue
        kept.append(m)
    return kept


_TRANSCRIPT_WINDOW = 20
_CONTENT_CAP = 500


def _build_transcript(messages) -> str:
    """Format the last 20 messages as `ROLE: content[:500]` lines."""
    tail = list(messages)[-_TRANSCRIPT_WINDOW:]
    return "\n".join(
        f"{m.role.upper()}: {m.content[:_CONTENT_CAP]}" for m in tail
    )


SYSTEM_PROMPT = (
    "You are reviewing a day's journal conversation to extract preference "
    "observations the USER revealed about themselves.\n\n"
    "Rules:\n"
    "- Only extract patterns, habits, recurring frustrations, or contextual "
    "facts the user said or demonstrated.\n"
    "- DO NOT restate facts that belong in structured fields: name, job title, "
    "industry, expertise level, response style, tone, interests. Those are "
    "handled separately.\n"
    "- DO NOT extract anything from the ASSISTANT turns about the user — only "
    "what the user themselves stated or demonstrated by their choices.\n"
    "- Write 2-5 short bullet points. Be specific and factual.\n"
    "- If nothing notable, output only: (nothing to note)"
)


async def run_for_user(user_id: int, yesterday: datetime.date) -> None:
    """Extract preference observations from yesterday's journal conversation.

    Skips silently when there is nothing meaningful to extract.
    """
    async with async_session() as session:
        conv_result = await session.execute(
            select(Conversation).where(
                Conversation.user_id == user_id,
                Conversation.conversation_type == "journal",
                Conversation.day_date == yesterday,
            )
        )
        conv = conv_result.scalar_one_or_none()
        if conv is None:
            logger.debug("closeout: no journal conv for user %d on %s", user_id, yesterday)
            return

        msg_result = await session.execute(
            select(Message)
            .where(
                Message.conversation_id == conv.id,
                Message.role.in_(("user", "assistant")),
                or_(
                    Message.msg_metadata.is_(None),
                    ~Message.msg_metadata["kind"].astext.in_(EXCLUDED_KINDS),
                ),
            )
            .order_by(Message.created_at)
        )
        messages = list(msg_result.scalars().all())

    # Defensive second-pass filter (covers any message with metadata the
    # SQL JSON path can't reach, e.g. older rows where kind nesting differs).
    messages = _filter_messages(messages)

    if len(messages) < 2:
        logger.debug("closeout: not enough messages for user %d (%d)", user_id, len(messages))
        return

    transcript = _build_transcript(messages)
    model = await get_setting(user_id, "background_model", Config.OLLAMA_BACKGROUND_MODEL)

    try:
        output = (await generate_completion(
            [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": transcript},
            ],
            model,
        )).strip()
    except Exception:
        logger.warning("closeout LLM failed for user %d", user_id, exc_info=True)
        return

    if not output or "(nothing to note)" in output.lower():
        logger.debug("closeout: nothing to note for user %d", user_id)
        return

    await append_observations(user_id, output)
    logger.info("closeout: appended observations for user %d (%s)", user_id, yesterday)
