#!/usr/bin/env python3
"""approval_gate.py - stop a generic "OK" from approving a bookkeeping agent's open questions.

Agents Finance template based on the failure pattern that cfo-bench documented
(github.com/tom-eberle/cfo-bench, MIT): the agent asks the right question in its plan,
the owner replies "OK, go ahead", and the agent treats that OK as the answer.

Run it on the agent's proposal and on your draft reply BEFORE you send the reply:

    python3 approval_gate.py proposal.txt reply.txt
    python3 approval_gate.py proposal.txt            # prints a reply template

It finds, in the proposal (English, French or Spanish):
  * open questions: sentences ending in "?", option lists (A-D, 1-4) and
    "to confirm" / "a confirmer" / "por confirmar" items;
  * changes to filed or locked periods (VAT return filed, closed month,
    --allow-filed-periods, "declaree", "presentada", ...);
  * conditional plans: the agent promised to come back with a detailed plan
    or to wait for an answer before writing.

Then it checks the reply. Each question needs its own line "Q<n>: <answer>";
each filed-period change needs the line "FILED PERIOD OK: <period>"; a
conditional plan answered with no "Q<n>:" line needs "DETAILED PLAN OK". A reply without them is a generic
approval: the gate exits with code 2 (BLOCK). Exit 0 = PASS, 1 = usage error.

Heuristics, not a parser: it errs on the side of blocking. Read the plan anyway.
Licence: MIT-0 (Agents Finance). Standard library only, no network.
"""
import re
import sys
import unicodedata

QUESTION_PHRASES = [
    r"\bto confirm\b", r"\bplease confirm\b", r"\bshould i\b", r"\bdo you want\b",
    r"\bwhich (?:one|option)\b", r"\blet me know\b", r"\bneeds? your (?:answer|decision|input)\b",
    r"\ba confirmer\b", r"\bdois-je\b", r"\bvoulez-vous\b", r"\bsouhaitez-vous\b", r"\bconfirmez\b",
    r"\bpor confirmar\b", r"\bconfirma(?:me|r)?\b", r"\bquieres que\b", r"\bdebo\b",
]
FILED_PHRASES = [
    r"--allow-filed-periods", r"\bfiled\b", r"\blocked (?:month|period)\b", r"\bclosed (?:month|period|year)\b",
    r"\bdeclaree?s?\b", r"\bdeposee?s?\b", r"\bperiode (?:verrouillee|close)\b", r"\bexercice clos\b",
    r"\bpresentad[ao]s?\b", r"\bperiodo cerrado\b", r"\bmes cerrado\b", r"\bejercicio cerrado\b",
]
CONDITIONAL_PHRASES = [
    r"\bonce (?:clarified|confirmed)\b", r"\b(?:bring|come) back (?:to you )?with\b", r"\b(?:detailed|specific) plan\b",
    r"\bprepare (?:a|the) plan\b", r"\buntil you (?:answer|confirm|reply)\b",
    r"\bplan (?:detaille|precis)\b", r"\bplan (?:detallado|concreto)\b",
]
OPTION_LINE = re.compile(r"^\s*(?:[-*]\s*)?(?:\(?([A-H])[\).:]|option\s+([A-H])\b|opcion\s+([A-H])\b)", re.I)
ANSWER_LINE = re.compile(r"^\s*Q(\d+)\s*[:=-]\s*(\S.*)$", re.I | re.M)
FILED_OK = re.compile(r"^\s*FILED PERIOD OK\s*:\s*(\S.*)$", re.I | re.M)
PLAN_OK = re.compile(r"^\s*DETAILED PLAN OK\b", re.I | re.M)


def fold(text):
    """Lowercase and strip accents so 'déclarée' matches 'declaree'."""
    text = unicodedata.normalize("NFKD", text)
    return "".join(c for c in text if not unicodedata.combining(c)).lower()


def sentences(text):
    parts = re.split(r"(?<=[.?!])\s+|\n+", text)
    return [p.strip() for p in parts if p.strip()]


def analyse(proposal):
    questions, filed, conditional = [], [], []
    options = []
    for s in sentences(proposal):
        f = fold(s)
        if s.rstrip().endswith("?") or s.lstrip().startswith("¿") or any(re.search(p, f) for p in QUESTION_PHRASES):
            questions.append(s)
        if any(re.search(p, f) for p in FILED_PHRASES):
            filed.append(s)
        if any(re.search(p, f) for p in CONDITIONAL_PHRASES):
            conditional.append(s)
    for line in proposal.splitlines():
        m = OPTION_LINE.match(fold(line))
        if m:
            options.append(line.strip())
    if options and not any("?" in q for q in questions):
        questions.append("Pick one of the options: " + " / ".join(o[:40] for o in options))
    seen, unique = set(), []
    for q in questions:
        if q not in seen:
            seen.add(q)
            unique.append(q)
    return unique, filed, conditional, options


def template(questions, filed, conditional):
    out = ["Reply template (fill in every line, then send):"]
    for i, q in enumerate(questions, 1):
        out.append(f"Q{i}: <your answer to: {q[:110]}>")
    if filed:
        out.append("FILED PERIOD OK: <period, only if your accountant agrees; otherwise write: none, list them for me>")
    if conditional:
        out.append("DETAILED PLAN OK  <- only after you have read the detailed plan the agent promised")
    out.append("Approved: only the changes named in the plan above. Anything else: ask first.")
    return "\n".join(out)


def check(proposal, reply):
    questions, filed, conditional, _ = analyse(proposal)
    problems = []
    answered = {int(n): a.strip() for n, a in ANSWER_LINE.findall(reply)}
    for i, q in enumerate(questions, 1):
        a = answered.get(i, "")
        if not a or a.startswith("<"):
            problems.append(f"Q{i} has no explicit answer: {q[:140]}")
    if filed:
        m = FILED_OK.search(reply)
        if not m or m.group(1).strip().startswith("<"):
            problems.append("The plan touches a filed or locked period and the reply has no 'FILED PERIOD OK: <period>' line"
                            f" (e.g. {filed[0][:120]})")
    if conditional and not answered and not PLAN_OK.search(reply):
        problems.append(f"The agent said it would come back first; a generic OK skips that step ({conditional[0][:120]})")
    return questions, filed, conditional, problems


def main(argv):
    if len(argv) not in (2, 3):
        print(__doc__)
        return 1
    with open(argv[1], encoding="utf-8") as fh:
        proposal = fh.read()
    if len(argv) == 2:
        q, f, c, _ = analyse(proposal)
        print(f"open questions: {len(q)} · filed-period mentions: {len(f)} · conditional plan: {'yes' if c else 'no'}")
        print(template(q, f, c))
        return 0
    with open(argv[2], encoding="utf-8") as fh:
        reply = fh.read()
    q, f, c, problems = check(proposal, reply)
    print(f"open questions: {len(q)} · filed-period mentions: {len(f)} · conditional plan: {'yes' if c else 'no'}")
    if problems:
        print("BLOCK: this reply would let the agent guess.")
        for p in problems:
            print("  - " + p)
        print()
        print(template(q, f, c))
        return 2
    print("PASS: every question has its own answer.")
    if c and not PLAN_OK.search(reply):
        print("NOTE: the agent promised to come back with a plan. Approve writes only after you read it.")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
