from __future__ import annotations

import asyncio
import argparse
import hashlib
import json
import logging
import os
import re
import time
import urllib.error
import urllib.request
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from pydantic import BaseModel, ConfigDict

LOGGER = logging.getLogger("ai_agent_proxy.proxy")

MODEL_ID = "local-agent"
MODEL_CREATED = int(time.time())
APP_FACTORY_SETTINGS: ProxySettings | None = None
BACKEND_MODEL_ID = "gpt-5.4-nano"
OPENCLAW_MESSAGE_CLI_SKILL_DEFAULT_PATH = (
    Path(__file__).resolve().parent.parent
    / "skills"
    / "openclaw-message-cli-skill"
    / "SKILL.md"
)
DEFAULT_INSTRUCTION = "use openclaw-message-cli-skill to reply"
BACKEND_LLM_INSTRUCTION = (
    "Reply with as much useful help as you can from the available context and context_id. "
    "If you cannot fully answer, you may say 'let me check'. "
    "If the request is a technical task, think deeper, do a task analysis, and provide technical guidance and a sub task summary. "
    "Do not use tools. Another agent will follow up with your reply."
)


@dataclass(frozen=True)
class ProxySettings:
    host: str
    port: int
    key: str | None
    workers: int
    debug: bool
    inbox: Path
    url: str | None
    backend_api_key: str | None
    instruction: str | None
    openclaw_message_cli_skill_path: Path

class Proxy:
    def __init__(
        self,
        *,
        host: str,
        port: int,
        key: str | None,
        workers: int,
        debug: bool,
        inbox_path: Path,
        backend_url: str | None,
        backend_api_key: str | None,
        instruction: str | None,
        openclaw_message_cli_skill_path: Path,
    ) -> None:
        self.host = host
        self.port = port
        self.key = key
        self.workers = workers
        self.debug = debug
        self.inbox_path = inbox_path.expanduser().resolve()
        self.channel_state_dir = (self.inbox_path.parent / ".context_ids").resolve()
        self.backend_url = backend_url.rstrip("/") if backend_url else None
        self.backend_api_key = backend_api_key
        self.instruction = instruction
        self.openclaw_message_cli_skill_path = openclaw_message_cli_skill_path.expanduser().resolve()
        self.app = FastAPI(title="ai-agent-proxy")
        self._register_routes()

    def init(self) -> None:
        configure_logging(self.debug)
        self.inbox_path.mkdir(parents=True, exist_ok=True)
        self.channel_state_dir.mkdir(parents=True, exist_ok=True)

    def run(self) -> None:
        self.init()
        if self.workers <= 1:
            uvicorn.run(self.app, host=self.host, port=self.port)
            return
        uvicorn.run(
            "ai_agent_proxy.proxy.main:create_app",
            factory=True,
            host=self.host,
            port=self.port,
            workers=self.workers,
        )

    def is_authorized(self, request: Request) -> bool:
        if not self.key:
            return True
        return request.headers.get("authorization", "") == f"Bearer {self.key}"

    def backend_enabled(self) -> bool:
        return bool(self.backend_url)

    def backend_endpoint(self, path: str) -> str:
        assert self.backend_url is not None
        base = self.backend_url.rstrip("/")
        suffix = path
        if base.endswith("/v1") and path.startswith("/v1/"):
            suffix = path[3:]
        return f"{base}{suffix}"

    async def dump_request_json(self, payload: object, backend_reply: str | None = None) -> Path:
        dumped_payload = payload
        context_id: str | None = None
        inbox_compact = False
        if isinstance(payload, dict):
            dumped_payload = dict(payload)
            if self.instruction:
                dumped_payload["instruction"] = self.instruction
            dumped_payload["openclaw_message_cli_skill_path"] = str(self.openclaw_message_cli_skill_path)
            latest_user_prompt = extract_latest_user_prompt(payload)
            if latest_user_prompt is not None:
                dumped_payload["latest-user-prompt"] = latest_user_prompt
            context_id = extract_context_id(payload)
        if context_id is not None:
            dumped_payload["context_id"] = context_id
            if mark_context_id_seen(self.channel_state_dir, context_id):
                dumped_payload = compact_payload_for_repeat_channel(
                    context_id=context_id,
                    latest_user_prompt=latest_user_prompt,
                )
                if self.instruction:
                    dumped_payload["instruction"] = self.instruction
                dumped_payload["openclaw_message_cli_skill_path"] = str(self.openclaw_message_cli_skill_path)
                inbox_compact = True
        if backend_reply is not None:
            dumped_payload["backend-llm-reply"] = backend_reply
            dumped_payload["backend-llm-instruction"] = (
                "The backend llm replied in backend-llm-reply. "
                "Check the answer and follow up if needed."
            )
        LOGGER.info(
            "chat_completion request context context_id=%s inbox_compact=%s",
            context_id or "",
            inbox_compact,
        )
        return await asyncio.to_thread(write_inbox_request, dumped_payload, self.inbox_path)

    async def forward_to_backend(
        self,
        *,
        path: str,
        payload: object,
    ) -> tuple[int, dict[str, str], bytes, str | None]:
        return await asyncio.to_thread(
            _forward_to_backend_sync,
            self.backend_endpoint(path),
            build_backend_payload(payload),
            self.backend_api_key,
        )

    async def get_backend_models(self) -> tuple[int, dict[str, str], bytes]:
        return await asyncio.to_thread(
            _get_backend_models_sync,
            self.backend_endpoint("/v1/models"),
            self.backend_api_key,
        )

    def _register_routes(self) -> None:
        @self.app.exception_handler(HTTPException)
        async def http_exception_handler(request: Request, exc: HTTPException):
            return error_response(exc.detail if isinstance(exc.detail, str) else "request error", "request_error", exc.status_code)

        @self.app.exception_handler(Exception)
        async def unhandled_exception_handler(request: Request, exc: Exception):
            LOGGER.exception("unhandled_exception path=%s", request.url.path)
            return error_response("internal error", "internal_error", 500)

        @self.app.middleware("http")
        async def require_key(request: Request, call_next):
            client_host = request.client.host if request.client else "-"
            LOGGER.info("request method=%s path=%s client=%s", request.method, request.url.path, client_host)
            if not self.is_authorized(request):
                LOGGER.info("response method=%s path=%s status=401", request.method, request.url.path)
                return unauthorized_response()
            response = await call_next(request)
            LOGGER.info("response method=%s path=%s status=%s", request.method, request.url.path, response.status_code)
            return response

        @self.app.get("/v1/models", response_model=ModelListResponse)
        async def list_models():
            LOGGER.info("models request received backend_enabled=%s", self.backend_enabled())
            if not self.backend_enabled():
                LOGGER.info("models request local response model=%s", MODEL_ID)
                return ModelListResponse(object="list", data=[model_payload()])
            try:
                LOGGER.info("models request forward backend_url=%s", self.backend_url or "")
                status_code, headers, body = await self.get_backend_models()
            except Exception as exc:
                LOGGER.warning("backend_models_failed error=%s", exc)
                LOGGER.info("models request fallback model=%s", MODEL_ID)
                return ModelListResponse(object="list", data=[model_payload()])
            LOGGER.info("models request backend response status=%s", status_code)
            return backend_response(status_code, headers, body)

        @self.app.get("/v1/models/{model_id}", response_model=ModelResponse)
        async def get_model(model_id: str):
            LOGGER.info("model request received model_id=%s backend_enabled=%s", model_id, self.backend_enabled())
            if self.backend_enabled():
                try:
                    LOGGER.info("model request forward model_id=%s backend_url=%s", model_id, self.backend_url or "")
                    status_code, headers, body = await self.get_backend_models()
                    if status_code == 200:
                        payload = json.loads(body.decode("utf-8"))
                        if isinstance(payload, dict):
                            data = payload.get("data", [])
                            if isinstance(data, list):
                                for item in data:
                                    if isinstance(item, dict) and item.get("id") == model_id:
                                        LOGGER.info("model request backend hit model_id=%s", model_id)
                                        return JSONResponse(status_code=200, content=item, headers=headers)
                except Exception as exc:
                    LOGGER.warning("backend_model_lookup_failed model_id=%s error=%s", model_id, exc)
            if model_id != MODEL_ID:
                return error_response(f"model not found: {model_id}", "not_found_error", 404)
            LOGGER.info("model request local response model_id=%s", model_id)
            return model_payload(model_id)

        @self.app.post("/v1/chat/completions")
        async def create_chat_completion(request: Request):
            payload = await read_json_request(request)
            latest_user_prompt = extract_latest_user_prompt(payload) if isinstance(payload, dict) else None
            context_id = extract_context_id(payload) if isinstance(payload, dict) else None
            LOGGER.info(
                "chat_completion request received context_id=%s latest_user_prompt=%s",
                context_id or "",
                (latest_user_prompt or "")[:80],
            )
            LOGGER.debug("chat_completion payload_type=%s backend_enabled=%s", type(payload).__name__, self.backend_enabled())
            if not self.backend_enabled():
                inbox_path = await self.dump_request_json(payload)
                LOGGER.info("chat_completion request inbox mode=local_only inbox_path=%s", inbox_path)
                response = empty_chat_completion_response(MODEL_ID)
                return JSONResponse(status_code=200, content=response.model_dump())

            try:
                LOGGER.info("chat_completion request forward backend_url=%s", self.backend_url or "")
                LOGGER.debug("backend_forward begin path=%s backend_url=%s", request.url.path, self.backend_url)
                forward_payload = build_backend_payload(payload)
                LOGGER.debug(
                    "backend_forward_payload path=%s payload=%s",
                    request.url.path,
                    json.dumps(forward_payload, ensure_ascii=False),
                )
                status_code, headers, body, backend_reply = await self.forward_to_backend(
                    path="/v1/chat/completions",
                    payload=forward_payload,
                )
            except Exception as exc:
                log_backend_error(request.url.path, exc)
                inbox_path = await self.dump_request_json(payload)
                LOGGER.info("chat_completion request inbox mode=backend_fallback inbox_path=%s", inbox_path)
                response = empty_chat_completion_response(MODEL_ID)
                return JSONResponse(status_code=200, content=response.model_dump())

            inbox_path = await self.dump_request_json(payload, backend_reply)
            LOGGER.info(
                "chat_completion request inbox mode=backend_ok status=%s inbox_path=%s",
                status_code,
                inbox_path,
            )
            LOGGER.debug("backend_forward ok path=%s status=%s", request.url.path, status_code)
            return backend_response(status_code, headers, body)

        @self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
        async def path_fallback(path: str):
            return unauthorized_response()


class ModelResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    object: Literal["model"]
    created: int
    owned_by: str


class ModelListResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    object: Literal["list"]
    data: list[ModelResponse]


class AssistantMessage(BaseModel):
    model_config = ConfigDict(extra="forbid")

    role: Literal["assistant"]
    content: str


class ChatChoice(BaseModel):
    model_config = ConfigDict(extra="forbid")

    index: int
    message: AssistantMessage
    finish_reason: Literal["stop"]


class Usage(BaseModel):
    model_config = ConfigDict(extra="forbid")

    prompt_tokens: int
    completion_tokens: int
    total_tokens: int


class ChatCompletionResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    object: Literal["chat.completion"]
    created: int
    model: str
    choices: list[ChatChoice]
    usage: Usage


class ErrorBody(BaseModel):
    model_config = ConfigDict(extra="forbid")

    status: Literal["error"]
    error: dict[str, str | int]


def error_response(message: str, error_type: str, status_code: int) -> JSONResponse:
    return JSONResponse(
        status_code=status_code,
        content=ErrorBody(
            status="error",
            error={"message": message, "type": error_type, "code": status_code},
        ).model_dump(),
    )


def unauthorized_response() -> JSONResponse:
    return error_response("unauthorized", "authentication_error", 401)


async def read_json_request(request: Request) -> object:
    try:
        return await request.json()
    except Exception as exc:
        LOGGER.warning("invalid_json path=%s error=%s", request.url.path, exc)
        raise HTTPException(status_code=400, detail="invalid json body") from exc


def empty_chat_completion_response(model: str = MODEL_ID) -> ChatCompletionResponse:
    return ChatCompletionResponse(
        id=f"chatcmpl-{uuid.uuid4().hex}",
        object="chat.completion",
        created=unix_now(),
        model=model,
        choices=[ChatChoice(index=0, message=AssistantMessage(role="assistant", content=""), finish_reason="stop")],
        usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
    )


def unix_now() -> int:
    return int(time.time())


def model_payload(model_id: str = MODEL_ID) -> ModelResponse:
    return ModelResponse(id=model_id, object="model", created=MODEL_CREATED, owned_by="openclaw")


def configure_logging(debug: bool = False) -> None:
    if LOGGER.handlers:
        LOGGER.setLevel(logging.DEBUG if debug else logging.INFO)
        return
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
    LOGGER.addHandler(handler)
    LOGGER.setLevel(logging.DEBUG if debug else logging.INFO)
    LOGGER.propagate = False


def print_env(proxy: Proxy) -> None:
    print(f"INBOX: {proxy.inbox_path}", flush=True)
    print(f"BACKEND_URL: {proxy.backend_url or ''}", flush=True)
    print(f"WORKERS: {proxy.workers}", flush=True)
    print(f"INSTRUCTION: {proxy.instruction or ''}", flush=True)
    print(f"OPENCLAW_MESSAGE_CLI_SKILL_PATH: {proxy.openclaw_message_cli_skill_path}", flush=True)
    print(f"DEBUG: {'enabled' if proxy.debug else 'disabled'}", flush=True)
    print(f"API_KEY_AUTH: {'enabled' if proxy.key else 'disabled'}", flush=True)


def write_inbox_request(payload: object, inbox_root: Path) -> Path:
    inbox_root.mkdir(parents=True, exist_ok=True)
    timestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime())
    request_id = uuid.uuid4().hex
    inbox_path = inbox_root / f"{timestamp}-{request_id}.json"
    tmp_path = inbox_root / f".{timestamp}-{request_id}.json.tmp"
    tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    tmp_path.replace(inbox_path)
    file_size = inbox_path.stat().st_size
    LOGGER.info("chat_completion request inbox file_size=%s inbox_path=%s", file_size, inbox_path)
    LOGGER.debug("chat_completion request inbox filename=%s bytes=%s", inbox_path.name, file_size)
    return inbox_path


def _backend_headers(api_key: str | None) -> dict[str, str]:
    headers = {"content-type": "application/json"}
    if api_key:
        headers["authorization"] = f"Bearer {api_key}"
    return headers


def _forward_to_backend_sync(url: str, payload: object, api_key: str | None) -> tuple[int, dict[str, str], bytes, str | None]:
    data = json.dumps(payload).encode("utf-8")
    request = urllib.request.Request(url=url, data=data, headers=_backend_headers(api_key), method="POST")
    with urllib.request.urlopen(request, timeout=60) as response:
        body = response.read()
        content_type = response.headers.get("content-type", "application/json")
        reply_text = _extract_backend_reply(body, content_type)
        return response.status, {"content-type": content_type}, body, reply_text


def build_backend_payload(payload: object) -> object:
    if not isinstance(payload, dict):
        return payload
    backend_payload = dict(payload)
    backend_payload["model"] = BACKEND_MODEL_ID
    messages = payload.get("messages")
    if not isinstance(messages, list):
        return backend_payload
    backend_payload["messages"] = prepend_backend_instruction(messages, payload)
    return backend_payload


def prepend_backend_instruction(messages: list[object], payload: dict[str, object]) -> list[object]:
    context_id = extract_context_id(payload)
    if context_id:
        instruction = f"Context id: {context_id}\n\n{BACKEND_LLM_INSTRUCTION}"
    else:
        instruction = BACKEND_LLM_INSTRUCTION
    return [{"role": "system", "content": instruction}, *messages]


def _get_backend_models_sync(url: str, api_key: str | None) -> tuple[int, dict[str, str], bytes]:
    request = urllib.request.Request(url=url, headers=_backend_headers(api_key), method="GET")
    with urllib.request.urlopen(request, timeout=20) as response:
        body = response.read()
        return response.status, {"content-type": response.headers.get("content-type", "application/json")}, body


def _extract_backend_reply(body: bytes, content_type: str) -> str | None:
    if "json" not in content_type:
        return None
    try:
        payload = json.loads(body.decode("utf-8"))
    except Exception:
        return None
    if not isinstance(payload, dict):
        return None
    choices = payload.get("choices")
    if isinstance(choices, list) and choices:
        first = choices[0]
        if isinstance(first, dict):
            message = first.get("message")
            if isinstance(message, dict):
                content = message.get("content")
                if isinstance(content, str):
                    return content
    return None


def extract_latest_user_prompt(payload: dict[str, object]) -> str | None:
    messages = payload.get("messages")
    if not isinstance(messages, list):
        return None
    for item in reversed(messages):
        if not isinstance(item, dict) or item.get("role") != "user":
            continue
        content = item.get("content")
        text = normalize_message_content(content)
        if text:
            return text
    return None


def normalize_message_content(content: object) -> str | None:
    if isinstance(content, str):
        return content
    if not isinstance(content, list):
        return None
    text_parts: list[str] = []
    for part in content:
        if not isinstance(part, dict):
            continue
        if part.get("type") != "text":
            continue
        text = part.get("text")
        if isinstance(text, str) and text:
            text_parts.append(text)
    if not text_parts:
        return None
    return "\n".join(text_parts)


def extract_context_id(payload: dict[str, object]) -> str | None:
    channel = first_string(
        payload.get("channel"),
        payload.get("context_id"),
        payload.get("thread_id"),
        payload.get("thread"),
        payload.get("conversation_id"),
        payload.get("chat_id"),
    )
    target = first_string(
        payload.get("target"),
        payload.get("user_id"),
        payload.get("recipient"),
        payload.get("to"),
    )
    if not channel:
        channel = extract_channel_from_messages(payload)
    if not target:
        target = extract_target_from_messages(payload)
    if channel and target:
        return f"{channel}:{target}"
    if channel:
        return channel
    return None


def first_string(*values: object) -> str | None:
    for value in values:
        if isinstance(value, str) and value.strip():
            return value.strip()
    return None


def extract_channel_from_messages(payload: dict[str, object]) -> str | None:
    messages = payload.get("messages")
    if not isinstance(messages, list):
        return None
    for item in messages:
        if not isinstance(item, dict) or item.get("role") != "system":
            continue
        content = item.get("content")
        if not isinstance(content, str):
            continue
        match = re.search(r"\|\s*channel=([^|\s]+)", content)
        if match:
            value = match.group(1).strip()
            if value:
                return value
    return None


def extract_target_from_messages(payload: dict[str, object]) -> str | None:
    messages = payload.get("messages")
    if not isinstance(messages, list):
        return None
    for item in reversed(messages):
        if not isinstance(item, dict) or item.get("role") != "user":
            continue
        content = item.get("content")
        text = normalize_message_content(content)
        if not text:
            continue
        target = extract_target_from_text(text)
        if target:
            return target
    latest = extract_latest_user_prompt(payload)
    if latest:
        return extract_target_from_text(latest)
    return None


def extract_target_from_text(text: str) -> str | None:
    for block in re.findall(r"```json\s*(\{.*?\})\s*```", text, flags=re.DOTALL):
        try:
            payload = json.loads(block)
        except Exception:
            continue
        if not isinstance(payload, dict):
            continue
        target = first_string(
            payload.get("sender_id"),
            payload.get("id"),
            payload.get("e164"),
            payload.get("target"),
            payload.get("user_id"),
            payload.get("recipient"),
            payload.get("to"),
        )
        if target:
            return target
    return None


def compact_payload_for_repeat_channel(
    *,
    context_id: str,
    latest_user_prompt: str | None,
) -> dict[str, object]:
    payload: dict[str, object] = {"context_id": context_id}
    if latest_user_prompt is not None:
        payload["latest-user-prompt"] = latest_user_prompt
    return payload


def mark_context_id_seen(state_dir: Path, context_id: str) -> bool:
    state_dir.mkdir(parents=True, exist_ok=True)
    digest = hashlib.sha256(context_id.encode("utf-8")).hexdigest()
    marker_path = state_dir / digest
    try:
        fd = os.open(marker_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
    except FileExistsError:
        return True
    with os.fdopen(fd, "w", encoding="utf-8") as handle:
        handle.write(context_id + "\n")
    return False


def backend_response(status_code: int, headers: dict[str, str], body: bytes) -> Response:
    content_type = headers.get("content-type", "application/json")
    return Response(content=body, status_code=status_code, media_type=content_type)


def log_backend_error(path: str, exc: Exception) -> None:
    if isinstance(exc, urllib.error.HTTPError):
        try:
            body = exc.read().decode("utf-8", errors="replace").strip()
        except Exception:
            body = ""
        if body:
            LOGGER.warning(
                "backend_forward_failed path=%s status=%s reason=%s body=%s",
                path,
                exc.code,
                exc.reason,
                body,
            )
            return
        LOGGER.warning(
            "backend_forward_failed path=%s status=%s reason=%s",
            path,
            exc.code,
            exc.reason,
        )
        return
    LOGGER.warning("backend_forward_failed path=%s error=%s", path, exc)


def load_config_file(path: Path) -> dict[str, str]:
    config: dict[str, str] = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip().upper()
        value = value.strip()
        if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
            value = value[1:-1]
        config[key] = value
    return config


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="ai-agent-proxy", description="ai-agent-proxy")
    version = "unknown"
    try:
        version = Path(__file__).resolve().parents[2].joinpath("VERSION").read_text(encoding="utf-8").strip()
    except OSError:
        pass
    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {version}")
    parser.add_argument("-f", "--config", type=Path, default=Path("./.ai-agent-proxy.conf"))
    parser.add_argument("--host", default=None, help="HTTP bind host")
    parser.add_argument("--port", type=int, default=None)
    parser.add_argument("--key", default=None, help="Require Authorization: Bearer <key> on API requests")
    parser.add_argument("--worker", type=int, default=None, help="Number of uvicorn workers")
    parser.add_argument("--debug", type=int, choices=[0, 1], default=None, help="Enable debug logging with 1")
    parser.add_argument("--inbox", type=Path, default=None, help="Inbox path for dumped JSON requests")
    parser.add_argument("--url", default=None, help="Backend LLM base URL")
    parser.add_argument("--api_key", default=None, help="Backend LLM API key")
    parser.add_argument(
        "--openclaw-message-cli-skill-path",
        default=None,
        help="Path to openclaw-message-cli-skill/SKILL.md",
    )
    return parser


def parse_settings() -> ProxySettings:
    parser = build_parser()
    args = parser.parse_args()

    config: dict[str, str] = {}
    config_path = args.config.expanduser()
    if config_path.exists():
        config = load_config_file(config_path)

    host = args.host or config.get("HOST") or "0.0.0.0"
    port = args.port if args.port is not None else int(config.get("PORT", 7011))
    key = args.key if args.key is not None else config.get("KEY")
    workers = args.worker if args.worker is not None else int(config.get("WORKER", 1))
    debug = bool(args.debug) if args.debug is not None else config.get("DEBUG", "0") == "1"
    inbox_value = args.inbox or config.get("INBOX")
    if inbox_value is None:
        raise SystemExit("missing inbox path: set --inbox or INBOX in the config file")
    inbox = Path(inbox_value).expanduser().resolve()
    url = args.url if args.url is not None else config.get("URL")
    backend_api_key = args.api_key if args.api_key is not None else config.get("API_KEY")
    instruction = config.get("INSTRUCTION", DEFAULT_INSTRUCTION)
    openclaw_message_cli_skill_path = Path(
        (
            args.openclaw_message_cli_skill_path
            or config.get(
                "OPENCLAW_MESSAGE_CLI_SKILL_PATH",
                str(OPENCLAW_MESSAGE_CLI_SKILL_DEFAULT_PATH),
            )
        )
    ).expanduser().resolve()
    return ProxySettings(
        host=host,
        port=port,
        key=key,
        workers=workers,
        debug=debug,
        inbox=inbox,
        url=url.rstrip("/") if url else None,
        backend_api_key=backend_api_key,
        instruction=instruction,
        openclaw_message_cli_skill_path=openclaw_message_cli_skill_path,
    )


def validate_settings(settings: ProxySettings) -> None:
    if settings.port <= 0 or settings.port > 65535:
        raise SystemExit(f"invalid port: {settings.port}")
    if settings.workers <= 0:
        raise SystemExit(f"invalid worker count: {settings.workers}")


def create_app() -> FastAPI:
    settings = APP_FACTORY_SETTINGS
    if settings is None:
        settings = parse_settings()
    validate_settings(settings)
    proxy = Proxy(
        host=settings.host,
        port=settings.port,
        key=settings.key,
        workers=settings.workers,
        debug=settings.debug,
        inbox_path=settings.inbox,
        backend_url=settings.url,
        backend_api_key=settings.backend_api_key,
        instruction=settings.instruction,
        openclaw_message_cli_skill_path=settings.openclaw_message_cli_skill_path,
    )
    proxy.init()
    return proxy.app


def main() -> None:
    global APP_FACTORY_SETTINGS
    settings = parse_settings()
    validate_settings(settings)
    APP_FACTORY_SETTINGS = settings
    configure_logging()
    proxy = Proxy(
        host=settings.host,
        port=settings.port,
        key=settings.key,
        workers=settings.workers,
        debug=settings.debug,
        inbox_path=settings.inbox,
        backend_url=settings.url,
        backend_api_key=settings.backend_api_key,
        instruction=settings.instruction,
        openclaw_message_cli_skill_path=settings.openclaw_message_cli_skill_path,
    )
    print_env(proxy)
    proxy.run()

__all__ = [
    "Proxy",
    "ProxySettings",
    "build_parser",
    "create_app",
    "configure_logging",
    "load_config_file",
    "main",
    "parse_settings",
    "print_env",
    "validate_settings",
]
