"""
Session Context Layer — Humanized Stepan v3 / v3.1 / v3.2 / v3.5.

In-memory, per-chat сесійний контекст з TTL 15 хвилин.
Не персистується між рестартами контейнера (це очікувано — сесія коротка).

Структура SessionContext:
  {
    "last_messages":  list[str]  (max 3, найновіші),
    "last_depth":     "light" | "deep" | None,
    "last_agents":    list[str]  (max 5),
    "last_question":  str | None,
    "pending_action": dict | None  — Confirmation Gate (v3.1),
    "doc_facts":      dict | None  — Fact Lock Layer (v3.2):
                        числові факти з документу (profit_uah, area_ha тощо),
                        зберігаються між запитами щоб уникнути RAG-інконсистентності,
    "fact_claims":    list[dict]   — Self-Correction (v3.2):
                        останні 3 твердження агента, напр.
                        [{"key":"profit_present","value":False,"ts":1234}],
    "active_doc_id":  str | None   — Doc Anchor (v3.3):
                        doc_id поточного активного документу;
                        при зміні → скидаємо doc_facts і fact_claims,
    "doc_focus":      bool         — Doc Focus Gate (v3.5):
                        True = документ "приклеєний" до діалогу (активний режим).
                        False = документ є, але не нав'язуємо його контекст.
    "doc_focus_ts":   float        — timestamp активації doc_focus (time.time()),
    "updated_at":     float  (time.time())
  }

doc_focus TTL: DOC_FOCUS_TTL (600 с = 10 хв).
Скидається автоматично при photo/URL/vision-інтенті або вручну через /doc off.

Telemetry:
  AGX_STEPAN_METRIC session_loaded   chat_id=h:...
  AGX_STEPAN_METRIC session_expired  chat_id=h:...
  AGX_STEPAN_METRIC session_updated  chat_id=h:... depth=... agents=...
"""

from __future__ import annotations

import logging
import time
from copy import deepcopy
from typing import Any

from crews.agromatrix_crew.telemetry import tlog

logger = logging.getLogger(__name__)

# TTL 15 хвилин
SESSION_TTL: float = 900.0

# Doc Focus Gate TTL: 10 хвилин після останньої активації
DOC_FOCUS_TTL: float = 600.0

# v3.6: Cooldown після auto-clear — 2 хв блокування implicit doc re-activate
DOC_FOCUS_COOLDOWN_S: float = 120.0

_STORE: dict[str, dict] = {}


def _default_session() -> dict:
    return {
        "last_messages": [],
        "last_depth": None,
        "last_agents": [],
        "last_question": None,
        "pending_action": None,   # v3.1: Confirmation Gate
        "doc_facts": None,        # v3.2: Fact Lock Layer
        "fact_claims": [],        # v3.2: Self-Correction Policy
        "active_doc_id": None,    # v3.3: Doc Anchor Reset
        "doc_focus": False,              # v3.5: Doc Focus Gate
        "doc_focus_ts": 0.0,             # v3.5: timestamp активації doc_focus
        "doc_focus_cooldown_until": 0.0, # v3.6: epoch seconds, 0=inactive
        "last_photo_ts": 0.0,            # v3.5 fix: timestamp останнього фото
        "updated_at": 0.0,
    }


def is_doc_focus_cooldown_active(session: dict, now_ts: float | None = None) -> bool:
    """
    Повертає True якщо cooldown активний (після auto-clear по web/vision домену).
    Поки cooldown — implicit doc re-activate заблокований.
    Fail-safe: будь-яка помилка → False.
    """
    try:
        until = float(session.get("doc_focus_cooldown_until") or 0.0)
        now = now_ts if now_ts is not None else time.time()
        return until > now
    except Exception:
        return False


def is_doc_focus_active(session: dict, now_ts: float | None = None) -> bool:
    """
    Повертає True якщо doc_focus увімкнений і TTL ще не минув.

    Використовується в run.py для вирішення чи підмішувати doc_context в промпт.
    Fail-safe: будь-яка помилка → False.
    """
    try:
        if not session.get("doc_focus"):
            return False
        ts = session.get("doc_focus_ts") or 0.0
        now = now_ts if now_ts is not None else time.time()
        return (now - ts) <= DOC_FOCUS_TTL
    except Exception:
        return False


def load_session(chat_id: str) -> dict:
    """
    Завантажити SessionContext для chat_id.

    - Якщо нема → повернути default (порожній).
    - Якщо протух (now - updated_at > TTL) → очистити, повернути default.
    - Fail-safe: ніяких винятків назовні.
    """
    try:
        if not chat_id:
            return _default_session()

        existing = _STORE.get(chat_id)
        if existing is None:
            tlog(logger, "session_loaded", chat_id=chat_id, status="new")
            return _default_session()

        age = time.time() - existing.get("updated_at", 0.0)
        if age > SESSION_TTL:
            _STORE.pop(chat_id, None)
            tlog(logger, "session_expired", chat_id=chat_id, age_s=round(age))
            return _default_session()

        tlog(logger, "session_loaded", chat_id=chat_id, status="hit",
             last_depth=existing.get("last_depth"))
        return deepcopy(existing)

    except Exception as exc:
        logger.warning("load_session error (returning default): %s", exc)
        return _default_session()


def update_session(
    chat_id: str,
    message: str,
    depth: str,
    agents: list[str] | None = None,
    last_question: str | None = None,
    pending_action: dict | None = None,  # v3.1: Confirmation Gate
    doc_facts: dict | None = None,       # v3.2: Fact Lock
    fact_claims: list | None = None,     # v3.2: Self-Correction
    active_doc_id: str | None = None,    # v3.3: Doc Anchor Reset
    doc_focus: bool | None = None,                  # v3.5: Doc Focus Gate
    doc_focus_ts: float | None = None,              # v3.5: timestamp активації
    doc_focus_cooldown_until: float | None = None,  # v3.6: cooldown epoch
    last_photo_ts: float | None = None,             # v3.5 fix: timestamp фото
) -> None:
    """
    Оновити SessionContext для chat_id.

    - last_messages: append + trim до 3 (зберігає найновіші).
    - last_agents: встановити нові; trim до 5.
    - updated_at: time.time()
    - Fail-safe: не кидає назовні.
    """
    try:
        if not chat_id:
            return

        current = _STORE.get(chat_id) or _default_session()
        session = deepcopy(current)

        # last_messages: append + keep last 3
        msgs: list[str] = session.get("last_messages") or []
        if message:
            msgs.append(message[:500])   # guard against huge messages
        session["last_messages"] = msgs[-3:]

        # depth, agents, question, pending_action
        session["last_depth"] = depth
        new_agents = list(agents or [])[:5]
        session["last_agents"] = new_agents
        session["last_question"] = last_question
        # pending_action: зберігаємо якщо є; якщо None і питання немає — скидаємо
        if pending_action is not None:
            session["pending_action"] = pending_action
        elif not last_question:
            session["pending_action"] = None

        # v3.2: Fact Lock — merge якщо нові факти є
        if doc_facts is not None:
            session["doc_facts"] = doc_facts

        # v3.2: Self-Correction — append новий claim, тримати max 3
        if fact_claims is not None:
            existing_claims: list = session.get("fact_claims") or []
            existing_claims.extend(fact_claims)
            session["fact_claims"] = existing_claims[-3:]

        # v3.3: Doc Anchor — зберегти active_doc_id
        if active_doc_id is not None:
            session["active_doc_id"] = active_doc_id

        # v3.5: Doc Focus Gate
        if doc_focus is not None:
            session["doc_focus"] = doc_focus
        if doc_focus_ts is not None:
            session["doc_focus_ts"] = doc_focus_ts

        # v3.6: Cooldown
        if doc_focus_cooldown_until is not None:
            session["doc_focus_cooldown_until"] = doc_focus_cooldown_until

        # v3.5 fix: Photo timestamp
        if last_photo_ts is not None:
            session["last_photo_ts"] = last_photo_ts

        session["updated_at"] = time.time()

        _STORE[chat_id] = session

        tlog(logger, "session_updated", chat_id=chat_id, depth=depth,
             agents=new_agents)

    except Exception as exc:
        logger.warning("update_session error: %s", exc)


def clear_session(chat_id: str) -> None:
    """Примусово очистити сесію (для тестів та ops-команд)."""
    _STORE.pop(chat_id, None)
