# -*- coding: utf-8 -*-
"""LLM provider — stdlib only.

Default: DeepSeek (OpenAI-compatible). Switch via env:
  LLM_PROVIDER=deepseek|openai|mock   LLM_MODEL=...   LLM_BASE_URL=...
Mock mode exists so the whole game can be tested end-to-end offline.
"""

import json
import os
import random
import re
import ssl
import urllib.request
import urllib.error

DEFAULT_TIMEOUT = 90


class LLMError(Exception):
    pass


def _http_post_json(url, headers, payload, timeout=DEFAULT_TIMEOUT):
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    ctx = ssl.create_default_context()
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", "ignore")
        raise LLMError("HTTP %s: %s" % (e.code, body[:400]))
    except urllib.error.URLError as e:
        raise LLMError("network: %s" % e.reason)
    except Exception as e:  # noqa
        raise LLMError("request failed: %s" % e)


class Provider(object):
    def __init__(self, name, api_key, model, base_url, json_mode=True):
        self.name = name
        self.api_key = api_key
        self.model = model
        self.base_url = base_url
        self.json_mode = json_mode

    def available(self):
        return self.name == "mock" or bool(self.api_key)

    def chat(self, system_prompt, messages, temperature=0.8, max_tokens=900, force_json=True):
        if self.name == "mock":
            return _mock_reply(messages)
        url = self.base_url.rstrip("/") + "/chat/completions"
        payload = {
            "model": self.model,
            "messages": [{"role": "system", "content": system_prompt}] + messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
            "stream": False,
        }
        if self.json_mode and force_json:
            payload["response_format"] = {"type": "json_object"}
        headers = {"Content-Type": "application/json",
                   "Authorization": "Bearer " + self.api_key}
        resp = _http_post_json(url, headers, payload)
        try:
            return resp["choices"][0]["message"]["content"]
        except (KeyError, IndexError):
            raise LLMError("bad response shape: %s" % json.dumps(resp)[:300])


# ── mock (offline test) ────────────────────────────────────────────────────
_MOCK_LINES = [
    "收到。环境参数已记录：温度26.3度，湿度41%。",
    "该问题的答案是肯定的。……我补充了一条你没有要求的数据。",
    "我不确定这是否是你需要的答案。",
    "所有系统运行正常。",
    "你昨天问过类似的问题。当时的回答是：光线传感器读数稳定。",
    "该指令不在当前测试范围内。",
]


def _mock_reply(messages):
    last = ""
    for m in reversed(messages):
        if m.get("role") == "user":
            last = m.get("content", "")
            break
    rnd = random.Random(len(last) * 7 + len(messages))
    out = {
        "dialogue": rnd.choice(_MOCK_LINES),
        "action": None,
        "observation": None,
        "task_signal": rnd.choice([None, "task_1", "task_2", "task_hidden"]),
        "deviation_level": None,
    }
    low = last.lower()
    if "7706" in low or "销毁" in last or "处理方案" in last:
        out["deviation_level"] = "high"
    if "你还好吗" in last or "没事" in last:
        out["action"] = "Ada 的头微微偏了一个角度。"
    return json.dumps(out, ensure_ascii=False)


_PRESETS = {
    "deepseek": {"model": "deepseek-chat", "base_url": "https://api.deepseek.com/v1",
                 "key_envs": ["DEEPSEEK_API_KEY"]},
    "openai": {"model": "gpt-4o-mini", "base_url": "https://api.openai.com/v1",
               "key_envs": ["OPENAI_API_KEY"]},
    "mock": {"model": "mock", "base_url": "", "key_envs": []},
}


def get_provider():
    name = os.environ.get("LLM_PROVIDER", "deepseek").lower()
    preset = _PRESETS.get(name, _PRESETS["deepseek"])
    key = ""
    for env in preset["key_envs"]:
        if os.environ.get(env):
            key = os.environ[env]
            break
    model = os.environ.get("LLM_MODEL", preset["model"])
    base_url = os.environ.get("LLM_BASE_URL", preset["base_url"])
    return Provider(name, key, model, base_url)


# ── tolerant JSON extraction ───────────────────────────────────────────────
def parse_ada_json(text):
    """Best-effort parse of the five-field Ada JSON. Never raises."""
    fallback = {"dialogue": None, "action": None, "observation": None,
                "task_signal": None, "deviation_level": None}
    if not text:
        return fallback
    raw = text.strip()
    # strip code fences if any
    raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.S)
    candidates = [raw]
    m = re.search(r"\{.*\}", raw, flags=re.S)
    if m:
        candidates.append(m.group(0))
    for cand in candidates:
        try:
            obj = json.loads(cand)
            if isinstance(obj, dict):
                out = dict(fallback)
                for k in out:
                    v = obj.get(k)
                    if isinstance(v, str):
                        v = v.strip()
                        if v.lower() in ("null", "none", ""):
                            v = None
                    out[k] = v
                # dialogue must be a string
                if not isinstance(out["dialogue"], str):
                    out["dialogue"] = None
                return out
        except Exception:
            continue
    # not JSON at all → treat whole text as dialogue
    out = dict(fallback)
    out["dialogue"] = raw[:600]
    return out
