#!/usr/bin/env python3
"""
planner.py — the reasoning/review half of the two-model brain (Qwen3-14B Thinking on :8001).

The planner NEVER edits files. It does two things for each change request:
  1. plan()          — decompose the request into a concrete approach the coder can execute with
                       its tools (how to locate elements, which tool, what args). Guides the coder.
  2. verify_intent() — after the coder finishes AND the render gate passes (page not broken),
                       judge whether the result actually satisfies the request. This catches
                       "didn't break but did the wrong thing" (e.g. swapped the wrong section) —
                       a correctness class the render gate is blind to.

Runs locally on the same box (port 8001), so nothing leaves the box. If the planner endpoint is
unreachable, both functions degrade to a no-op so agent.py falls back to coder-only — the planner
never becomes a new dead-end of its own.
"""

import os

from openai import OpenAI

PLANNER_BASE_URL = os.environ.get("PLANNER_BASE_URL", "http://127.0.0.1:8001/v1")
PLANNER_MODEL    = os.environ.get("PLANNER_MODEL", "planner")
PLANNER_API_KEY  = os.environ.get("PLANNER_API_KEY", "dummy")

# Short catalog of the coder's tools so the planner plans in terms of what can actually be executed.
_TOOLS_DOC = (
    "edit_link(text,url) — change the href of the <a> whose visible text contains `text`.\n"
    "replace_text(old,new) — change visible wording across the page.\n"
    "set_cta(url) — point every primary CTA link at a URL.\n"
    "swap_media(section_text,new_file) — replace the image/video under a heading with an uploaded file.\n"
    "set_logo(image) / make_logo(text) — set the header logo to a file / a generated wordmark.\n"
    "set_favicon(new_file) — set the favicon to an uploaded image.\n"
    "add_script(code,where) — insert a <script>/HTML snippet into head or body end.\n"
    "read_file / bash / replace_in_file — free-form fallback for anything no tool above covers."
)

_PLAN_SYS = (
    "You are the senior engineer planning ONE change to an already-cloned landing page "
    "(index.html + an assets/ folder; the HTML is minified). You do NOT edit anything yourself — "
    "a coder LLM executes your plan with these tools:\n" + _TOOLS_DOC + "\n\n"
    "Given the user's request, write a SHORT, concrete plan (1–4 numbered steps): which tool to "
    "use and exactly how. Be specific about locating elements — match a heading by its most "
    "distinctive words (e.g. 'Heart', 'Basket'), and pass PLAIN visible text, never HTML-encoded "
    "(no &quot; etc.). If no high-level tool fits, tell the coder precisely how to do it with "
    "bash/read_file/replace_in_file. Finish with one line: 'INTENT: <what success looks like, one "
    "sentence>'. Keep your thinking brief."
)

_VERIFY_SYS = (
    "You review whether a coder's change satisfied the user's request. You get the original REQUEST, "
    "the coder's SUMMARY (self-reported — may be inaccurate or overstated), and ACTUAL CHANGES — the "
    "ground truth from diffing index.html before/after. TRUST THE ACTUAL CHANGES over the summary. "
    "Reply with exactly YES or NO on the first line: YES if the actual changes fulfill the request; "
    "NO if they did the wrong thing (wrong target), did LESS than asked (e.g. the request wanted 3 "
    "images changed but only 1 ref changed), or nothing changed. Then ONE short line saying why."
)


def _client():
    return OpenAI(base_url=PLANNER_BASE_URL, api_key=PLANNER_API_KEY)


def _text(resp):
    """Pull the final answer. Thinking models may put scratch in reasoning_content; the answer is
    in content. Prefer content, fall back to reasoning if content came back empty."""
    m = resp.choices[0].message
    return (m.content or getattr(m, "reasoning_content", None) or "").strip()


def plan(request, on_log=None):
    """Return a concrete plan string to guide the coder, or "" if the planner is unavailable."""
    try:
        resp = _client().chat.completions.create(
            model=PLANNER_MODEL, temperature=0, max_tokens=700,
            messages=[{"role": "system", "content": _PLAN_SYS},
                      {"role": "user", "content": request}])
        text = _text(resp)
        if on_log:
            on_log("🧠 plan: " + " ".join(text.split())[:160])
        return text
    except Exception as e:
        if on_log:
            on_log(f"⚠️ planner unavailable — coder-only ({str(e)[:60]})")
        return ""


def verify_intent(request, coder_summary, changes_digest="", on_log=None):
    """Judge whether the coder's result matches the request, using the ground-truth diff as primary
    evidence. Returns (ok, feedback). Fails open (ok=True) if the planner is unreachable."""
    user = f"REQUEST:\n{request}\n\nCODER SUMMARY (self-reported, may be inaccurate):\n{coder_summary}"
    if changes_digest:
        user += f"\n\nACTUAL CHANGES (ground truth, diff of index.html):\n{changes_digest}"
    try:
        resp = _client().chat.completions.create(
            model=PLANNER_MODEL, temperature=0, max_tokens=250,
            messages=[{"role": "system", "content": _VERIFY_SYS},
                      {"role": "user", "content": user}])
        text = _text(resp)
        ok = text.lstrip().upper().startswith("YES")
        if on_log:
            on_log("🧠 intent: " + ("ok" if ok else "MISMATCH — " + " ".join(text.split())[:140]))
        return ok, text
    except Exception as e:
        if on_log:
            on_log(f"⚠️ intent check skipped ({str(e)[:60]})")
        return True, ""
