import re
from dataclasses import dataclass, replace

from features.form_of_address.address import voice_of
from features.form_of_address.voices import BUTLER, Voice

NAMING_LAW = "L5"


@dataclass(frozen=True)
class Law:
    name: str
    text: str
    reason: str
    keywords: tuple[str, ...]
    keywords_in: str


LAWS = (
    Law("L1", "Every subagent dispatch names its model and chooses the least expensive model that reliably fits the work.",
        "Use a fast, economical model for mechanical work with a known answer, a capable general model for careful implementation, and the strongest model only when the task turns on difficult judgement. Inheriting the orchestrator's model is not a model choice. If the dispatch API cannot accept a model, that operation is exempt.",
        ("subagent", "spawn_agent", "haiku", "sonnet", "opus"), "everything"),
    Law("L2", "Every subagent is bound to a concrete job; never dispatch a generic or default agent.",
        "Use the most specific available agent type whose declared purpose matches the assignment. On providers without agent types, give the dispatch a concrete task name and bounded prompt. If no suitable specialization exists, keep the work in the main agent instead of manufacturing an unscoped helper.",
        ("subagent", "spawn_agent", "general-purpose"), "everything"),
    Law("L3", "Read narrowly: grep for the line, sed a range, head the file; never print a whole file or long output you do not need.",
        "Everything a tool returns stays in the context for good and is paid for on every turn after it. Search before you read, read the range you need, and cap output with grep, head or tail. Read a whole file only when you need all of it.",
        ("cat", "less", "git show", "git diff", "journal carry"), "commands"),
    Law("L4", "Related work goes back to the helper or subagent that already worked on it, in its own session; never start a fresh one on work another already knows.",
        "A helper or subagent that drew a design, wrote the code or ran the research keeps what it learned. When new work changes its work, is related to it or touches the same code, send it there with a message (SendMessage, journal helper say) rather than dispatching a new one that has to rediscover everything; start fresh only when the earlier one is gone or the new work is unrelated. Reuse means its whole session, not its name: a helper whose agent ended is resumed in the session it ran, with its context, never started again under the same name.",
        ("subagent", "spawn_agent", "designer", "SendMessage", "helper dispatch"), "everything"),
    Law(NAMING_LAW, "Every subagent dispatch names the agent, in the naming style of the profile in use.",
        "A name is how the user and the chat tell subagents apart and how they are messaged later; an id or a task line is not a name. Start the dispatch's description with the name, a colon, then the task. The profile in use says how its agents are named.",
        ("subagent", "spawn_agent", "dispatch"), "everything"),
    Law("L6", "A journal line is an instruction, never a message: act on it and write nothing in the chat, unless the user needs to know (a failure, finished work, a decision that waits on them).",
        "A line that starts with [journal], a reminder, a notice or an old helper report is the journal telling the agent what to do, not the user speaking. Answering it fills the user's chat with noise. Act on it, or note it and carry on; write in the chat only what the user needs to know, such as a failure, a finished piece of work or a decision that waits on them.",
        ("[journal]", "reminder", "notice", "nudge"), "everything"),
    Law("L7", "Write the least code that solves the whole problem: find what already does it and reuse it, and never write the same logic twice.",
        "Before writing, search the code for what already does the job or most of it, and extend that instead of adding a second way. Every read or write of one kind of thing (a file, a record, a setting, a provider) goes through the one funnel that owns it, which is where caching and checks live. A fix lands where the fault is born, not where it shows. When you finish, say in a line what you skipped or did not check.",
        ("def", "class", "function", "refactor", "duplicate", "Edit", "Write"), "everything"),
)

GENERIC = frozenset({"", "agent", "default", "general", "general-purpose"})
NAMED = re.compile(r"^[A-Z][\w.,'-]*(?:\s+[\w.,'-]+){0,3}\s*:\s*\S")


def laws(record=None) -> tuple[Law, ...]:
    voice = voice_in_use(record)
    address_rule = (f"{voice.naming} Every helper and subagent addresses you as {voice.agent_name} and never the user: "
            f"their reports are written to you.")
    return tuple(replace(law, text=f"{law.text} {address_rule}") if law.name == NAMING_LAW else law for law in LAWS)


def naming_of(record=None) -> str:
    return voice_in_use(record).naming


def voice_in_use(record=None) -> Voice:
    return BUTLER if record is None else voice_of(record)


def carry(record=None) -> str:
    rows = "\n".join(f"  - {law.text}  [{law.name}]" for law in laws(record))
    return f"LAWS THE JOURNAL SHIPS, always in force:\n{rows}"

def refusal(dispatch, naming: str) -> str:
    if dispatch.kind in GENERIC and dispatch.task in GENERIC:
        return "Journal law L2 refuses generic subagents. Choose a specific agent type or give the dispatch a concrete task name and bounded assignment."
    if dispatch.model_supported and not dispatch.model:
        return "Journal law L1 requires an explicit model on every subagent dispatch. Choose the least expensive model that reliably fits the work."
    if not dispatch.offers_model():
        return f"{dispatch.model} is not a model this account offers. Choose one of {', '.join(dispatch.models)}."
    if dispatch.name_supported and not NAMED.match(dispatch.description):
        return f"Journal law L5 requires a name on every subagent dispatch: start the description with the name, a colon, then the task. {naming}"
    return ""
