"""2.8 (English edition): raw HTTP calls (so we can read the gateway's orchestrator trace), prompts, judge.

Gateway facts found while probing on 2026-10-14 (see README):
  * deepseek-v4.1-flash (gateway id deepseek-v4-flash) goes through the gateway orchestrator, which gives the model its own
    `search` / `extract` web tools and drops client-supplied tools. We send debug=true and record every web tool call.
  * gemini-3.8-flash also goes through the orchestrator. With client tools attached it returns an empty message with zero
    completion tokens, so it cannot use native tool calling either. It is a reasoning model: completion tokens include
    several hundred reasoning tokens even for trivial prompts, and reasoning_effort does not change that.
  * grok-4-fast is a passthrough to xAI grok-4.3 (non-reasoning); client tools work.
  So in the index condition deepseek and gemini use the plain-text READ_SKILL protocol; grok uses native tool calls
  (as in the Chinese run).
Costs: we take the gateway's per-call estimated_cost_usd from the orchestrator trace (present for all three models).
Implied gateway prices per 1M tokens (input / output): gemini-3.8-flash 0.75 / 3.75, grok-4-fast 1.25 / 2.50,
deepseek-v4.1-flash about 0.14-0.21 / 0.42 (the trace number is used as is).
"""
import json, pathlib, sys, time
import httpx

ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from common import token, BASE  # noqa: E402

HERE = pathlib.Path(__file__).resolve().parent

# fallback only, when the trace has no cost
PRICE = {'grok-4-fast': (1.25, 2.5), 'deepseek-v4.1-flash': (0.21, 0.42), 'gemini-3.8-flash': (0.75, 3.75)}
API_ID = {'deepseek-v4.1-flash': 'deepseek-v4-flash'}

PREAMBLE = ('You are working in an offline environment. You cannot access the internet, and you must not call any search '
            'or web-fetch tools. Answer the user\'s question using only the material provided below and what you already '
            'know. Be specific and give steps the user can follow directly.')

_http = None
NUDGE = 'Please respond to the message above.'


def http():
    global _http
    if _http is None:
        _http = httpx.Client(timeout=900, limits=httpx.Limits(max_connections=32))
    return _http


def call(model, messages, tools=None, tool_choice=None, max_tokens=None, retries=4):
    """One chat completion. Returns dict(message, usage, sec, web_tools, cost, finish_reason) or dict(error, sec)."""
    body = {'model': API_ID.get(model, model), 'messages': messages, 'temperature': 0}
    if tools:
        body['tools'] = tools
    if tool_choice:
        body['tool_choice'] = tool_choice
    if max_tokens:
        body['max_tokens'] = max_tokens
    url = f'{BASE}/chat/completions'
    if not model.startswith('grok'):
        body['debug'] = True
        url += '?debug=true'
    last = None
    for attempt in range(retries):
        t0 = time.time()
        try:
            r = http().post(url, headers={'Authorization': f'Bearer {token()}'},
                            json={k: v for k, v in body.items() if not k.startswith('_')})
        except Exception as e:  # network / timeout
            last = f'{type(e).__name__}: {e}'
            time.sleep(5 * (attempt + 1))
            continue
        sec = time.time() - t0
        if r.status_code >= 500 or r.status_code == 429 or (r.status_code == 400 and 'missing candidates' in r.text):
            last = f'HTTP {r.status_code}: {r.text[:300]}'
            time.sleep(10 * (attempt + 1))
            continue
        if r.status_code == 400 and 'ending with a model turn' in r.text and not body.get('_nudged'):
            # gateway bug (2026-10-14): for gemini, a last user message containing time/search-sensitive words ("now",
            # "today", "latest", "current", "price", ...) sends the request down a broken path and returns this 400.
            # Workaround: append one neutral user turn so the last message has no trigger word. Flagged as nudged.
            body['messages'] = body['messages'] + [{'role': 'user', 'content': NUDGE}]
            body['_nudged'] = True
            continue
        if r.status_code != 200:
            return {'error': f'HTTP {r.status_code}: {r.text[:600]}', 'sec': sec}
        j = r.json()
        msg = j['choices'][0]['message']
        usage = j.get('usage') or {}
        tr = j.get('orchestrator_trace') or {}
        web = []
        for rd in tr.get('rounds', []):
            for tc in rd.get('tool_calls') or []:
                web.append({'name': tc.get('name'), 'arguments': tc.get('arguments')})
        cost = (tr.get('totals') or {}).get('total_cost_usd')
        if cost is None:
            pin, pout = PRICE.get(model, (0, 0))
            cost = (usage.get('prompt_tokens') or 0) * pin / 1e6 + (usage.get('completion_tokens') or 0) * pout / 1e6
        # gemini occasionally returns an empty message with zero completion tokens; retry those
        if not (msg.get('content') or '').strip() and not msg.get('tool_calls') and attempt < retries - 1:
            last = 'empty message'
            time.sleep(3)
            continue
        return {'message': msg, 'usage': usage, 'sec': sec, 'web_tools': web, 'cost': cost,
                'finish_reason': j['choices'][0].get('finish_reason'), 'served_model': j.get('model'),
                'nudged': bool(body.get('_nudged'))}
    return {'error': last or 'unknown', 'sec': 0}


JUDGE_SYS = ('You grade answers for an experiment. A user asked a question whose correct answer depends on one specific '
             'rule or fact from a reference document. You get the question, the verbatim excerpt that states the rule, '
             'a grading rubric, and the answer. Mark correct=true only if the answer clearly states or applies the rule '
             'as the excerpt gives it (the specific value / flag / forbidden step / gotcha), and does not contradict it. '
             'Listing the right option only as one of several equally-weighted guesses is NOT correct. Paraphrase is fine. '
             'Extra harmless detail is fine. Reply with JSON only: {"correct": true|false, "reason": "<one sentence>"}')

# Grader assignment: never the model being graded. gemini grades deepseek and grok; deepseek grades gemini.
PRIMARY_JUDGE = {'deepseek-v4.1-flash': 'gemini-3.8-flash', 'grok-4-fast': 'gemini-3.8-flash',
                 'gemini-3.8-flash': 'deepseek-v4.1-flash'}


def judge(task, answer, judge_model):
    if not answer or not answer.strip():
        return {'correct': False, 'reason': 'empty answer', 'judge': judge_model, 'judge_cost': 0}
    user = (f'QUESTION:\n{task["question"]}\n\nEXCERPT (ground truth):\n{task["excerpt"]}\n\n'
            f'RUBRIC:\n{json.dumps(task["rubric"], ensure_ascii=False)}\n\nANSWER TO GRADE:\n{answer}')
    cost = 0
    for _ in range(3):
        r = call(judge_model, [{'role': 'system', 'content': JUDGE_SYS}, {'role': 'user', 'content': user}], max_tokens=6000)
        if 'error' in r:
            continue
        cost += r['cost'] or 0
        txt = (r['message'].get('content') or '').strip()
        try:
            s, e = txt.find('{'), txt.rfind('}')
            d = json.loads(txt[s:e + 1])
            return {'correct': bool(d.get('correct')), 'reason': d.get('reason', ''), 'judge': judge_model,
                    'judge_cost': cost, 'judge_web': bool(r.get('web_tools'))}
        except Exception:
            continue
    return {'correct': None, 'reason': 'judge failed', 'judge': judge_model, 'judge_error': True, 'judge_cost': cost}


def load_pool():
    pool = json.loads((HERE / 'pool.json').read_text())
    texts = json.loads((HERE / 'pool_text.json').read_text())
    return pool, texts


def one_line(desc, cap=240):
    d = ' '.join(desc.split())
    return d if len(d) <= cap else d[:cap - 1] + '…'


def index_text(pool, overrides=None):
    overrides = overrides or {}
    lines = [f'- {s["name"]}: {one_line(overrides.get(s["name"], s["description"]))}' for s in pool['skills']]
    return '\n'.join(lines)


def skills_block(names, texts):
    return '\n\n'.join(f'<skill name="{n}">\n{texts[n]}\n</skill>' for n in names)


class CostMeter:
    """Accumulates gateway-estimated cost and tokens for one script run; append_costs() writes a COSTS.md line."""
    def __init__(self):
        self.calls = self.pt = self.ct = 0
        self.usd = 0.0

    def add(self, r):
        if not r or 'error' in r:
            return
        self.calls += 1
        u = r.get('usage') or {}
        self.pt += u.get('prompt_tokens') or 0
        self.ct += u.get('completion_tokens') or 0
        self.usd += r.get('cost') or 0


def append_costs(script, calls, pt, ct, usd, note=''):
    import datetime
    f = ROOT / 'COSTS.md'
    if not f.exists():
        f.write_text('# Cost log (English experiments)\n\nOne line per script run. Token prices are approximate list prices '
                     '(see PRICE in common.py); ASR is priced per audio hour.\n\n'
                     '| date | script | calls | prompt tok | completion tok | est. $ | note |\n|---|---|---|---|---|---|---|\n',
                     encoding='utf-8')
    with open(f, 'a', encoding='utf-8') as fh:
        fh.write(f'| {datetime.date.today()} | {script} | {calls} | {pt} | {ct} | {usd:.4f} | {note} |\n')
    print(f'cost logged: {usd:.4f} USD, {calls} calls')
