"""Сессии и авто-компакт контекста Viktor.

Viktor держит общий контекст всего DM, поэтому за длинную сессию он
«забивается» и начинает путать задачи/галлюцинировать. Решение: считаем
ходы в сессии и после порога делаем РОТАЦИЮ — просим у Viktor краткое
handoff-summary и стартуем «чистую» сессию, явно велев игнорировать
прежнюю переписку, но передав summary для непрерывности.

Идентичность сессии берём из заголовков OpenCode (x-session-id и т.п.),
иначе — из хэша первого пользовательского сообщения.
"""
from __future__ import annotations

import json
import os
import re
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any


def _fnv1a(text: str) -> str:
    """FNV-1a 32-bit hex — стабильный компактный ключ сессии."""
    h = 2166136261
    for ch in text:
        h ^= ord(ch) & 0xFF
        h = (h * 16777619) & 0xFFFFFFFF
    return f"{h:08x}"


def _compact_content(content: Any) -> str:
    """Текст из message.content (строка или массив частей)."""
    if isinstance(content, str):
        return content
    if content is None:
        return ""
    if isinstance(content, list):
        parts = []
        for p in content:
            if isinstance(p, str):
                parts.append(p)
            elif isinstance(p, dict):
                parts.append(p.get("text", "") if (p.get("type") == "text" or p.get("text")) else "")
        return "\n".join(x for x in parts if x)
    return str(content)


def _norm_session_value(value: Any) -> str:
    if isinstance(value, list):
        return _norm_session_value(value[0]) if value else ""
    text = str(value if value is not None else "").strip()
    if not text or text in ("undefined", "null"):
        return ""
    return text[:500]


# Кандидаты идентичности сессии (заголовки и поля тела запроса).
_HDR_CANDIDATES = (
    "x-session-id", "x-opencode-session-id", "session_id",
    "session-id", "x-amp-thread-id", "x-client-request-id",
)
_BODY_CANDIDATES = (
    ("metadata", "user_id"), ("metadata", "session_id"),
    ("metadata", "conversation_id"), ("metadata", "thread_id"),
    ("session_id",), ("conversation_id",), ("thread_id",),
)


@dataclass(frozen=True)
class SessionInfo:
    hash: str
    source: str
    explicit: bool


def _first_user_text(messages: list[dict[str, Any]]) -> str:
    for m in messages:
        if m.get("role") == "user":
            t = _compact_content(m.get("content")).strip()
            if t:
                return t
    return ""


def extract_session(headers: Any, body: dict[str, Any]) -> SessionInfo:
    """Идентичность сессии: заголовки -> тело(metadata/*) -> fallback (1-е сообщение)."""
    if headers is not None:
        for k in _HDR_CANDIDATES:
            v = _norm_session_value(headers.get(k))
            if v:
                return SessionInfo(_fnv1a(f"header:{k}:{v}"), f"header:{k}", True)
    for path in _BODY_CANDIDATES:
        node: Any = body
        for key in path:
            node = node.get(key) if isinstance(node, dict) else None
        v = _norm_session_value(node)
        if v:
            return SessionInfo(_fnv1a(f"body:{'.'.join(path)}:{v}"), f"body:{'.'.join(path)}", True)
    txt = _first_user_text(body.get("messages") or [])
    if txt:
        return SessionInfo(_fnv1a(f"fallback:first-user:{txt[:4000]}"), "fallback:first-user", False)
    return SessionInfo(_fnv1a("fallback:empty"), "fallback:empty", False)


# Детект служебного запроса «сгенерируй заголовок чата».
_TITLE_RE = re.compile(
    r"^generate\s+(?:a\s+)?(?:short\s+)?title\s+for\s+(?:this\s+)?"
    r"(?:conversation|converstation|converstion|chat|session)\b",
    re.IGNORECASE,
)


def is_title_request(messages: list[dict[str, Any]]) -> bool:
    for m in messages:
        if m.get("role") in ("user", "system"):
            if _TITLE_RE.match(_compact_content(m.get("content")).strip()):
                return True
    return False


def strip_title_prefix(text: str) -> str:
    """Убрать «Generate a short title for this conversation:» из текста."""
    return _TITLE_RE.sub("", (text or "").strip()).lstrip(": ").strip()


# Команды управления git pull для сессии (печатаются пользователем в OpenCode/чате).
_PULL_CMDS = {
    "/nopull": "never", "/pullnever": "never", "/pull-never": "never",
    "/pullask": "ask", "/pull-ask": "ask", "/askpull": "ask",
    "/pullauto": "auto", "/pull-auto": "auto", "/autopull": "auto",
}
_PULL_PREF_MSG = {
    "never": "🚫 git pull для этой сессии отключён — больше не спрашиваю. "
             "Вернуть вопрос: /pullask, тянуть всегда: /pullauto.",
    "ask": "🔄 Буду спрашивать про git pull после каждого ответа Viktor'а. "
           "Отключить: /nopull, тянуть всегда: /pullauto.",
    "auto": "⏩ git pull для этой сессии будет выполняться автоматически. "
            "Спрашивать: /pullask, отключить: /nopull.",
}


def pull_command(text: str) -> str | None:
    """Если текст — команда управления pull, вернуть режим (never/ask/auto)."""
    return _PULL_CMDS.get((text or "").strip().lower())


def pull_pref_message(pref: str) -> str:
    return _PULL_PREF_MSG.get(pref, "")


def make_title(text: str) -> str:
    """Короткий заголовок из текста (порт makeTitle)."""
    cleaned = re.sub(r"[^\w\s.,!?-]", "", text, flags=re.UNICODE).strip()
    words = [w for w in cleaned.split() if w][:8]
    return " ".join(words) or "Conversation"


@dataclass
class SessionState:
    turns: int = 0
    created: float = 0.0
    last: float = 0.0


class SessionStore:
    """Простое JSON-хранилище счётчиков ходов по сессиям."""

    def __init__(self, path: str, now: float | None = None) -> None:
        self.path = Path(path)
        self.data: dict[str, dict] = {}
        if self.path.exists():
            try:
                self.data = json.loads(self.path.read_text(encoding="utf-8"))
            except Exception:  # noqa: BLE001
                self.data = {}

    def turns(self, key: str) -> int:
        return int(self.data.get(key, {}).get("turns", 0))

    def folder(self, key: str) -> str:
        return str(self.data.get(key, {}).get("path", ""))

    def root(self, key: str) -> str:
        """ts корневого сообщения треда сессии (= разговор у Viktor)."""
        return str(self.data.get(key, {}).get("root", ""))

    def pull_pref(self, key: str) -> str:
        """Предпочтение git pull для сессии: ask | auto | never."""
        return str(self.data.get(key, {}).get("pull", "ask"))

    def account(self, key: str) -> str:
        """Имя аккаунта Viktor, использованного в этой сессии в прошлый ход."""
        return str(self.data.get(key, {}).get("acct", ""))

    def set_account(self, key: str, name: str, ts: float) -> None:
        st = self.data.setdefault(key, {"turns": 0, "created": ts, "last": ts, "path": "", "root": ""})
        st["acct"] = name
        self._save()

    def set_pull_pref(self, key: str, val: str, ts: float) -> None:
        st = self.data.setdefault(key, {"turns": 0, "created": ts, "last": ts, "path": "", "root": ""})
        st["pull"] = val
        self._save()

    def bump(self, key: str, ts: float, path: str = "", root: str = "") -> int:
        st = self.data.setdefault(key, {"turns": 0, "created": ts, "last": ts, "path": path, "root": root})
        st["turns"] = int(st.get("turns", 0)) + 1
        st["last"] = ts
        if path:
            st["path"] = path
        if root:
            st["root"] = root
        self._save()
        return st["turns"]

    def reset(self, key: str, ts: float, path: str = "") -> None:
        """Сброс сессии: счётчик 0 и пустой корень (новый тред заведётся при посте).
        Предпочтение git pull сохраняем — это пользовательская настройка сессии."""
        pref = self.data.get(key, {}).get("pull", "ask")
        self.data[key] = {"turns": 0, "created": ts, "last": ts, "path": path, "root": "", "pull": pref}
        self._save()

    def _save(self) -> None:
        # держим не больше 200 последних сессий
        if len(self.data) > 200:
            items = sorted(self.data.items(), key=lambda kv: kv[1].get("last", 0))
            self.data = dict(items[-200:])
        tmp = self.path.with_suffix(".json.tmp")
        try:
            tmp.write_text(json.dumps(self.data, ensure_ascii=False), encoding="utf-8")
            os.replace(tmp, self.path)
        except Exception:  # noqa: BLE001
            pass


# Промпт для handoff-summary при ротации сессии.
HANDOFF_PROMPT = (
    "Напиши краткое HANDOFF SUMMARY этой сессии, чтобы новый ассистент мог "
    "продолжить без потери контекста. Обычный текст, без вступлений. Включи:\n"
    "- общую цель / что строим\n"
    "- ключевые решения и ограничения\n"
    "- созданные/изменённые файлы (с путями)\n"
    "- текущее состояние и что осталось\n"
    "- ближайший следующий шаг"
)


def new_session_preamble() -> str:
    """Лёгкая заметка для нового треда (тред сам даёт чистый контекст; просим
    не подтягивать историю прошлых тредов поиском по логам)."""
    return (
        "🆕 Новая независимая задача (новый тред). Не подтягивай контекст из других "
        "тредов/прошлых задач и не ищи их в истории — работай только с этой ветки.\n\n"
    )


def compact_preamble(summary: str) -> str:
    """Ротация: стартуем чистую сессию, сохранив непрерывность через summary."""
    head = (
        "🔄 НОВАЯ СЕССИЯ. Полностью забудь детальный контекст предыдущих сообщений "
        "в этом чате и не ссылайся на них — начинаем с чистого листа."
    )
    if summary:
        head += (
            "\n\nРЕЗЮМЕ ПРЕДЫДУЩЕЙ СЕССИИ (продолжай бесшовно, не начинай заново):\n"
            + summary
        )
    return head + "\n\n"
