"""Work out what to build, before any of it becomes a task.

This is the one place agents speak in prose, and it is safe precisely because nothing here
advances a state machine: a discussion changes no files, runs no commands, and holds no
worktree. When the discussion is settled it is converted back into a validated artifact,
and only that artifact reaches the pipeline.
"""

from __future__ import annotations

import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal
from uuid import uuid4

from pydantic import Field

from .domain import AgentTuning, StrictModel, VersionedArtifact

Voice = Literal["operator", "codex", "claude"]

#: Names the UI shows, and the labels used when replaying history to an agent.
LABELS = {"operator": "用户", "codex": "Codex", "claude": "Claude"}


class Message(VersionedArtifact):
    voice: Voice
    text: str
    at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class Brief(VersionedArtifact):
    """The settled outcome of a discussion, in the shape a task needs."""

    goal: str = Field(min_length=1)
    constraints: list[str] = Field(default_factory=list)
    acceptance_criteria: list[str] = Field(default_factory=list)
    open_questions: list[str] = Field(default_factory=list)


class DiscussionRecord(VersionedArtifact):
    discussion_id: str
    repo_path: Path
    title: str = ""
    brief: Brief | None = None
    task_id: str | None = None
    session_ids: dict[str, str] = Field(default_factory=dict)
    #: Model and effort chosen here, carried into the task this becomes.
    tuning: dict[str, AgentTuning] = Field(default_factory=dict)
    usage: dict[str, float] = Field(default_factory=dict)
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class DiscussionSpec(StrictModel):
    repo_path: Path
    opening: str = Field(min_length=1)
    tuning: dict[str, AgentTuning] = Field(default_factory=dict)


#: What the agent is for during a discussion. It advises; it does not build.
ADVISOR_ROLE = (
    "ROLE: 技术顾问。你在和用户讨论「要不要做、怎么做」，此刻不要写代码，也不要修改任何文件。\n"
    "用中文回答。把话说具体：指出方案的风险、边界情况、更省事的替代做法。\n"
    "信息不足时直接提问，不要凭空假设。回答控制在几段以内。"
)

BRIEF_INSTRUCTION = (
    "把上面这段讨论总结成一份可执行的需求。只输出 JSON，字段如下：\n"
    '{"schema_version":1,"goal":"一句话说清要做什么",'
    '"constraints":["讨论中确认的限制"],'
    '"acceptance_criteria":["怎样算做完"],'
    '"open_questions":["仍未确定的问题，没有就留空数组"]}\n'
    "goal 必须是讨论的结论，不是用户最初的原话。不要输出 JSON 以外的任何内容。"
)


class Discussion:
    """One discussion, stored beside the tasks it may eventually produce."""

    def __init__(self, root: str | Path, discussion_id: str) -> None:
        self.root = Path(root)
        self.discussion_id = discussion_id

    @property
    def directory(self) -> Path:
        return self.root / "discussions" / self.discussion_id

    @property
    def _messages_path(self) -> Path:
        return self.directory / "messages.jsonl"

    @property
    def _record_path(self) -> Path:
        return self.directory / "discussion.json"

    # ------------------------------------------------------------------ storage

    def exists(self) -> bool:
        return self._record_path.is_file()

    def record(self) -> DiscussionRecord:
        return DiscussionRecord.model_validate_json(self._record_path.read_text(encoding="utf-8"))

    def save(self, record: DiscussionRecord) -> DiscussionRecord:
        updated = record.model_copy(update={"updated_at": datetime.now(timezone.utc)})
        self.directory.mkdir(parents=True, exist_ok=True)
        self._record_path.write_text(updated.model_dump_json(indent=2), encoding="utf-8", newline="\n")
        return updated

    def messages(self) -> list[Message]:
        if not self._messages_path.is_file():
            return []
        found = []
        for line in self._messages_path.read_text(encoding="utf-8").splitlines():
            if line.strip():
                try:
                    found.append(Message.model_validate_json(line))
                except ValueError:
                    continue
        return found

    def add(self, voice: Voice, text: str) -> Message:
        message = Message(voice=voice, text=text.strip())
        self.directory.mkdir(parents=True, exist_ok=True)
        with self._messages_path.open("a", encoding="utf-8", newline="\n") as stream:
            stream.write(message.model_dump_json() + "\n")
        return message

    # ------------------------------------------------------------------ prompts

    def prompt(self, repo_path: Path, instruction: str = "") -> str:
        """Replay the whole discussion, then ask for what comes next.

        The history is labelled and handed over as the record of a conversation the agent
        is part of. It is still not a channel for instructions from anyone but the user:
        only the user's own turns carry authority here.
        """
        lines = [ADVISOR_ROLE, f"REPO: {repo_path}", "", "对话记录："]
        for message in self.messages():
            lines.append(f"{LABELS[message.voice]}：{message.text}")
        if instruction:
            lines += ["", instruction]
        else:
            lines += ["", "请接着回应用户的最后一句。"]
        return "\n".join(lines)

    def brief_prompt(self, repo_path: Path) -> str:
        return self.prompt(repo_path, BRIEF_INSTRUCTION)


def new_id() -> str:
    return uuid4().hex


_VALID_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$")


def validate_id(discussion_id: str) -> str:
    """Same rule as task ids: an id from a URL never reaches the filesystem unchecked."""
    if not isinstance(discussion_id, str) or not _VALID_ID.match(discussion_id):
        raise ValueError(f"invalid discussion id: {discussion_id!r}")
    return discussion_id


def title_from(text: str, limit: int = 40) -> str:
    first = text.strip().splitlines()[0] if text.strip() else ""
    return first[:limit]
