import json
import os
from typing import Any

import requests


class OpenAIClientError(Exception):
    pass


def _api_key() -> str:
    key = os.getenv("OPENAI_API_KEY")
    if not key:
        raise OpenAIClientError("OPENAI_API_KEY is not set")
    return key


def suggest_test_mutations(*, request: dict, max_tests: int, model: str) -> list[dict[str, Any]]:
    """Use OpenAI to suggest additional test cases.

    Returns a list of test dicts like:
      {"name": "...", "mutation": {...}}

    This is best-effort and must never break the run.
    """

    prompt = {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": (
                    "You are an expert API QA engineer. Generate additional API mutation test cases.\n"
                    "Rules:\n"
                    "- Output MUST be valid JSON only (no markdown).\n"
                    "- Return an array of objects with keys: name, mutation.\n"
                    "- mutation schema: {target, path, operation, original_value, new_value, strategy}.\n"
                    "- target is one of: query, body, headers, method, body_whole.\n"
                    "- operation is REMOVE or REPLACE.\n"
                    "- Keep names short and unique.\n"
                    f"- Generate at most {max_tests} tests.\n\n"
                    "Request under test:\n"
                    + json.dumps(request, ensure_ascii=False)
                ),
            }
        ],
    }

    body = {
        "model": model,
        "input": [prompt],
        "temperature": 0.2,
    }

    r = requests.post(
        "https://api.openai.com/v1/responses",
        headers={
            "Authorization": f"Bearer {_api_key()}",
            "Content-Type": "application/json",
        },
        json=body,
        timeout=30,
    )

    if r.status_code >= 400:
        raise OpenAIClientError(f"OpenAI error {r.status_code}: {r.text[:500]}")

    data = r.json()

    # Responses API returns output blocks; extract text
    text = ""
    for item in data.get("output", []) or []:
        for c in item.get("content", []) or []:
            if c.get("type") in ("output_text", "text"):
                text += c.get("text", "")

    text = text.strip()
    if not text:
        return []

    try:
        parsed = json.loads(text)
    except Exception as e:
        raise OpenAIClientError(f"Failed to parse OpenAI JSON: {e}; text={text[:500]}")

    if not isinstance(parsed, list):
        raise OpenAIClientError("OpenAI output was not a list")

    # Basic validation
    out: list[dict[str, Any]] = []
    for t in parsed[:max_tests]:
        if not isinstance(t, dict):
            continue
        name = t.get("name")
        mutation = t.get("mutation")
        if not name or not isinstance(mutation, (dict, type(None))):
            continue
        out.append({"name": str(name), "mutation": mutation})

    return out


def summarize_run(*, request: dict, decision: str, results: list[dict], model: str) -> dict[str, Any]:
    """Generate a human-readable QA summary from results.

    Best-effort: callers should wrap in try/except.
    """

    # Keep prompt smaller: send only key fields per test
    compact = []
    for r in results:
        resp = r.get("response") or {}
        compact.append({
            "name": r.get("name"),
            "failure_type": r.get("failure_type"),
            "status_code": resp.get("status_code"),
            "elapsed_ms": resp.get("elapsed_ms"),
            "attempt": resp.get("attempt"),
        })

    prompt = {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": (
                    "You are a senior QA lead. Summarize an API mutation test run for humans.\n"
                    "Output MUST be valid JSON only (no markdown).\n"
                    "Return an object with keys:\n"
                    "- executive_summary (string, 3-6 sentences)\n"
                    "- key_findings (array of strings)\n"
                    "- suspected_root_causes (array of strings)\n"
                    "- recommendations (array of strings)\n"
                    "- top_failures (array of objects: {name, failure_type, status_code, note})\n"
                    "Be careful: a 2xx to security payload is a potential risk; a 4xx to security payload is safe validation.\n\n"
                    "Request (redacted headers may be present):\n"
                    + json.dumps(request, ensure_ascii=False)
                    + "\n\nDecision: " + str(decision)
                    + "\n\nResults (compact):\n"
                    + json.dumps(compact, ensure_ascii=False)
                ),
            }
        ],
    }

    body = {
        "model": model,
        "input": [prompt],
        "temperature": 0.2,
    }

    r = requests.post(
        "https://api.openai.com/v1/responses",
        headers={
            "Authorization": f"Bearer {_api_key()}",
            "Content-Type": "application/json",
        },
        json=body,
        timeout=30,
    )

    if r.status_code >= 400:
        raise OpenAIClientError(f"OpenAI error {r.status_code}: {r.text[:500]}")

    data = r.json()

    text = ""
    for item in data.get("output", []) or []:
        for c in item.get("content", []) or []:
            if c.get("type") in ("output_text", "text"):
                text += c.get("text", "")

    text = text.strip()
    if not text:
        return {}

    parsed = json.loads(text)
    if not isinstance(parsed, dict):
        raise OpenAIClientError("OpenAI summary output was not an object")

    return parsed
