#!/usr/bin/env python3
"""
XiaoZhi 视频通话代理服务器
用途：代理 HTTP 请求到 webrtc.aiboot.ltd（解决浏览器 CORS 限制）
运行：python xiaozhi_proxy.py
访问：http://localhost:8080/xiaozhi_videocall.html

代理规则：
  POST /proxy/join/{room}        → https://webrtc.aiboot.ltd/join/{room}
  POST /proxy/message/{r}/{cid}  → https://webrtc.aiboot.ltd/message/{r}/{cid}
  POST /proxy/ice_config         → https://webrtc.aiboot.ltd/ice_config
  POST /proxy/leave/{r}/{cid}    → https://webrtc.aiboot.ltd/leave/{r}/{cid}
  DELETE /proxy/...              → 同理
  WebSocket 直连（无 CORS 限制）

mDNS 候选地址重写：
  Chrome/Edge 默认会把本地 host ICE 候选用 xxx.local (mDNS) 隐藏真实 LAN IP，
  而 ESP32 libpeer 无 mDNS 解析器 → 无法建连。
  本代理在转发 POST /message/... (SDP answer / trickle candidate) 之前，
  把所有 .local 主机名替换为本机 LAN IP（LAN_IP），让 ESP32 能直接走
  局域网路径，绕过发夹式 NAT（hairpin NAT）问题。
"""
import json
import re
import socket
import ssl
import urllib.request
import urllib.error
from http.server import HTTPServer, SimpleHTTPRequestHandler

UPSTREAM = 'https://webrtc.aiboot.ltd'

SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE


def detect_lan_ip():
    """本机通过默认路由访问外网时使用的 IPv4 局域网地址，例如 192.168.2.103。"""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        try:
            return socket.gethostbyname(socket.gethostname())
        except Exception:
            return '127.0.0.1'


LAN_IP = detect_lan_ip()

# SDP a=candidate 行：… udp <优先级> <地址> <端口> typ host …
# 连接地址在「udp 与端口」等字段中；此处只替换 *.local 的 mDNS 主机名。
_MDNS_RE = re.compile(r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.local)', re.IGNORECASE)


def rewrite_mdns(text):
    """将 *.local 的 mDNS 主机名替换为 LAN_IP。返回 (新文本, 替换次数)。"""
    if '.local' not in text:
        return text, 0
    new_text, n = _MDNS_RE.subn(LAN_IP, text)
    return new_text, n


def rewrite_signaling_body(body_bytes):
    """
    改写发往外部的信令正文（answer / trickle candidate 等），把其中 .local
    全部换成本机 LAN IP。入参、返回值均为 bytes；JSON 解析失败时原样返回。
    """
    if not body_bytes:
        return body_bytes
    try:
        text = body_bytes.decode('utf-8', errors='replace')
    except Exception:
        return body_bytes
    if '.local' not in text:
        return body_bytes

    # AppRTC 的 /message 正文多为 JSON，例如
    #   {"type":"answer","sdp":"v=0\\r\\n…"} 或
    #   {"type":"candidate","candidate":"candidate:… .local …"}
    try:
        obj = json.loads(text)
    except Exception:
        new_text, n = rewrite_mdns(text)
        if n:
            print(f'[proxy] 已改写 {n} 处 .local → {LAN_IP}（原始正文）')
        return new_text.encode('utf-8')

    changed = 0
    if isinstance(obj, dict):
        if isinstance(obj.get('sdp'), str):
            obj['sdp'], n = rewrite_mdns(obj['sdp'])
            changed += n
        if isinstance(obj.get('candidate'), str):
            obj['candidate'], n = rewrite_mdns(obj['candidate'])
            changed += n
    if changed:
        print(f'[proxy] 已改写 {changed} 处 .local → {LAN_IP}（type={obj.get("type", "?")}）')
        return json.dumps(obj, ensure_ascii=False).encode('utf-8')
    return body_bytes


class ProxyHandler(SimpleHTTPRequestHandler):

    def log_message(self, fmt, *args):
        print(f'[proxy] {fmt % args}')

    def do_OPTIONS(self):
        self.send_response(200)
        self._add_cors()
        self.end_headers()

    def _proxy(self, method):
        if not self.path.startswith('/proxy/'):
            self.send_error(404, '非 /proxy 路径')
            return

        target = UPSTREAM + self.path[len('/proxy'):]
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length) if length else b''

        # 对外发的 SDP/候选里改写 .local：libpeer 无 mDNS，否则无法与浏览器建连
        if method == 'POST' and '/message/' in self.path:
            body = rewrite_signaling_body(body)

        req = urllib.request.Request(target, data=body or None, method=method)
        ct = self.headers.get('Content-Type')
        req.add_header('Content-Type', ct if ct else 'text/plain;charset=UTF-8')
        if self.headers.get('Referer'):
            req.add_header('Referer', self.headers['Referer'])

        try:
            with urllib.request.urlopen(req, context=SSL_CTX) as resp:
                data = resp.read()
                ctype = resp.headers.get('Content-Type', 'application/json')
            self.send_response(200)
            self.send_header('Content-Type', ctype)
            self._add_cors()
            self.end_headers()
            self.wfile.write(data)
            print(f'[proxy] {method} {target} → 200（{len(data)} 字节）')
        except urllib.error.HTTPError as e:
            ebody = e.read()
            self.send_response(e.code)
            self._add_cors()
            self.end_headers()
            self.wfile.write(ebody)
            print(f'[proxy] {method} {target} → 上游返回 HTTP {e.code}')
        except Exception as e:
            self.send_response(502)
            self._add_cors()
            self.end_headers()
            self.wfile.write(str(e).encode())
            print(f'[proxy] {method} {target} → 错误: {e}')

    def do_POST(self):
        self._proxy('POST')

    def do_DELETE(self):
        self._proxy('DELETE')

    def _add_cors(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type, Referer')


if __name__ == '__main__':
    port = 18080
    server = HTTPServer(('', port), ProxyHandler)
    print(f'代理服务器已启动: http://localhost:{port}')
    print(f'打开页面: http://localhost:{port}/xiaozhi_videocall.html')
    print(f'本机 LAN IP (用于 .local → IP 改写): {LAN_IP}')
    print('按 Ctrl+C 退出\n')
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print('\n已停止')
