"""Session-scoped goal memory for Avatar follow-up commands (P1-2 MVP)."""

from __future__ import annotations

import json
import re
import threading
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any

from .content_creation_intent import (
    detect_content_creation_intent,
    extract_content_topic,
    extract_literal_post_text,
    infer_content_type,
    is_document_generation_intent,
    sanitize_content_topic,
)
from .website_resolver import extract_target_site, extract_target_site_from_prompt, extract_url_from_prompt, prompt_specifies_site

_GOAL_MEMORY_PATH = Path.home() / ".config" / "iuve-connect-agent" / "goal-memory.json"
_LOCK = threading.Lock()

_FOLLOW_UP_THERE_RE = re.compile(
    r"\b(?:туда|там|положи\s+туда|туда\s+файл|put\s+there|place\s+there)\b",
    re.IGNORECASE,
)
_FOLLOW_UP_MOVE_RE = re.compile(
    r"\b(?:положи|перемести|скопируй|перенеси|move|copy|put)\b.{0,40}\b(?:их|эти|найденн|results?|files?)\b",
    re.IGNORECASE,
)
_FOLLOW_UP_FOLDER_RE = re.compile(
    r"\b(?:в\s+папк|into\s+folder|to\s+folder)\b",
    re.IGNORECASE,
)
_POST_FOLLOW_UP_RE = re.compile(r"\b(?:напиш\w*|write)\s+пост\b", re.IGNORECASE)
_WRONG_TARGET_POST_CORRECTION_RE = re.compile(
    r"(?:"
    r"неправильн\w*|"
    r"не\s+так|"
    r"нужно\s+было|"
    r"а\s+не\s+(?:сообщен|message|dm)|"
    r"это\s+сообщен"
    r")",
    re.IGNORECASE,
)
_FOLLOW_UP_EXECUTE_DOC_RE = re.compile(
    r"\b(?:исполн\w*|execute|go\s+ahead|продолж\w*|создай\s+документ|create\s+document)\b",
    re.IGNORECASE,
)


@dataclass
class GoalMemory:
    current_goal: str = ""
    current_topic: str = ""
    current_target_site: str = ""
    current_output_type: str = ""
    current_files: list[str] = field(default_factory=list)
    last_search_results: list[dict[str, Any]] = field(default_factory=list)
    last_folder: str = ""
    last_created_folder: str = ""
    last_browser_url: str = ""
    active_site: str = ""
    active_composer: str = ""
    last_written_file_path: str = ""
    last_written_file_relative: str = ""
    last_artifact_topic: str = ""
    last_artifact_plan: str = ""
    goal_state: dict[str, Any] | None = None
    outcome_tail: list[dict[str, Any]] = field(default_factory=list)

    def as_dict(self) -> dict[str, Any]:
        out: dict[str, Any] = {
            "current_goal": self.current_goal,
            "current_topic": self.current_topic,
            "current_target_site": self.current_target_site,
            "current_output_type": self.current_output_type,
            "current_files": list(self.current_files),
            "last_search_results": list(self.last_search_results),
            "last_folder": self.last_folder,
            "last_created_folder": self.last_created_folder,
            "last_browser_url": self.last_browser_url,
            "active_site": self.active_site,
            "active_composer": self.active_composer,
            "last_written_file_path": self.last_written_file_path,
            "last_written_file_relative": self.last_written_file_relative,
            "last_artifact_topic": self.last_artifact_topic,
            "last_artifact_plan": self.last_artifact_plan,
            "goal_state": dict(self.goal_state) if isinstance(self.goal_state, dict) else None,
            "outcome_tail": list(self.outcome_tail),
        }
        return out

    @classmethod
    def from_dict(cls, raw: dict[str, Any] | None) -> GoalMemory:
        if not isinstance(raw, dict):
            return cls()
        files = [str(item) for item in (raw.get("current_files") or []) if str(item).strip()]
        results = [
            dict(item)
            for item in (raw.get("last_search_results") or [])
            if isinstance(item, dict)
        ]
        goal_state_raw = raw.get("goal_state")
        goal_state = dict(goal_state_raw) if isinstance(goal_state_raw, dict) else None
        outcome_tail = [
            dict(item)
            for item in (raw.get("outcome_tail") or [])
            if isinstance(item, dict)
        ]
        return cls(
            current_goal=str(raw.get("current_goal") or ""),
            current_topic=str(raw.get("current_topic") or ""),
            current_target_site=str(raw.get("current_target_site") or ""),
            current_output_type=str(raw.get("current_output_type") or ""),
            current_files=files,
            last_search_results=results,
            last_folder=str(raw.get("last_folder") or ""),
            last_created_folder=str(raw.get("last_created_folder") or ""),
            last_browser_url=str(raw.get("last_browser_url") or ""),
            active_site=str(raw.get("active_site") or ""),
            active_composer=str(raw.get("active_composer") or ""),
            last_written_file_path=str(raw.get("last_written_file_path") or ""),
            last_written_file_relative=str(raw.get("last_written_file_relative") or ""),
            last_artifact_topic=str(raw.get("last_artifact_topic") or ""),
            last_artifact_plan=str(raw.get("last_artifact_plan") or ""),
            goal_state=goal_state,
            outcome_tail=outcome_tail,
        )


def goal_memory_path() -> Path:
    return _GOAL_MEMORY_PATH


def _read_disk() -> GoalMemory:
    path = goal_memory_path()
    if not path.is_file():
        return GoalMemory()
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return GoalMemory()
    return GoalMemory.from_dict(raw if isinstance(raw, dict) else {})


def _write_disk(memory: GoalMemory, *, persist: bool) -> None:
    if not persist:
        return
    path = goal_memory_path()
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        payload = memory.as_dict()
        if payload.get("goal_state") is None:
            payload.pop("goal_state", None)
        if not payload.get("outcome_tail"):
            payload.pop("outcome_tail", None)
        path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    except OSError:
        pass
    else:
        try:
            from .runtime_context import update_from_goal_memory

            update_from_goal_memory(memory.as_dict())
        except Exception:
            pass
        try:
            from .orchestra_runtime_sync import upsert_server_goal_memory

            snap = memory.as_dict()
            upsert_server_goal_memory(
                {
                    "entry_key": str(snap.get("current_topic") or snap.get("last_artifact_topic") or "default"),
                    "topic": snap.get("current_topic") or snap.get("last_artifact_topic") or "",
                    "current_goal": snap.get("current_goal") or "",
                    "last_written_file_path": snap.get("last_written_file_path") or "",
                    "last_written_file_relative": snap.get("last_written_file_relative") or "",
                    "active_site": snap.get("active_site") or "",
                    "goal_state": snap.get("goal_state") if isinstance(snap.get("goal_state"), dict) else None,
                    "outcome_tail": snap.get("outcome_tail") if isinstance(snap.get("outcome_tail"), list) else None,
                }
            )
        except Exception:
            pass


def resolve_from_prompt_first(
    text: str,
    *,
    memory: GoalMemory | None = None,
    conversation_context: str = "",
) -> dict[str, str]:
    """
    Merge goal fields with strict priority:
    1. Current prompt
    2. Conversation context
    3. Goal memory
    4. Defaults (empty)
    """
    raw = str(text or "").strip()
    ctx = str(conversation_context or "").strip()
    mem = memory or GoalMemory()

    topic = ""
    target_site = ""
    output_type = ""

    if raw:
        literal = extract_literal_post_text(raw)
        if literal:
            topic = literal
        else:
            topic = sanitize_content_topic(extract_content_topic(raw))
        if is_document_generation_intent(raw):
            output_type = "document"
        else:
            intent = detect_content_creation_intent(raw)
            if intent:
                output_type = str(intent.content_type or "")
            elif infer_content_type(raw):
                output_type = infer_content_type(raw)
        prompt_site = extract_target_site_from_prompt(raw)
        if prompt_site:
            target_site = prompt_site.url

    if not topic:
        topic = sanitize_content_topic(extract_content_topic(ctx))

    if not target_site:
        ctx_site = extract_target_site_from_prompt(ctx) if ctx else None
        if ctx_site:
            target_site = ctx_site.url

    if not target_site:
        target_site = str(mem.current_target_site or mem.active_site or mem.last_browser_url or "").strip()

    if not topic:
        memory_topic = sanitize_content_topic(str(mem.current_topic or mem.last_artifact_topic or ""))
        if memory_topic and not extract_literal_post_text(raw) and not prompt_specifies_site(raw):
            topic = memory_topic

    if not output_type:
        output_type = str(mem.current_output_type or "").strip()

    return {
        "current_topic": topic,
        "current_target_site": target_site,
        "current_output_type": output_type,
    }


class GoalMemoryStore:
    """In-memory goal context with optional disk persistence."""

    def __init__(self) -> None:
        self._memory = GoalMemory()
        self._persist_enabled = True

    def load(self, *, persist: bool | None = None) -> GoalMemory:
        with _LOCK:
            if persist is not None:
                self._persist_enabled = bool(persist)
            self._memory = _read_disk()
            return GoalMemory.from_dict(self._memory.as_dict())

    def snapshot(self) -> GoalMemory:
        with _LOCK:
            return GoalMemory.from_dict(self._memory.as_dict())

    def update(self, **fields: Any) -> GoalMemory:
        with _LOCK:
            data = self._memory.as_dict()
            data.update({key: value for key, value in fields.items() if key in data})
            self._memory = GoalMemory.from_dict(data)
            _write_disk(self._memory, persist=self._persist_enabled)
            return GoalMemory.from_dict(self._memory.as_dict())

    def clear(self) -> None:
        with _LOCK:
            self._memory = GoalMemory()
            _write_disk(self._memory, persist=self._persist_enabled)

    def ingest_prompt(self, text: str, *, conversation_context: str = "") -> GoalMemory:
        """Recompute session fields from prompt-first priority on each new message."""
        resolved = resolve_from_prompt_first(
            text,
            memory=self.snapshot(),
            conversation_context=conversation_context,
        )
        fields: dict[str, Any] = {}
        for key in ("current_topic", "current_target_site", "current_output_type"):
            value = str(resolved.get(key) or "").strip()
            if value:
                fields[key] = value
        prompt_site = extract_target_site_from_prompt(text)
        mem = self.snapshot()
        if prompt_site:
            fields["active_site"] = prompt_site.url
            fields["last_browser_url"] = prompt_site.url
            fields["current_target_site"] = prompt_site.url
            stale_site = str(mem.current_target_site or mem.active_site or mem.last_browser_url or "").strip()
            if stale_site and stale_site != prompt_site.url:
                fields["active_composer"] = ""
        elif prompt_specifies_site(text):
            pass
        goal = str(text or "").strip()
        if goal:
            fields["current_goal"] = goal
        if fields:
            return self.update(**fields)
        return self.snapshot()

    def record_goal(self, goal_summary: str) -> None:
        goal = str(goal_summary or "").strip()
        if goal:
            self.update(current_goal=goal)

    def record_goal_state(self, goal_state: dict[str, Any] | None) -> GoalMemory:
        if isinstance(goal_state, dict) and goal_state:
            return self.update(goal_state=dict(goal_state))
        return self.snapshot()

    def record_outcome(self, outcome: dict[str, Any]) -> GoalMemory:
        if not isinstance(outcome, dict) or not outcome:
            return self.snapshot()
        with _LOCK:
            data = self._memory.as_dict()
            tail = data.get("outcome_tail") if isinstance(data.get("outcome_tail"), list) else []
            tail = [*tail, dict(outcome)]
            if len(tail) > 12:
                tail = tail[-12:]
            data["outcome_tail"] = tail
            self._memory = GoalMemory.from_dict(data)
            _write_disk(self._memory, persist=self._persist_enabled)
            return GoalMemory.from_dict(self._memory.as_dict())

    def latest_outcomes(
        self,
        *,
        task_type: str = "",
        strategy: str = "",
        limit: int = 8,
    ) -> list[dict[str, Any]]:
        mem = self.snapshot()
        tail = list(mem.outcome_tail or [])
        task_key = str(task_type or "").strip().lower()
        strategy_key = str(strategy or "").strip().lower()
        filtered: list[dict[str, Any]] = []
        for item in tail:
            if task_key and str(item.get("task_type") or "").strip().lower() != task_key:
                continue
            if strategy_key and str(item.get("strategy") or "").strip().lower() != strategy_key:
                continue
            filtered.append(dict(item))
        safe_limit = max(1, min(int(limit or 8), 50))
        return filtered[-safe_limit:]

    def record_folder(self, folder_path: str) -> None:
        folder = str(folder_path or "").strip()
        if folder:
            self.update(last_folder=folder)

    def record_created_folder(self, folder_path: str) -> None:
        folder = str(folder_path or "").strip()
        if folder:
            self.update(last_created_folder=folder, last_folder=folder)

    def record_browser_url(self, url: str) -> None:
        safe_url = str(url or "").strip()
        if safe_url:
            self.update(
                last_browser_url=safe_url,
                active_site=safe_url,
                current_target_site=safe_url,
            )

    def record_active_composer(self, composer: str, *, site: str = "") -> None:
        label = str(composer or "").strip()
        if label:
            fields: dict[str, Any] = {"active_composer": label}
            if site:
                fields["active_site"] = site
                fields["current_target_site"] = site
            self.update(**fields)

    def record_search_results(
        self,
        entries: list[dict[str, Any]] | list[str],
        *,
        folder_path: str = "",
    ) -> None:
        normalized: list[dict[str, Any]] = []
        paths: list[str] = []
        for item in entries:
            if isinstance(item, dict):
                path = str(item.get("path") or item.get("name") or "").strip()
                normalized.append(dict(item))
                if path:
                    paths.append(path)
            else:
                text = str(item or "").strip()
                if text:
                    normalized.append({"name": text, "path": text})
                    paths.append(text)
        if not normalized:
            return
        fields: dict[str, Any] = {
            "last_search_results": normalized,
            "current_files": paths,
        }
        if folder_path:
            fields["last_folder"] = folder_path
        self.update(**fields)

    def record_written_file(
        self,
        *,
        absolute_path: str = "",
        relative_path: str = "",
        topic: str = "",
    ) -> None:
        abs_path = str(absolute_path or "").strip()
        rel_path = str(relative_path or "").strip()
        fields: dict[str, Any] = {}
        if abs_path:
            fields["last_written_file_path"] = abs_path
            fields["current_files"] = [abs_path]
        if rel_path:
            fields["last_written_file_relative"] = rel_path
        topic_clean = sanitize_content_topic(str(topic or ""))
        if topic_clean:
            fields["last_artifact_topic"] = topic_clean
            fields["current_topic"] = topic_clean
        if fields:
            self.update(**fields)

    def record_artifact_plan(self, *, goal_summary: str = "", topic: str = "") -> None:
        goal = str(goal_summary or "").strip()
        topic_clean = sanitize_content_topic(str(topic or ""))
        fields: dict[str, Any] = {}
        if goal:
            fields["last_artifact_plan"] = goal
            fields["current_goal"] = goal
        if topic_clean:
            fields["last_artifact_topic"] = topic_clean
            fields["current_topic"] = topic_clean
        if fields:
            self.update(**fields)

    def record_from_plan(self, plan: dict[str, Any] | None) -> None:
        if not isinstance(plan, dict):
            return
        goal = str(plan.get("goal_summary") or "").strip()
        if goal:
            self.record_goal(goal)
        artifact_topic = str(plan.get("last_artifact_topic") or "").strip()
        if artifact_topic or plan.get("compound_intent") == "RESEARCH_THEN_ARTIFACT":
            self.record_artifact_plan(goal_summary=goal, topic=artifact_topic)
        for step in plan.get("steps") or []:
            if not isinstance(step, dict):
                continue
            payload = step.get("payload") or {}
            if not isinstance(payload, dict):
                continue
            folder = str(payload.get("absolute_path") or payload.get("path") or "").strip()
            if folder:
                self.record_folder(folder)
            rel = str(payload.get("relative_path") or payload.get("folder_name") or "").strip()
            if rel and folder:
                self.record_created_folder(folder)
            elif rel:
                self.record_created_folder(rel)
            url = str(payload.get("url") or "").strip()
            if url:
                self.record_browser_url(url)
            active_site = str(payload.get("active_site") or "").strip()
            active_composer = str(payload.get("active_composer") or "").strip()
            if active_composer:
                self.record_active_composer(active_composer, site=active_site or url)

    def record_from_execution(
        self,
        *,
        command_type: str,
        payload: dict[str, Any] | None,
        evidence: dict[str, Any] | None,
    ) -> None:
        ctype = str(command_type or "").strip().lower()
        pay = dict(payload or {})
        ev = dict(evidence or {})
        if ctype in {"browser_open_url", "open_url"}:
            self.record_browser_url(str(pay.get("url") or ev.get("url") or ""))
        if ctype == "keyboard_type_text":
            site = str(pay.get("active_site") or "")
            composer = str(pay.get("active_composer") or pay.get("window_name") or "")
            if composer or site:
                self.record_active_composer(composer, site=site)
        if ctype == "list_files_in_folder":
            folder = str(pay.get("absolute_path") or pay.get("path") or ev.get("folder") or "")
            entries = ev.get("entries") or ev.get("matches") or []
            if isinstance(entries, list) and entries:
                self.record_search_results(entries, folder_path=folder)
            elif folder:
                self.record_folder(folder)
        if ctype == "open_folder":
            folder = str(ev.get("path") or pay.get("absolute_path") or pay.get("path") or "")
            if folder:
                self.record_created_folder(folder)
        if ctype == "write_file_in_workspace":
            abs_path = str(ev.get("path") or "")
            rel_path = str(pay.get("relative_path") or "")
            topic = str(pay.get("artifact_topic") or pay.get("topic") or "")
            self.record_written_file(
                absolute_path=abs_path,
                relative_path=rel_path,
                topic=topic,
            )
        if ctype == "generate_content":
            topic = str(ev.get("topic") or pay.get("topic") or "")
            if topic:
                self.update(current_topic=sanitize_content_topic(topic), last_artifact_topic=sanitize_content_topic(topic))

    def expand_follow_up(self, text: str) -> str:
        raw = str(text or "").strip()
        if not raw:
            return raw
        if prompt_specifies_site(raw) or extract_url_from_prompt(raw):
            return raw
        if re.search(r"\bтам\s+где\b", raw, re.IGNORECASE):
            return raw
        if extract_literal_post_text(raw):
            return raw
        mem = self.snapshot()
        # Wrong UI target correction: DM/message → public feed post (0.2.2.60)
        try:
            from .web_post_correction import build_web_post_correction

            correction = build_web_post_correction(
                raw,
                prior_user_text="",
                memory_site=str(mem.current_target_site or mem.active_site or mem.last_browser_url or ""),
                memory_goal=str(mem.current_goal or ""),
            )
            if correction is not None and (
                mem.current_target_site or mem.active_site or mem.last_browser_url or mem.current_goal
            ):
                return correction.recovery_prompt
        except ImportError:
            pass
        if _FOLLOW_UP_EXECUTE_DOC_RE.search(raw) and (mem.last_artifact_topic or mem.current_topic):
            topic = mem.last_artifact_topic or mem.current_topic
            plan_hint = mem.last_artifact_plan or mem.current_goal
            return (
                f"{raw} — тема документа из предыдущего запроса: {topic}. "
                f"План: {plan_hint or 'досье из интернета'}."
            )
        if _FOLLOW_UP_THERE_RE.search(raw) and mem.last_created_folder:
            target = mem.last_created_folder
            return (
                f"{raw} — целевая папка из предыдущего шага: {target}. "
                f"Создай или положи файл в эту папку. Цель: {mem.current_goal or 'продолжить задачу'}."
            )
        lower = raw.lower()
        try:
            from .libreoffice_document_intent import is_document_article_failure_follow_up

            if is_document_article_failure_follow_up(raw):
                goal = str(mem.current_goal or mem.last_artifact_plan or "").strip()
                if goal and any(
                    token in goal.lower()
                    for token in ("документ", "document", "libreoffice", "writer", "стать")
                ):
                    return f"{goal} — повторить написание статьи в LibreOffice Writer"
        except ImportError:
            pass
        if (
            _POST_FOLLOW_UP_RE.search(lower) or _WRONG_TARGET_POST_CORRECTION_RE.search(lower)
        ) and mem.active_site and not prompt_specifies_site(raw):
            if _WRONG_TARGET_POST_CORRECTION_RE.search(lower):
                return (
                    f"{raw} — перепубликовать как пост на {mem.active_site}, "
                    f"не как личное сообщение. Composer ленты: {mem.active_composer or 'feed'}."
                )
            return (
                f"{raw} — активный сайт из предыдущего шага: {mem.active_site}. "
                f"Composer: {mem.active_composer or 'browser'}."
            )
        if not (_FOLLOW_UP_MOVE_RE.search(raw) or _FOLLOW_UP_FOLDER_RE.search(raw)):
            return raw
        mem = self.snapshot()
        if not mem.last_search_results and not mem.current_files:
            return raw
        file_names = [
            str(item.get("name") or item.get("path") or item)
            for item in mem.last_search_results
            if isinstance(item, dict)
        ] or list(mem.current_files)
        file_names = [name for name in file_names if str(name).strip()][:12]
        if not file_names:
            return raw
        folder_hint = mem.last_folder or "IuVeWorkspace"
        names_text = ", ".join(file_names[:6])
        if len(file_names) > 6:
            names_text = f"{names_text} и ещё {len(file_names) - 6}"
        return (
            f"{raw} — файлы из предыдущего поиска: {names_text}. "
            f"Целевая папка: {folder_hint}. Цель: {mem.current_goal or 'переместить найденные файлы'}."
        )


_STORE: GoalMemoryStore | None = None


def goal_memory_store() -> GoalMemoryStore:
    global _STORE
    if _STORE is None:
        _STORE = GoalMemoryStore()
        _STORE.load()
    return _STORE
