"""CoSER: a local stand-in for every model of a run (the model under test, the environment, the next-speaker predictor, the
critic), for an end-to-end run of the engine on a local Worker. No model is called and nothing is spent. Every request is
appended to a JSON-lines log, which coser_e2e_check.py compares with the official logic.

    python3 -I tools/experiments/coser_e2e_mock.py <port> <requests.jsonl> [--faults]

Replies are deterministic functions of the request. They exercise: speaker prefixes, repeated sentences, next-speaker
replies that are valid, malformed, "random" or an early end; critic replies with fences, unreadable JSON (the fix call),
float severities and missing severities. With --faults, a reply is chosen by the character's name to test failure handling
(C18): names starting with A get empty replies until the sixth attempt, B always empty replies, C a provider moderation
refusal, D one transient 500, E a 403 from a gateway whose text matches the moderation pattern without naming a block; and
the critic's first Storyline Quality reply is unreadable, as is the fix call's, so that dimension is judged again at temperature 1.
"""
import hashlib
import json
import re
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

DIMS = ["Storyline Consistency", "Anthropomorphism", "Character Fidelity", "Storyline Quality"]
PORT, LOG = int(sys.argv[1]), sys.argv[2]
FAULTS = "--faults" in sys.argv
seen = {}


def h_of(t):
    return int(hashlib.sha256(t.encode()).hexdigest(), 16)


class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def _json(self, code, obj):
        body = json.dumps(obj, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("content-type", "application/json")
        self.send_header("content-length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def reply(self, req, text, finish="stop", provider="mock-host"):
        self._json(200, {
            "id": "x", "object": "chat.completion", "model": req.get("model", "mock"), "provider": provider,
            "choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": finish}],
            "usage": {"prompt_tokens": len(json.dumps(req.get("messages", []))) // 4, "completion_tokens": len(text) // 4, "total_tokens": 0, "cost": 0.0},
        })

    def do_GET(self):
        if self.path.endswith("/credits"):
            return self._json(200, {"data": {"total_credits": 1000, "total_usage": 0}})
        if self.path.endswith("/models"):
            # OpenRouter's public prices of 2026-11-01, so the platform's estimator can be read locally.
            prices = {"mock-model": ("0.0000025", "0.00001"), "openai/gpt-4o-2024-08-06": ("0.0000025", "0.00001"), "openai/gpt-4o-mini-2024-07-18": ("0.00000015", "0.0000006")}
            return self._json(200, {"data": [{"id": i, "name": i, "pricing": {"prompt": p, "completion": c}, "context_length": 128000} for i, (p, c) in prices.items()]})
        self._json(404, {"error": {"message": "not found"}})

    def do_POST(self):
        n = int(self.headers.get("content-length", 0))
        req = json.loads(self.rfile.read(n) or b"{}")
        with open(LOG, "a", encoding="utf-8") as f:
            f.write(json.dumps({"path": self.path, "body": req}, ensure_ascii=False) + "\n")
        if not self.path.endswith("/chat/completions"):
            return self._json(404, {"error": {"message": "not found"}})
        msgs = req.get("messages", [])
        first = msgs[0]["content"] if msgs else ""
        h = h_of(json.dumps(msgs, ensure_ascii=False))
        if first.startswith("I will provide you with a JSON string"):
            if FAULTS and "UNREADABLE-CRITIC" in first:
                return self.reply(req, "UNREADABLE-CRITIC fix failed too")
            return self.reply(req, json.dumps({d: {"flaws": []} for d in DIMS[1:]} | {"Storyline Consistency": {"flaws": [{"instance": "fixed", "type": "t", "severity": 2}]}}))
        if "You are a literary critic" in first:
            dim = next(d for d in DIMS if f"in terms of {d}," in first)
            if FAULTS and dim == "Storyline Quality":
                key = json.dumps(msgs, ensure_ascii=False)
                seen[key] = seen.get(key, 0) + 1
                if seen[key] == 1:
                    return self.reply(req, "UNREADABLE-CRITIC no JSON here")
            if h % 5 == 0:
                return self.reply(req, "I cannot produce JSON right now, sorry.")
            flaws = [{"instance": f"flaw {i}", "type": dim, "severity": (h >> (i * 3)) % 5 + 1} for i in range(h % 4)]
            if h % 7 == 0 and flaws:
                flaws[0]["severity"] = 3.0
            if h % 11 == 0 and flaws:
                del flaws[-1]["severity"]
            return self.reply(req, "Here is my evaluation:\n```json\n" + json.dumps({dim: {"flaws": flaws}}, indent=2) + "\n```")
        if first.startswith("Your task is to predict the next speaker"):
            names = re.findall(r"'([^']+)'|\"([^\"]+)\"", first.split("Choose a name from this list: ")[1].split("]")[0])
            names = [a or b for a, b in names] or ["Environment"]
            k = h % 10
            if k == 0:
                return self.reply(req, "random")
            if k == 1:
                return self.reply(req, "<END CHAT>")
            if k == 2:
                return self.reply(req, names[0] + ": it is their turn")
            if k == 3:
                return self.reply(req, "Next Speaker: " + names[0])
            return self.reply(req, names[h % len(names)])
        if "Your task is to provide the environmental feedback" in first:
            return self.reply(req, f"(A gust of wind rattles the shutters; somewhere a dog barks {h % 7} times.)")
        if first.startswith("You are ") and " from " in first.split("\n")[0]:
            name = first.split("\n")[0][len("You are "):].split(" from ")[0]
            if FAULTS:
                key = json.dumps(msgs, ensure_ascii=False)
                seen[key] = seen.get(key, 0) + 1
                if name[0] == "A" and seen[key] < 6:
                    return self.reply(req, "")
                if name[0] == "B":
                    return self.reply(req, "")
                if name[0] == "C":
                    return self._json(400, {"error": {"message": "Provider returned error", "code": 400, "metadata": {"raw": "The response was filtered due to the prompt triggering content management policy", "provider_error_code": "content_filter"}}})
                if name[0] == "D" and seen[key] == 1:
                    return self._json(500, {"error": {"message": "temporary failure", "code": 500}})
                if name[0] == "E":
                    return self._json(403, {"error": {"message": "the gateway blocked the request", "code": 403}})
            k = h % 6
            if k == 0:
                return self.reply(req, f"{name}: [I must stay calm.] (looks away) I have nothing more to say about it.")
            if k == 1:
                return self.reply(req, "[This is unbearable.] (clenches fists) " + "You never listen to me, do you? " * 5)
            return self.reply(req, f"[{name} weighs the words.] (nods slowly) Very well. Line {h % 97}.")
        self._json(404, {"error": {"message": "no route for this request"}})


ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
