from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import re
import time
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError, URLError
from pathlib import Path
from urllib.parse import urlparse
from urllib.request import Request, urlopen


HOME = Path(os.environ.get("WXBOT_HOME", "/wxbot-home")).resolve()
RUNTIME_HOME = os.environ.get("WXBOT_RUNTIME_HOME", "/home/wechat").rstrip("/")
CONFIG_PATH = Path(os.environ.get("WXBOT_CONFIG_PATH", str(HOME / ".config/wx-bot-client/config.json"))).resolve()
CLIENT_STATE_PATH = Path(
    os.environ.get(
        "WXBOT_CLIENT_STATE_PATH",
        str(HOME / ".local/share/wx-bot-sdk/client_state.json"),
    )
).resolve()
SDK_LOG_PATH = Path(os.environ.get("WXBOT_SDK_LOG_PATH", "/wxbot-logs/sdk.log")).resolve()
WXBOT_UID = int(os.environ.get("WXBOT_UID", "1001") or "1001")
WXBOT_GID = int(os.environ.get("WXBOT_GID", "1001") or "1001")
PORTAL_USER = os.environ.get("PORTAL_USER", "admin") or "admin"
PORTAL_PASSWORD = os.environ.get("PORTAL_PASSWORD", "")
PORTAL_SESSION_TTL = int(os.environ.get("PORTAL_SESSION_TTL", "43200") or "43200")
SDK_STATUS_URL = os.environ.get("WXBOT_SDK_STATUS_URL", "http://wx-bot-linux:5080/status").strip()
SDK_STATUS_TIMEOUT = float(os.environ.get("WXBOT_SDK_STATUS_TIMEOUT", "2.5") or "2.5")
SDK_GROUP_MODE_URL = os.environ.get(
    "WXBOT_SDK_GROUP_MODE_URL",
    f"{SDK_STATUS_URL.rsplit('/', 1)[0]}/debug/trigger-config" if SDK_STATUS_URL else "",
).strip()
SDK_STARTUP_GRACE_SEC = int(os.environ.get("WXBOT_SDK_STARTUP_GRACE_SEC", "180") or "180")
SESSION_COOKIE = "wxbot_portal_session"

if not PORTAL_PASSWORD:
    raise RuntimeError("PORTAL_PASSWORD must be non-empty")
if PORTAL_SESSION_TTL <= 0:
    raise RuntimeError("PORTAL_SESSION_TTL must be greater than zero")

SESSION_KEY = hashlib.pbkdf2_hmac(
    "sha256",
    PORTAL_PASSWORD.encode("utf-8"),
    b"wxbot-portal-session-v1\0" + PORTAL_USER.encode("utf-8"),
    200_000,
)


def _b64encode(value: bytes) -> str:
    return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")


def _b64decode(value: str) -> bytes:
    return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))


def _issue_session() -> str:
    now = int(time.time())
    payload = json.dumps(
        {"v": 1, "user": PORTAL_USER, "iat": now, "exp": now + PORTAL_SESSION_TTL},
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")
    encoded = _b64encode(payload)
    signature = _b64encode(hmac.new(SESSION_KEY, encoded.encode("ascii"), hashlib.sha256).digest())
    return f"{encoded}.{signature}"


def _validate_session(token: str) -> bool:
    try:
        encoded, signature = token.split(".", 1)
        expected = _b64encode(hmac.new(SESSION_KEY, encoded.encode("ascii"), hashlib.sha256).digest())
        if not hmac.compare_digest(signature.encode("ascii"), expected.encode("ascii")):
            return False
        payload = json.loads(_b64decode(encoded).decode("utf-8"))
        now = int(time.time())
        issued_at = int(payload.get("iat", 0))
        expires_at = int(payload.get("exp", 0))
        return (
            payload.get("v") == 1
            and hmac.compare_digest(str(payload.get("user", "")).encode("utf-8"), PORTAL_USER.encode("utf-8"))
            and issued_at <= now + 60
            and now < expires_at
            and 0 < expires_at - issued_at <= PORTAL_SESSION_TTL
        )
    except (ValueError, TypeError, UnicodeError, json.JSONDecodeError):
        return False


def _runtime_path(portal_path: Path) -> str:
    resolved = portal_path.expanduser().resolve()
    try:
        rel = resolved.relative_to(HOME)
    except ValueError:
        return str(resolved)
    return f"{RUNTIME_HOME}/{rel.as_posix()}"


def _portal_path(runtime_path: str) -> Path:
    text = str(runtime_path or "").strip()
    if text.startswith(RUNTIME_HOME + "/"):
        rel = text[len(RUNTIME_HOME) + 1 :]
        return (HOME / rel).resolve()
    return Path(text).expanduser().resolve()


def _derive_self_wxid(data_dir: str) -> str:
    name = Path(data_dir or "").name
    if not name:
        return ""
    match = re.match(r"^(.+)_[A-Za-z0-9]+$", name)
    return match.group(1) if match else name


def _account_dirs(root: Path) -> list[Path]:
    try:
        return sorted(
            (item for item in root.iterdir() if item.is_dir() and (item / "db_storage").exists()),
            key=lambda item: (item / "db_storage").stat().st_mtime,
            reverse=True,
        )
    except Exception:
        return []


def _candidate_roots() -> list[Path]:
    return [
        HOME / "Documents" / "xwechat_files",
        HOME / "文档" / "xwechat_files",
    ]


def _detect_accounts() -> list[dict]:
    accounts: list[dict] = []
    seen: set[str] = set()
    for root in _candidate_roots():
        for account in _account_dirs(root):
            runtime = _runtime_path(account)
            if runtime in seen:
                continue
            seen.add(runtime)
            accounts.append(
                {
                    "path": runtime,
                    "name": account.name,
                    "self_wxid": _derive_self_wxid(runtime),
                    "updated_ts": int((account / "db_storage").stat().st_mtime),
                }
            )
    return accounts


def _load_config() -> dict:
    try:
        if CONFIG_PATH.exists():
            return json.loads(CONFIG_PATH.read_text(encoding="utf-8-sig"))
    except Exception:
        pass
    return {
        "auth_base_url": os.environ.get("WXBOT_AUTH_BASE_URL", "https://auth.zhxapp.com"),
        "activation_code": "",
        "api_host": "0.0.0.0",
        "api_port": 5080,
        "sender_backend": "auto",
        "send_api_enabled": True,
        "send_start_paused": False,
        "ingest_start_paused": False,
        "group_require_at_me": False,
    }


def _load_client_identity() -> dict:
    identity = {
        "device_id": "",
        "device_public_key_sha256": "",
    }
    try:
        raw = json.loads(CLIENT_STATE_PATH.read_text(encoding="utf-8-sig"))
    except (OSError, UnicodeError, json.JSONDecodeError):
        return identity
    if not isinstance(raw, dict):
        return identity
    for key in identity:
        value = raw.get(key)
        if isinstance(value, str):
            identity[key] = value.strip()
    return identity


def _write_config(raw: dict) -> None:
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    temp_path = CONFIG_PATH.with_name(f".{CONFIG_PATH.name}.tmp")
    temp_path.write_text(json.dumps(raw, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    temp_path.replace(CONFIG_PATH)
    try:
        os.chown(CONFIG_PATH, WXBOT_UID, WXBOT_GID)
        os.chmod(CONFIG_PATH, 0o600)
    except Exception:
        pass


def _as_bool(value: object, default: bool) -> bool:
    if value is None:
        return default
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return value != 0
    text = str(value).strip().lower()
    if text in {"1", "true", "yes", "on"}:
        return True
    if text in {"0", "false", "no", "off"}:
        return False
    return default


def _apply_message_settings(raw: dict, body: dict) -> tuple[bool, bool, bool]:
    receive_enabled = _as_bool(
        body.get("receive_enabled"),
        not _as_bool(raw.get("ingest_start_paused"), False),
    )
    send_enabled = _as_bool(
        body.get("send_enabled"),
        _as_bool(raw.get("send_api_enabled"), True)
        and not _as_bool(raw.get("send_start_paused"), False),
    )
    group_require_at_me = _as_bool(
        body.get("group_require_at_me"),
        _as_bool(raw.get("group_require_at_me", raw.get("ingest_group_mentions_only")), False),
    )
    raw["send_api_enabled"] = send_enabled
    raw["send_start_paused"] = not send_enabled
    raw["ingest_start_paused"] = not receive_enabled
    raw["group_require_at_me"] = group_require_at_me
    raw["ingest_group_mentions_only"] = group_require_at_me
    return receive_enabled, send_enabled, group_require_at_me


def _sdk_last_error() -> str:
    try:
        with SDK_LOG_PATH.open("rb") as handle:
            handle.seek(0, os.SEEK_END)
            size = handle.tell()
            handle.seek(max(0, size - 131_072), os.SEEK_SET)
            lines = handle.read().decode("utf-8", errors="replace").splitlines()
    except Exception:
        return ""

    markers = (
        "[auth] refresh failed:",
        "[main] initial decrypt failed:",
        "[decrypt] initial decrypt failed:",
        "[sdk] startup failed:",
    )
    translations = {
        "activation_code already bound to another device": "激活码已绑定到另一台设备，请换绑或使用新的激活码",
        "activation_code_already_consumed": "激活码已绑定原设备；请在管理端为本机创建安全换绑窗口后重新激活",
        "invalid activation_code": "激活码无效",
        "invalid_activation_code": "激活码无效",
        "activation_code expired": "激活码已过期",
        "activation_code_expired": "激活码已过期",
        "activation_code revoked": "激活码已被吊销",
        "activation_code_revoked": "激活码已被吊销",
        "license_already_online": "该授权已有活动实例在线；请先停止原实例，等待租约过期或在管理端吊销设备",
        "session_replayed": "检测到会话凭证被重复使用，授权会话已安全吊销",
        "session_inactive": "授权会话已失效，请停止可能存在的其他实例后重试",
        "session_expired": "授权租约已过期，客户端将重新建立会话",
        "device_revoked": "当前设备授权已被吊销",
        "client_instance_already_running": "同一数据目录已有 SDK 实例运行",
        "device proof invalid": "设备身份签名校验失败",
        "invalid_device_proof": "设备身份签名校验失败",
        "device_public_key_mismatch": "设备身份密钥不匹配；如密钥丢失请在管理端换绑",
    }
    for line in reversed(lines):
        text = line.strip()
        if "[sdk] configuration ready; starting wx-bot-client" in text:
            return ""
        if "初始解密未完成" in text:
            return "首次数据库解密失败，请确认微信已登录且 SDK 具有 SYS_PTRACE 权限"
        for marker in markers:
            if marker not in text:
                continue
            detail = text.split(marker, 1)[1].strip()
            return translations.get(detail, detail[:240])
    return ""


def _mask_activation_code(value: str) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    if len(text) <= 8:
        return text[:2] + "***" + text[-2:]
    return text[:8] + "..." + text[-4:]


def _is_authorization_error(message: str) -> bool:
    text = str(message or "").strip().lower()
    return any(
        marker in text
        for marker in (
            "激活码",
            "授权",
            "activation_code",
            "activation code",
            "license expired",
            "license revoked",
        )
    )


def _config_updated_ts() -> int:
    try:
        return int(CONFIG_PATH.stat().st_mtime)
    except OSError:
        return 0


def _fetch_sdk_status() -> tuple[dict | None, str]:
    if not SDK_STATUS_URL:
        return None, "SDK status URL is not configured"
    try:
        request = Request(SDK_STATUS_URL, headers={"Accept": "application/json"})
        with urlopen(request, timeout=SDK_STATUS_TIMEOUT) as response:
            payload = json.loads(response.read().decode("utf-8"))
        if not isinstance(payload, dict):
            return None, "SDK status response is not an object"
        return payload, ""
    except (HTTPError, URLError, TimeoutError, OSError, ValueError, json.JSONDecodeError) as exc:
        return None, str(exc)[:240]


def _apply_live_group_mode(enabled: bool) -> tuple[bool, str]:
    if not SDK_GROUP_MODE_URL:
        return False, "SDK group mode URL is not configured"
    try:
        body = json.dumps({"group_require_at_me": bool(enabled)}).encode("utf-8")
        request = Request(
            SDK_GROUP_MODE_URL,
            data=body,
            method="POST",
            headers={"Accept": "application/json", "Content-Type": "application/json"},
        )
        with urlopen(request, timeout=SDK_STATUS_TIMEOUT) as response:
            payload = json.loads(response.read().decode("utf-8"))
        if not isinstance(payload, dict):
            return False, "SDK group mode response is not an object"
        if _as_bool(payload.get("group_require_at_me"), not enabled) != bool(enabled):
            return False, "SDK did not confirm the requested group mode"
        return True, ""
    except (HTTPError, URLError, TimeoutError, OSError, ValueError, json.JSONDecodeError) as exc:
        return False, str(exc)[:240]


def _classify_sdk_runtime(
    raw: dict,
    *,
    sdk_status: dict | None,
    sdk_error: str = "",
    last_error: str = "",
    now_ts: int | None = None,
    config_updated_ts: int | None = None,
) -> dict:
    now = int(now_ts if now_ts is not None else time.time())
    updated_ts = int(config_updated_ts if config_updated_ts is not None else _config_updated_ts())
    activation_code = str(raw.get("activation_code") or "").strip()
    auth_base_url = str(raw.get("auth_base_url") or "").strip()
    data_dir = str(raw.get("wechat_data_dir_linux") or raw.get("wechat_data_dir") or "").strip()
    activation_configured = bool(activation_code)
    account_configured = bool(data_dir)
    config_ready = bool(activation_configured and auth_base_url and account_configured)
    sdk_online = isinstance(sdk_status, dict)
    auth_active = bool((sdk_status or {}).get("auth_active"))

    receive_enabled = not _as_bool(raw.get("ingest_start_paused"), False)
    send_api_enabled = _as_bool(raw.get("send_api_enabled"), True)
    send_enabled = send_api_enabled and not _as_bool(raw.get("send_start_paused"), False)
    group_require_at_me = _as_bool(
        raw.get("group_require_at_me", raw.get("ingest_group_mentions_only")),
        False,
    )
    if sdk_online:
        paused = (sdk_status or {}).get("paused") or {}
        receive_enabled = not bool(paused.get("ingest_paused"))
        send_enabled = send_api_enabled and not bool(paused.get("send_paused"))
        runtime_config = (sdk_status or {}).get("config") or {}
        if "group_require_at_me" in runtime_config:
            group_require_at_me = _as_bool(runtime_config.get("group_require_at_me"), False)

    if not activation_configured or not auth_base_url:
        phase = "unconfigured"
        label = "等待配置"
        detail = "填写授权服务地址和 Activation Code 后保存"
    elif not account_configured:
        phase = "waiting_wechat"
        label = "等待微信登录"
        detail = "扫码登录微信并选择检测到的账号目录"
    elif last_error and _is_authorization_error(last_error):
        phase = "authorization_error"
        label = "授权未生效"
        detail = last_error
    elif sdk_online and auth_active:
        phase = "active"
        label = "已激活并运行"
        detail = "SDK 授权有效，消息服务已启动"
    elif sdk_online:
        phase = "authorization_error"
        label = "授权未生效"
        detail = last_error or "SDK 已启动，但授权状态未激活"
    elif last_error:
        phase = "error"
        label = "启动失败"
        detail = last_error
    elif config_ready and updated_ts and now - updated_ts <= SDK_STARTUP_GRACE_SEC:
        phase = "starting"
        label = "正在启动"
        detail = "配置已保存，SDK 正在授权或初始化数据库"
    else:
        phase = "offline"
        label = "SDK 离线"
        detail = "配置已完整，但暂时无法连接 SDK API"

    return {
        "phase": phase,
        "label": label,
        "detail": detail,
        "sdk_online": sdk_online,
        "auth_active": auth_active,
        "config_ready": config_ready,
        "activation": {
            "configured": activation_configured,
            "masked": _mask_activation_code(activation_code),
        },
        "account": {
            "configured": account_configured,
            "data_dir": data_dir,
        },
        "receive_enabled": receive_enabled,
        "send_enabled": send_enabled,
        "send_api_enabled": send_api_enabled,
        "group_require_at_me": group_require_at_me,
        "group_capture_mode": "mention_only" if group_require_at_me else "all_group_messages",
        "api_port": int(raw.get("api_port", 5080) or 5080),
        "last_error": last_error,
        "connection_error": str(sdk_error or "")[:240],
        "config_updated_ts": updated_ts,
        "checked_ts": now,
    }


def _sdk_runtime_snapshot(raw: dict) -> dict:
    sdk_status, sdk_error = _fetch_sdk_status()
    snapshot = _classify_sdk_runtime(
        raw,
        sdk_status=sdk_status,
        sdk_error=sdk_error,
        last_error=_sdk_last_error(),
    )
    snapshot["device_identity"] = _load_client_identity()
    return snapshot


def _validate_account_dir(value: str) -> tuple[str, str]:
    runtime = str(value or "").strip()
    if not runtime:
        raise ValueError("请选择微信数据目录")
    portal = _portal_path(runtime)
    if not portal.exists() or not portal.is_dir():
        raise ValueError(f"目录不存在：{runtime}")
    if (portal / "db_storage").exists():
        resolved_runtime = _runtime_path(portal)
        return resolved_runtime, _derive_self_wxid(resolved_runtime)
    accounts = _account_dirs(portal)
    if len(accounts) == 1:
        resolved_runtime = _runtime_path(accounts[0])
        return resolved_runtime, _derive_self_wxid(resolved_runtime)
    if len(accounts) > 1:
        raise ValueError("该目录下有多个微信账号，请选择具体账号目录")
    raise ValueError("所选目录不是微信账号目录，缺少 db_storage")


class Handler(BaseHTTPRequestHandler):
    server_version = "wxbot-portal-config/1.0"

    def _send(self, status: int, payload: dict, headers: dict[str, str] | None = None) -> None:
        data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        for name, value in (headers or {}).items():
            self.send_header(name, value)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def _read_json(self) -> dict:
        length = int(self.headers.get("content-length") or "0")
        if length <= 0:
            return {}
        return json.loads(self.rfile.read(length).decode("utf-8"))

    def _is_https(self) -> bool:
        forwarded = self.headers.get("X-Forwarded-Proto", "")
        return forwarded.split(",", 1)[0].strip().lower() == "https"

    def _session_token(self) -> str:
        try:
            cookie = SimpleCookie()
            cookie.load(self.headers.get("Cookie", ""))
            morsel = cookie.get(SESSION_COOKIE)
            return morsel.value if morsel else ""
        except Exception:
            return ""

    def _authenticated(self) -> bool:
        token = self._session_token()
        return bool(token) and _validate_session(token)

    def _require_auth(self) -> bool:
        if self._authenticated():
            return True
        self._send(401, {"ok": False, "error": "authentication required"})
        return False

    def _session_cookie(self, token: str, *, clear: bool = False) -> str:
        if clear:
            value = (
                f"{SESSION_COOKIE}=; Path=/; Max-Age=0; "
                "Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict"
            )
        else:
            value = (
                f"{SESSION_COOKIE}={token}; Path=/; Max-Age={PORTAL_SESSION_TTL}; "
                "HttpOnly; SameSite=Strict"
            )
        if self._is_https():
            value += "; Secure"
        return value

    def _login(self) -> None:
        try:
            body = self._read_json()
            supplied_user = str(body.get("user", "")).encode("utf-8")
            supplied_password = str(body.get("password", "")).encode("utf-8")
        except Exception:
            supplied_user = b""
            supplied_password = b""

        user_matches = hmac.compare_digest(supplied_user, PORTAL_USER.encode("utf-8"))
        password_matches = hmac.compare_digest(supplied_password, PORTAL_PASSWORD.encode("utf-8"))
        if not (user_matches and password_matches):
            time.sleep(0.25)
            self._send(401, {"ok": False, "error": "invalid credentials"})
            return

        token = _issue_session()
        self._send(
            200,
            {"ok": True, "authenticated": True, "user": PORTAL_USER},
            {"Set-Cookie": self._session_cookie(token)},
        )

    def do_GET(self) -> None:
        path = urlparse(self.path).path
        if path == "/auth/check":
            if self._authenticated():
                self._send(200, {"ok": True, "authenticated": True, "user": PORTAL_USER})
            else:
                self._send(401, {"ok": False, "authenticated": False})
            return
        if path == "/status":
            if not self._require_auth():
                return
            raw = _load_config()
            self._send(200, {"ok": True, "sdk_runtime": _sdk_runtime_snapshot(raw)})
            return
        if path != "/config":
            self._send(404, {"ok": False, "error": "not found"})
            return
        if not self._require_auth():
            return
        raw = _load_config()
        data_dir = str(raw.get("wechat_data_dir_linux") or raw.get("wechat_data_dir") or "")
        accounts = _detect_accounts()
        receive_enabled = not _as_bool(raw.get("ingest_start_paused"), False)
        send_enabled = _as_bool(raw.get("send_api_enabled"), True) and not _as_bool(
            raw.get("send_start_paused"), False
        )
        group_require_at_me = _as_bool(
            raw.get("group_require_at_me", raw.get("ingest_group_mentions_only")),
            False,
        )
        self._send(
            200,
            {
                "ok": True,
                "config_path": _runtime_path(CONFIG_PATH),
                "desktop": {
                    "vnc_password": os.environ.get("VNC_PASSWORD", ""),
                },
                "sdk_runtime": _sdk_runtime_snapshot(raw),
                "accounts": accounts,
                "config": {
                    "auth_base_url": str(raw.get("auth_base_url") or ""),
                    "activation_code": str(raw.get("activation_code") or ""),
                    "wechat_data_dir_linux": data_dir,
                    "self_wxid_linux": str(raw.get("self_wxid_linux") or raw.get("self_wxid") or ""),
                    "sender_backend": str(raw.get("sender_backend") or "auto"),
                    "receive_enabled": receive_enabled,
                    "send_enabled": send_enabled,
                    "group_require_at_me": group_require_at_me,
                },
            },
        )

    def do_POST(self) -> None:
        path = urlparse(self.path).path
        if path == "/auth/login":
            self._login()
            return
        if path == "/auth/logout":
            self._send(
                200,
                {"ok": True, "authenticated": False},
                {"Set-Cookie": self._session_cookie("", clear=True)},
            )
            return
        if path == "/message-settings":
            if not self._require_auth():
                return
            try:
                body = self._read_json()
                if "group_require_at_me" not in body:
                    raise ValueError("缺少群消息入队范围")
                group_require_at_me = _as_bool(body.get("group_require_at_me"), False)
                raw = _load_config()
                raw["group_require_at_me"] = group_require_at_me
                raw["ingest_group_mentions_only"] = group_require_at_me
                _write_config(raw)
                applied_live, runtime_error = _apply_live_group_mode(group_require_at_me)
                self._send(
                    200,
                    {
                        "ok": True,
                        "sdk_action": "hot_reload" if applied_live else "saved_for_next_start",
                        "applied_live": applied_live,
                        "runtime_error": runtime_error,
                        "group_require_at_me": group_require_at_me,
                    },
                )
            except Exception as exc:
                self._send(400, {"ok": False, "error": str(exc)})
            return
        if path != "/config":
            self._send(404, {"ok": False, "error": "not found"})
            return
        if not self._require_auth():
            return
        try:
            body = self._read_json()
            auth_base_url = str(body.get("auth_base_url") or "").strip()
            activation_code = str(body.get("activation_code") or "").strip()
            if not auth_base_url:
                raise ValueError("请填写授权服务地址")
            if not activation_code:
                raise ValueError("请填写 activation code")
            data_dir, detected_wxid = _validate_account_dir(str(body.get("wechat_data_dir_linux") or ""))
            self_wxid = str(body.get("self_wxid_linux") or "").strip() or detected_wxid
            if not self_wxid:
                raise ValueError("无法识别 self_wxid，请手工填写")
            raw = _load_config()
            raw["auth_base_url"] = auth_base_url
            raw["activation_code"] = activation_code
            raw["wechat_data_dir"] = data_dir
            raw["wechat_data_dir_linux"] = data_dir
            raw["self_wxid"] = self_wxid
            raw["self_wxid_linux"] = self_wxid
            raw["sender_backend"] = str(body.get("sender_backend") or raw.get("sender_backend") or "auto")
            raw.setdefault("api_host", "0.0.0.0")
            raw.setdefault("api_port", 5080)
            receive_enabled, send_enabled, group_require_at_me = _apply_message_settings(raw, body)
            _write_config(raw)
            self._send(
                200,
                {
                    "ok": True,
                    "config_path": _runtime_path(CONFIG_PATH),
                    "sdk_action": "start_or_restart",
                    "config": {
                        "auth_base_url": auth_base_url,
                        "activation_code": activation_code,
                        "wechat_data_dir_linux": data_dir,
                        "self_wxid_linux": self_wxid,
                        "sender_backend": raw["sender_backend"],
                        "receive_enabled": receive_enabled,
                        "send_enabled": send_enabled,
                        "group_require_at_me": group_require_at_me,
                    },
                },
            )
        except Exception as exc:
            self._send(400, {"ok": False, "error": str(exc)})

    def log_message(self, fmt: str, *args) -> None:
        print("[config-api]", fmt % args, flush=True)


if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 18080), Handler).serve_forever()
