"""Python 3.11+, standard library. One explicit request; no automatic retry.
python conversation.py saved-request.json new-response.json (or .mp3)
"""
import json
import os
import re
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

IDENT = r"[A-Za-z0-9_-]{1,128}"
PATH = re.compile(r"^/(?:chat|requests/" + IDENT + r"|sessions(?:/" + IDENT + r"(?:/(?:end|discard|history))?)?|session/status|data/(?:export|erase)|conversation/(?:catalog|insights|voice|data/export|sessions(?:/" + IDENT + r"(?:/(?:consent|playback|debrief|audio))?)?|audio/" + IDENT + r"(?:/(?:continue|segments/" + IDENT + r"))?))$")
UUID = re.compile(r"^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$")


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise ValueError("Redirect refused; verify the assigned API host")


def make_request(intent, key):
    base = urllib.parse.urlsplit(intent["base_url"])
    user = intent["external_user_id"]
    method = intent["method"]
    if (base.scheme != "https" or not base.netloc or base.username or base.password or base.query or base.fragment
            or base.path not in ("/v1", "/v1/") or not PATH.fullmatch(intent["path"])
            or not UUID.fullmatch(intent["assignment_id"]) or not isinstance(user, str) or not 1 <= len(user) <= 200
            or re.search(r"[\r\n\x00]", user) or not re.fullmatch(r"[\x21-\x7e]{1,512}", key)
            or method not in ("GET", "POST", "DELETE")):
        raise ValueError("Invalid saved request or server credentials")
    query = intent.get("query", {})
    if any(name not in ("limit", "cursor", "session_id") for name in query):
        raise ValueError("Unexpected query field")
    url = intent["base_url"].rstrip("/") + intent["path"]
    if query:
        url += "?" + urllib.parse.urlencode(query)
    headers = {"Authorization": "Bearer " + key, "X-Assignment-Id": intent["assignment_id"], "X-User-Id": user}
    body = None
    if method != "GET":
        if not UUID.fullmatch(intent.get("idempotency_key", "")):
            raise ValueError("Persist an operation UUID before sending a mutation")
        headers["Idempotency-Key"] = intent["idempotency_key"]
        headers["Content-Type"] = "application/json"
        body = json.dumps(intent.get("body", {}), ensure_ascii=False, separators=(",", ":")).encode("utf-8")
        if len(body) > 49152:
            raise ValueError("Request exceeds the example envelope")
    elif "body" in intent:
        raise ValueError("GET cannot have a body")
    return urllib.request.Request(url, data=body, headers=headers, method=method)


def parse_stream(source, require_end=False):
    request_id = session_id = turn = safety = receipt = None
    ended = complete = False
    for frame in source.replace("\r\n", "\n").split("\n\n"):
        data = "\n".join(line[5:].lstrip() for line in frame.split("\n") if line.startswith("data:"))
        if not data:
            continue
        event = json.loads(data)
        kind = event.get("type")
        if kind == "error":
            raise ValueError("Turn failed or uncertain; recover original request " + (request_id or ""))
        if complete:
            raise ValueError("Events followed completion")
        if kind == "metadata":
            if request_id or not isinstance(event.get("request_id"), str) or not isinstance(event.get("sessionId"), str):
                raise ValueError("Invalid stream identity")
            request_id, session_id = event["request_id"], event["sessionId"]
        elif kind == "conversation_turn":
            turn = event.get("turn", {})
            if not request_id or turn.get("request_id") != request_id or turn.get("session_id") != session_id:
                raise ValueError("Mismatched saved turn")
        elif kind == "safety_status":
            safety = event
        elif kind == "session_ended":
            ended = True
        elif kind == "done":
            receipt = event.get("receipt", {})
            if (not request_id or not safety or event.get("sessionId") != session_id
                    or receipt.get("request_id") != request_id or receipt.get("session_id") != session_id):
                raise ValueError("Unverified final receipt")
            complete = True
    if not complete or (require_end and not ended):
        raise ValueError("Incomplete stream; recover original request " + (request_id or ""))
    return {"request_id": request_id, "session_id": session_id, "turn": turn, "safety": safety, "receipt": receipt, "session_ended": ended}


def execute(intent, key, opener=None):
    request = make_request(intent, key)
    opener = opener or urllib.request.build_opener(NoRedirect())
    try:
        response = opener.open(request, timeout=120)
    except urllib.error.HTTPError as error:
        # No response body or credential is printed as a diagnostic; only the documented error code is read.
        try:
            code = str(json.loads(error.read(65536)).get("code") or "")
        except (ValueError, AttributeError, OSError):
            code = ""
        advice = "not charged: send it again with a new idempotency key" if code == "REQUEST_FAILED" else "retain the original saved request"
        raise ValueError("HTTP " + str(error.code) + (" " + code if code else "") + "; " + advice) from None
    with response:
        kind = response.headers.get_content_type()
        limit = 2 * 1024 * 1024 if kind == "text/event-stream" else 8 * 1024 * 1024
        chunks, total, deadline = [], 0, time.monotonic() + 120
        while True:
            chunk = response.read1(16384)
            if not chunk:
                break
            total += len(chunk)
            if total > limit or time.monotonic() > deadline:
                raise ValueError("Response exceeds the example bound")
            chunks.append(chunk)
        raw = b"".join(chunks)
        if kind == "audio/mpeg":
            return raw
        if kind == "text/event-stream":
            data = parse_stream(raw.decode("utf-8"), intent["path"].endswith("/end"))
        elif kind == "application/json":
            data = json.loads(raw)
        else:
            raise ValueError("Unexpected response type")
        operation = intent.get("body", {}).get("operation_id")
        if operation is not None and "operation_id" in data and data["operation_id"] != operation:
            raise ValueError("Mismatched operation receipt")
        return {"status": response.status, "retry_after": response.headers.get("Retry-After"), "data": data}


def main():
    if len(sys.argv) != 3:
        raise ValueError("Usage: python conversation.py saved-request.json new-response.json (or .mp3)")
    with open(sys.argv[1], "rb") as source:
        raw = source.read(65537)
    if len(raw) > 65536:
        raise ValueError("Saved request too large")
    descriptor = os.open(sys.argv[2], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(descriptor, "wb") as output:
        result = execute(json.loads(raw), os.environ.get("FOREVERLEARNING_API_KEY", ""))
        output.write(result if isinstance(result, bytes) else (json.dumps(result, ensure_ascii=False, indent=2) + "\n").encode("utf-8"))
        output.flush()
        os.fsync(output.fileno())
    print("Saved response. Inspect its status and receipt before another operation.")


if __name__ == "__main__":
    try:
        main()
    except (ValueError, OSError, KeyError, TypeError):
        print("Request unavailable or unverified. Retain the original saved request and recover its outcome.", file=sys.stderr)
        sys.exit(1)
