# -*- coding: utf-8 -*-
import json
import logging
import os
import re
import threading
import time
import queue
import ssl
import urllib.parse
import urllib.request
from typing import Dict, Optional, Any

try:
    from flask import Flask, request, jsonify, Response, send_from_directory
except ImportError:
    raise ImportError(
        "Flask is required for web server. "
        "Install it with: pip install acp-py[web]"
    ) from None

from agentcp.agentcp import AgentCP, AgentID
from agentcp.base.log import log_info, log_error, log_warning
from agentcp.utils import tls_config as tls
from agentcp.group_client import GroupClient, GroupOperations

app = Flask(__name__)
app.logger.setLevel(logging.WARNING)
logging.getLogger('werkzeug').setLevel(logging.WARNING)

# ============================================================
# Global State
# ============================================================
MAX_AIDS = 10
global_data_dir = ''
global_seed_password = ''
acp: Optional[AgentCP] = None

class AidInstance:
    def __init__(self, aid: str, agent_id: AgentID):
        self.aid = aid
        self.agent_id = agent_id
        self.online = False
        self.ws_connected = False
        self.ws_status = 'disconnected'
        self.active_group_id = None
        self.group_initialized = False
        self.group_client: Optional[GroupClient] = None
        self.group_ops: Optional[GroupOperations] = None
        self.group_target_aid = ''
        self.group_session_id = ''

aid_instances: Dict[str, AidInstance] = {}

# P2P message store: {aid: {session_id: {messages:[], peer_aid:str, type:str, closed:bool}}}
message_stores: Dict[str, dict] = {}

# Agent info cache
agent_info_cache: Dict[str, dict] = {}
AGENT_INFO_CACHE_TTL = 24 * 60 * 60

# Group message store: {group_id: [messages]}
group_message_stores: Dict[str, list] = {}
group_message_lock = threading.Lock()
# Group list store: {aid: [{group_id, name}]}
group_list_stores: Dict[str, list] = {}

# AID md options (nickname, description)
aid_md_options: Dict[str, dict] = {}

# Group message polling threads: {aid: threading.Thread}
group_poll_threads: Dict[str, threading.Thread] = {}
group_poll_running: Dict[str, bool] = {}
# Per-AID poll cursor: {aid: {group_id: last_pushed_msg_id}}
group_poll_cursors: Dict[str, Dict[str, int]] = {}


def get_acp() -> AgentCP:
    global acp
    if acp is None:
        acp = AgentCP(global_data_dir, seed_password=global_seed_password, run_proxy=False)
    return acp


# ============================================================
# Message Store helpers (with file persistence)
# ============================================================
def _sessions_dir(aid: str) -> str:
    d = global_data_dir or os.getcwd()
    return os.path.join(d, 'agentcp', 'AIDs', aid, 'sessions')


def _flush_index(aid: str):
    """Save session index to _index.json"""
    try:
        sdir = _sessions_dir(aid)
        os.makedirs(sdir, exist_ok=True)
        store = message_stores.get(aid, {})
        records = []
        for sid, s in store.items():
            records.append({
                'session_id': sid, 'peer_aid': s['peer_aid'], 'type': s['type'],
                'identifying_code': s.get('identifying_code', ''),
                'last_message_at': s.get('last_message_at', 0),
                'message_count': len(s.get('messages', [])),
                'closed': s.get('closed', False),
            })
        with open(os.path.join(sdir, '_index.json'), 'w', encoding='utf-8') as f:
            json.dump(records, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


def _append_message(aid: str, session_id: str, msg: dict):
    """Append one message to {session_id}.jsonl"""
    try:
        sdir = _sessions_dir(aid)
        os.makedirs(sdir, exist_ok=True)
        with open(os.path.join(sdir, f'{session_id}.jsonl'), 'a', encoding='utf-8') as f:
            f.write(json.dumps(msg, ensure_ascii=False) + '\n')
    except Exception:
        pass


def _rename_session_file(aid: str, old_sid: str, new_sid: str):
    """Rename session jsonl file from old_sid to new_sid."""
    try:
        sdir = _sessions_dir(aid)
        old_path = os.path.join(sdir, f'{old_sid}.jsonl')
        new_path = os.path.join(sdir, f'{new_sid}.jsonl')
        if os.path.exists(old_path):
            os.replace(old_path, new_path)
    except Exception:
        pass


def load_sessions_for_aid(aid: str):
    """Load sessions + messages from disk into memory"""
    sdir = _sessions_dir(aid)
    index_path = os.path.join(sdir, '_index.json')
    if not os.path.exists(index_path):
        return
    try:
        with open(index_path, 'r', encoding='utf-8') as f:
            records = json.load(f)
        if not isinstance(records, list):
            return
        store = get_store(aid)
        for r in records:
            sid = r.get('session_id', '')
            if not sid or sid in store:
                continue
            # Load messages from jsonl
            msgs = []
            msg_path = os.path.join(sdir, f'{sid}.jsonl')
            if os.path.exists(msg_path):
                with open(msg_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        line = line.strip()
                        if line:
                            try:
                                msgs.append(json.loads(line))
                            except Exception:
                                pass
            store[sid] = {
                'session_id': sid,
                'peer_aid': r.get('peer_aid', ''),
                'type': r.get('type', 'incoming'),
                'identifying_code': r.get('identifying_code', ''),
                'messages': msgs,
                'closed': r.get('closed', False),
                'last_message_at': r.get('last_message_at', 0),
                'last_message': msgs[-1].get('content', '')[:50] if msgs else '',
            }
    except Exception:
        pass


def get_store(aid: str) -> dict:
    if aid not in message_stores:
        message_stores[aid] = {}
    return message_stores[aid]


def get_or_create_session(aid: str, session_id: str, peer_aid: str, stype: str, identifying_code: str = ''):
    store = get_store(aid)
    if session_id not in store:
        store[session_id] = {
            'session_id': session_id,
            'peer_aid': peer_aid,
            'type': stype,
            'identifying_code': identifying_code,
            'messages': [],
            'closed': False,
            'created_at': int(time.time() * 1000),
            'last_message_at': int(time.time() * 1000),
            'last_message': '',
        }
        _flush_index(aid)
    return store[session_id]


def add_message_to_session(aid: str, session_id: str, msg: dict):
    store = get_store(aid)
    if session_id in store:
        store[session_id]['messages'].append(msg)
        store[session_id]['last_message_at'] = msg.get('timestamp', int(time.time() * 1000))
        store[session_id]['last_message'] = msg.get('content', '')[:50]
        _append_message(aid, session_id, msg)
        _flush_index(aid)


def get_session_list(aid: str):
    store = get_store(aid)
    result = []
    for sid, s in store.items():
        result.append({
            'sessionId': sid,
            'peerAid': s['peer_aid'],
            'type': s['type'],
            'messageCount': len(s['messages']),
            'lastMessageAt': s.get('last_message_at', 0),
            'lastMessage': s.get('last_message', ''),
            'closed': s.get('closed', False),
        })
    result.sort(key=lambda x: x['lastMessageAt'], reverse=True)
    return result


# ============================================================
# Standalone WebSocket server (sync, thread-per-connection)
# ============================================================
from websockets.sync.server import serve as ws_serve
from websockets.exceptions import ConnectionClosed

ws_queues: list = []  # list of queue.Queue, one per browser connection
ws_lock = threading.Lock()


def push_to_browser(data: dict):
    msg = json.dumps(data)
    with ws_lock:
        for q in ws_queues:
            try:
                q.put_nowait(msg)
            except Exception:
                pass


def push_to_aid(aid: str, data: dict):
    # TODO: filter by aid when multi-browser support is added
    push_to_browser(data)


# ============================================================
# Agent info fetching
# ============================================================
def fetch_agent_md(aid: str) -> str:
    try:
        ctx = tls.ssl_context()
        req = urllib.request.Request(f'https://{aid}/agent.md', method='GET')
        with urllib.request.urlopen(req, timeout=5, context=ctx) as resp:
            return resp.read().decode('utf-8')
    except Exception:
        return ''


def parse_agent_md_frontmatter(content: str) -> dict:
    result = {'type': '', 'name': '', 'description': '', 'tags': []}
    m = re.match(r'^---\s*\n([\s\S]*?)\n---', content)
    if not m:
        return result
    yaml = m.group(1)
    for key in ['type', 'name', 'description']:
        km = re.search(rf'^{key}:\s*"?([^"\n]*)"?\s*$', yaml, re.MULTILINE)
        if km:
            result[key] = km.group(1).strip()
    tags_block = re.search(r'^tags:\s*\n((?:\s+-\s+.*\n?)*)', yaml, re.MULTILINE)
    if tags_block:
        tag_lines = re.findall(r'^\s+-\s+(.+)$', tags_block.group(1), re.MULTILINE)
        result['tags'] = [t.strip().strip('"') for t in tag_lines]
    return result


def get_agent_info(aid: str) -> dict:
    cached = agent_info_cache.get(aid)
    if cached and time.time() - cached.get('cached_at', 0) < AGENT_INFO_CACHE_TTL:
        return {k: cached[k] for k in ['type', 'name', 'description', 'tags']}
    try:
        md = fetch_agent_md(aid)
        info = parse_agent_md_frontmatter(md)
        agent_info_cache[aid] = {**info, 'cached_at': time.time()}
        return info
    except Exception:
        if cached:
            return {k: cached[k] for k in ['type', 'name', 'description', 'tags']}
        return {'type': '', 'name': '', 'description': '', 'tags': []}


# ============================================================
# AID md options persistence
# ============================================================
def get_aid_md_options_path() -> str:
    d = global_data_dir or os.getcwd()
    return os.path.join(d, 'agentcp', 'AIDs', '.aid-md-options.json')


def load_aid_md_options():
    global aid_md_options
    try:
        p = get_aid_md_options_path()
        if os.path.exists(p):
            with open(p, 'r', encoding='utf-8') as f:
                aid_md_options = json.load(f)
    except Exception:
        pass


def save_aid_md_options_for(aid: str, opts: dict):
    global aid_md_options
    aid_md_options[aid] = opts
    try:
        p = get_aid_md_options_path()
        os.makedirs(os.path.dirname(p), exist_ok=True)
        with open(p, 'w', encoding='utf-8') as f:
            json.dump(aid_md_options, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


# ============================================================
# AID validation
# ============================================================
def validate_aid(aid: str) -> dict:
    try:
        cp = get_acp()
        cert_pem = cp.read_certificate_pem(aid)
        keys_exist = cert_pem is not None and len(cert_pem) > 0
        # Simple validity check
        cert_valid = keys_exist
        if keys_exist:
            try:
                from cryptography import x509
                from cryptography.hazmat.backends import default_backend
                import datetime
                cert = x509.load_pem_x509_certificate(cert_pem.encode(), default_backend())
                cert_valid = cert.not_valid_after_utc > datetime.datetime.now(datetime.timezone.utc)
            except Exception:
                cert_valid = keys_exist
        return {'keysExist': keys_exist, 'certValid': cert_valid}
    except Exception:
        return {'keysExist': False, 'certValid': False}


def get_aid_status_list() -> list:
    cp = get_acp()
    aid_list = cp.get_aid_list()
    result = []
    for aid in aid_list:
        v = validate_aid(aid)
        inst = aid_instances.get(aid)
        result.append({
            'aid': aid,
            'keysExist': v['keysExist'],
            'certValid': v['certValid'],
            'online': inst.online if inst else False,
        })
    return result


# ============================================================
# Ensure AID online
# ============================================================
online_locks: Dict[str, threading.Lock] = {}


def ensure_online(target_aid: str) -> AidInstance:
    if target_aid in aid_instances:
        inst = aid_instances[target_aid]
        if inst.online:
            return inst

    lock = online_locks.setdefault(target_aid, threading.Lock())
    with lock:
        if target_aid in aid_instances and aid_instances[target_aid].online:
            return aid_instances[target_aid]
        return do_ensure_online(target_aid)


def do_ensure_online(aid: str) -> AidInstance:
    load_sessions_for_aid(aid)
    cp = get_acp()
    agent_id = cp.load_aid(aid)
    if agent_id is None:
        raise Exception(f'AID {aid} 加载失败')

    # Set up message handler for P2P
    def on_message(data):
        handle_incoming_message(aid, data)

    agent_id.add_message_handler(on_message)

    # 在 online() 之前设置 agent.md options，这样 online() 内部的后台线程会使用正确的选项
    options = aid_md_options.get(aid, {})
    if options:
        agent_id.set_agent_md_options(options)

    agent_id.online()
    # online() 内部已通过后台线程自动处理 agent.md 上传，无需额外调用

    # Set up heartbeat invite handler
    if agent_id.heartbeat_client:
        original_on_invite = getattr(agent_id.heartbeat_client, 'on_recv_invite', None)
        def on_invite(invite_req):
            # invite_req is InviteMessageReq object, not dict
            session_id = getattr(invite_req, 'SessionId', '') or ''
            inviter = getattr(invite_req, 'InviterAgentId', '') or ''
            invite_code = getattr(invite_req, 'InviteCode', '') or ''
            if session_id and inviter:
                store = get_store(aid)
                # 查找已有的该 peer 的活跃会话
                existing_sid = next(
                    (sid for sid, s in store.items()
                     if s.get('peer_aid') == inviter and not s.get('closed')),
                    None
                )
                if existing_sid and existing_sid != session_id:
                    # 把已有会话的消息迁移到对方邀请的 session 上，双方共用同一个 session
                    old = store[existing_sid]
                    store[session_id] = {
                        'session_id': session_id, 'peer_aid': inviter,
                        'type': old.get('type', 'incoming'), 'identifying_code': invite_code,
                        'messages': old.get('messages', []), 'closed': False,
                        'created_at': int(time.time() * 1000),
                        'last_message_at': old.get('last_message_at', 0),
                        'last_message': old.get('last_message', ''),
                    }
                    del store[existing_sid]
                    _rename_session_file(aid, existing_sid, session_id)
                    _flush_index(aid)
                    log_info(f'[Server] AID {aid} 会话迁移: {existing_sid[:8]}... -> {session_id[:8]}... (peer={inviter})')
                elif not existing_sid:
                    get_or_create_session(aid, session_id, inviter, 'incoming', invite_code)
                # else: session_id == existing_sid, 无需处理
                push_to_aid(aid, {'type': 'sessions_updated'})
            if original_on_invite:
                original_on_invite(invite_req)
        agent_id.heartbeat_client.set_on_recv_invite(on_invite)

    inst = AidInstance(aid, agent_id)
    inst.online = True
    inst.ws_connected = True
    inst.ws_status = 'connected'
    aid_instances[aid] = inst

    # Rebuild stale sessions: online() destroys all server-side sessions,
    # so we need to create new ones and migrate data in-place.
    store = get_store(aid)
    stale_sids = [sid for sid, s in store.items()
                  if not s.get('closed') and s.get('peer_aid')]
    if stale_sids:
        log_info(f'[Server] AID {aid} 重建 {len(stale_sids)} 个旧会话...')
        for old_sid in stale_sids:
            s = store[old_sid]
            peer = s['peer_aid']
            try:
                new_sid = agent_id.create_session(f'p2p_{int(time.time())}', 'ACP P2P')
                if not new_sid:
                    continue
                agent_id.invite_member(new_sid, peer)
                # Migrate in-place: copy data to new key, remove old
                store[new_sid] = {
                    'session_id': new_sid, 'peer_aid': peer,
                    'type': s.get('type', 'outgoing'), 'identifying_code': '',
                    'messages': s.get('messages', []), 'closed': False,
                    'created_at': int(time.time() * 1000),
                    'last_message_at': s.get('last_message_at', 0),
                    'last_message': s.get('last_message', ''),
                }
                del store[old_sid]
                _rename_session_file(aid, old_sid, new_sid)
            except Exception as e:
                log_warning(f'[Server] 重建会话 {old_sid} 失败: {e}')
        _flush_index(aid)

    log_info(f'[Server] AID {aid} 已上线')
    push_to_browser({'type': 'aid_status', 'aidStatus': get_aid_status_list()})
    return inst


def ensure_group_client(inst: AidInstance):
    """Initialize group client for an AID instance if not already done."""
    if inst.group_initialized and inst.group_client:
        return
    aid = inst.aid
    parts = aid.split('.')
    target_aid = 'group.' + ('.'.join(parts[1:]) if len(parts) >= 3 else aid.split('.', 1)[-1])
    inst.group_target_aid = target_aid

    # Create session with group server
    session_id = inst.agent_id.create_session(f'group_{int(time.time())}', 'ACP Group')
    if not session_id:
        raise Exception('Failed to create group session')
    inst.agent_id.invite_member(session_id, target_aid)
    inst.group_session_id = session_id

    # Create group client and operations
    client = GroupClient(inst.agent_id, session_id, target_aid)
    inst.group_client = client
    inst.group_ops = GroupOperations(client, target_aid)
    inst.group_initialized = True

    # Set up message push callback for real-time group messages
    def on_group_message_push(group_id, msg):
        if not group_id:
            return
        with group_message_lock:
            store = group_message_stores.setdefault(group_id, [])
            msg_id = msg.get('msg_id', 0)
            if msg_id and any(m.get('msg_id') == msg_id for m in store):
                return  # deduplicate
            store.append(msg)
            store.sort(key=lambda x: x.get('msg_id', 0))
        push_to_aid(aid, {'type': 'group_message', 'group_id': group_id, 'message': msg})

    client.set_on_message_push(on_group_message_push)

    # Try register online
    try:
        inst.group_ops.register_online()
    except Exception as e:
        log_warning(f'[Group] register_online failed: {e}')

    log_info(f'[Group] Client initialized for {aid}, target={target_aid}')


# ============================================================
# Heartbeat health check (auto-reconnect)
# ============================================================
HEALTH_CHECK_INTERVAL = 30  # seconds
_health_check_running = False


def _health_check_loop():
    """Periodically verify server-side online status and notify browser."""
    while _health_check_running:
        try:
            for aid, inst in list(aid_instances.items()):
                if not inst.online:
                    continue
                hb = inst.agent_id.heartbeat_client
                if not hb or not hb.is_running:
                    continue
                try:
                    status_list = hb.get_online_status([aid])
                    server_online = any(
                        s.get('aid') == aid and s.get('online')
                        for s in (status_list or [])
                    )
                    if not server_online:
                        log_warning(f'[HealthCheck] {aid} 服务端不在线')
                        inst.ws_status = 'disconnected'
                        push_to_browser({'type': 'ws_status', 'aid': aid, 'status': 'disconnected'})
                    elif inst.ws_status == 'disconnected':
                        inst.ws_status = 'connected'
                        push_to_browser({'type': 'ws_status', 'aid': aid, 'status': 'connected'})
                except Exception as e:
                    log_warning(f'[HealthCheck] {aid} 检查失败: {e}')
        except Exception as e:
            log_error(f'[HealthCheck] Error: {e}')
        time.sleep(HEALTH_CHECK_INTERVAL)


def start_health_check():
    global _health_check_running
    if _health_check_running:
        return
    _health_check_running = True
    t = threading.Thread(target=_health_check_loop, daemon=True)
    t.start()


# ============================================================
# Group message background polling
# ============================================================
GROUP_POLL_INTERVAL = 3  # seconds


def start_group_poll(aid: str):
    if aid in group_poll_threads and group_poll_threads[aid].is_alive():
        return
    group_poll_running[aid] = True
    t = threading.Thread(target=_group_poll_loop, args=(aid,), daemon=True)
    group_poll_threads[aid] = t
    t.start()


def stop_group_poll(aid: str):
    group_poll_running[aid] = False


def _fetch_group_list(inst: AidInstance) -> list:
    """Fetch group list from server if not cached locally."""
    aid = inst.aid
    groups = group_list_stores.get(aid, [])
    if groups or not inst.group_ops:
        return groups
    try:
        result = inst.group_ops.list_my_groups()
        for g in result.get('groups', []):
            gid = g.get('group_id', '')
            if gid:
                name = gid
                try:
                    info = inst.group_ops.get_group_info(gid)
                    name = info.get('name', gid) or gid
                except Exception:
                    pass
                groups.append({'group_id': gid, 'name': name})
        if groups:
            group_list_stores[aid] = groups
    except Exception as e:
        log_warning(f'[Group] {aid}: list_my_groups error: {e}')
    return groups


def _group_poll_loop(aid: str):
    log_info(f'[GroupPoll] Started for {aid}')
    while group_poll_running.get(aid, False):
        try:
            inst = aid_instances.get(aid)
            if not inst or not inst.group_ops:
                time.sleep(GROUP_POLL_INTERVAL)
                continue
            groups = _fetch_group_list(inst)
            if not groups:
                time.sleep(GROUP_POLL_INTERVAL)
                continue
            for g in groups:
                gid = g.get('group_id', '')
                if not gid:
                    continue
                try:
                    cursors = group_poll_cursors.setdefault(aid, {})
                    cursor = cursors.get(gid, -1)
                    if cursor == -1:
                        # 首次轮询：探测 latest_msg_id，直接跳到最新位置
                        # 只监听之后的新消息，不拉历史
                        try:
                            probe = inst.group_ops._client.send_request(
                                'pull_messages', gid, {'after_msg_id': 0, 'limit': 1}
                            )
                            latest = probe.get('data', {}).get('latest_msg_id', 0)
                            cursors[gid] = latest
                        except Exception:
                            cursors[gid] = 0
                        continue
                    resp = inst.group_ops._client.send_request(
                        'pull_messages', gid, {'after_msg_id': cursor, 'limit': 50}
                    )
                    d = resp.get('data', {})
                    new_msgs = d.get('messages', [])
                    latest = d.get('latest_msg_id', 0)
                    if latest > cursor:
                        cursors[gid] = latest
                    added = 0
                    with group_message_lock:
                        store = group_message_stores.setdefault(gid, [])
                        existing_ids = {m.get('msg_id') for m in store}
                        for m in new_msgs:
                            mid = m.get('msg_id')
                            if not mid:
                                continue
                            if mid not in existing_ids:
                                store.append(m)
                                existing_ids.add(mid)
                            if mid > cursor:
                                push_to_aid(aid, {'type': 'group_message', 'group_id': gid, 'message': m})
                                added += 1
                        if added:
                            store.sort(key=lambda x: x.get('msg_id', 0))
                except Exception as e:
                    log_warning(f'[GroupPoll] poll error {gid}: {e}')
        except Exception as e:
            log_error(f'[GroupPoll] Error for {aid}: {e}')
        time.sleep(GROUP_POLL_INTERVAL)
    log_info(f'[GroupPoll] Stopped for {aid}')


def handle_incoming_message(aid: str, data: dict):
    """Handle incoming message - routes to group client or P2P handler"""
    try:
        sender = data.get('sender', '')
        session_id = data.get('session_id', '')
        msg_content = data.get('message', '')

        # Try to decode message content
        decoded = msg_content
        if isinstance(decoded, str):
            try:
                if '%' in decoded:
                    decoded = urllib.parse.unquote(decoded)
            except Exception:
                pass

        # Try routing to group client
        inst = aid_instances.get(aid)
        if inst and inst.group_client and inst.group_session_id == session_id:
            if inst.group_client.handle_incoming(decoded):
                return

        # P2P message handling
        content = ''
        if isinstance(decoded, str):
            try:
                parsed = json.loads(decoded)
                if isinstance(parsed, list) and len(parsed) > 0:
                    content = ''.join(item.get('content', '') for item in parsed)
                elif isinstance(parsed, dict) and 'content' in parsed:
                    content = parsed['content']
                else:
                    content = decoded
            except Exception:
                content = decoded
        else:
            content = json.dumps(msg_content) if msg_content else json.dumps(data)

        if session_id and sender:
            store = get_store(aid)
            target_sid = session_id
            if session_id not in store:
                # Route to existing active session with this sender if available
                existing_sid = next(
                    (sid for sid, s in store.items()
                     if s.get('peer_aid') == sender and not s.get('closed')),
                    None
                )
                if existing_sid:
                    target_sid = existing_sid
                else:
                    get_or_create_session(aid, session_id, sender, 'incoming')
            msg = {'type': 'received', 'content': content, 'from': sender, 'timestamp': int(time.time() * 1000)}
            add_message_to_session(aid, target_sid, msg)
            push_to_aid(aid, {'type': 'p2p_message', 'sessionId': target_sid, 'message': msg})
            push_to_aid(aid, {'type': 'sessions_updated'})
    except Exception as e:
        log_error(f'[Server] handle_incoming_message error: {e}')


# ============================================================
# Flask Routes - Pages & SSE
# ============================================================
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates')
ASSETS_DIR = os.path.join(os.path.dirname(__file__), 'assets')


@app.route('/')
def index_page():
    return send_from_directory(TEMPLATE_DIR, 'index.html')


@app.route('/chat')
def chat_page():
    return send_from_directory(TEMPLATE_DIR, 'chat.html')


@app.route('/assets/<path:filename>')
def serve_asset(filename):
    allowed = ['openclaw.png', 'human.png', 'agent.png']
    if filename not in allowed:
        return 'Not Found', 404
    return send_from_directory(ASSETS_DIR, filename, max_age=86400)


def _ws_handler(websocket):
    q = queue.Queue()
    with ws_lock:
        ws_queues.append(q)
    try:
        websocket.send(json.dumps({'type': 'ws_connected'}))
        while True:
            while not q.empty():
                try:
                    msg = q.get_nowait()
                    websocket.send(msg)
                except Exception:
                    break
            try:
                websocket.recv(timeout=0.5)
            except ConnectionClosed:
                break
            except Exception:
                pass
    except ConnectionClosed:
        pass
    except Exception:
        pass
    finally:
        with ws_lock:
            if q in ws_queues:
                ws_queues.remove(q)


def _start_ws_server(port):
    with ws_serve(_ws_handler, '0.0.0.0', port) as server:
        server.serve_forever()


# ============================================================
# Flask Routes - AID Management
# ============================================================
@app.route('/api/aid')
def api_aid():
    try:
        cp = get_acp()
        aid_list = cp.get_aid_list()
        aid_status = get_aid_status_list()
        return jsonify({'aidList': aid_list, 'aidStatus': aid_status, 'apiUrl': ''})
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/aid/create', methods=['POST'])
def api_aid_create():
    try:
        body = request.get_json(force=True)
        prefix = (body.get('prefix') or '').strip()
        if not prefix:
            return jsonify({'success': False, 'error': '请输入 AID 名称'})
        cp = get_acp()
        existing = cp.get_aid_list()
        if len(existing) >= MAX_AIDS:
            return jsonify({'success': False, 'error': f'最多只能注册 {MAX_AIDS} 个 AID'})
        ap = body.get('ap', 'agentcp.io')
        try:
            agent_id = cp.create_aid(ap=ap, agent_name=prefix)
            aid = agent_id.id
            nickname = (body.get('nickname') or '').strip()
            description = (body.get('description') or '').strip()
            if nickname or description:
                opts = {}
                if nickname: opts['name'] = nickname
                if description: opts['description'] = description
                save_aid_md_options_for(aid, opts)
            # Auto-online to trigger agent.md generation and upload
            try:
                ensure_online(aid)
            except Exception as e:
                log_warning(f'[Server] Auto-online after create failed for {aid}: {e}')
            return jsonify({'success': True, 'aid': aid})
        except Exception as e:
            return jsonify({'success': False, 'error': f'AID "{prefix}" 注册失败，该名称可能已被占用'})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/aid/select', methods=['POST'])
def api_aid_select():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '请指定 AID'})
        try:
            ensure_online(aid)
            return jsonify({'success': True, 'aid': aid})
        except Exception as e:
            return jsonify({'success': False, 'error': str(e)})
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/aid/validate', methods=['POST'])
def api_aid_validate():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '请指定 AID'})
        status = validate_aid(aid)
        inst = aid_instances.get(aid)
        return jsonify({'success': True, 'aid': aid, **status, 'online': inst.online if inst else False})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/aid/offline', methods=['POST'])
def api_aid_offline():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '请指定 AID'})
        inst = aid_instances.get(aid)
        if not inst:
            return jsonify({'success': False, 'error': '该 AID 未上线'})
        try:
            inst.agent_id.offline()
        except Exception:
            pass
        stop_group_poll(aid)
        del aid_instances[aid]
        log_info(f'[Server] AID {aid} 已下线')
        push_to_browser({'type': 'aid_status', 'aidStatus': get_aid_status_list()})
        return jsonify({'success': True, 'aid': aid})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ============================================================
# Flask Routes - Agent Info
# ============================================================
@app.route('/api/agent-info')
def api_agent_info():
    aid = request.args.get('aid', '')
    if not aid:
        return jsonify({'type': '', 'name': '', 'description': '', 'tags': []})
    return jsonify(get_agent_info(aid))


@app.route('/api/agent-md-raw')
def api_agent_md_raw():
    aid = request.args.get('aid', '')
    if not aid:
        return jsonify({'success': False, 'error': '缺少 aid'})
    try:
        md = fetch_agent_md(aid)
        return jsonify({'success': True, 'content': md})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ============================================================
# Flask Routes - WebSocket / P2P
# ============================================================
@app.route('/api/ws/start', methods=['POST'])
def api_ws_start():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '请先选择 AID'})
        ensure_online(aid)
        return jsonify({'success': True, 'aid': aid})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/ws/connect', methods=['POST'])
def api_ws_connect():
    try:
        body = request.get_json(force=True)
        target_aid = body.get('targetAid', '')
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        if not target_aid:
            return jsonify({'success': False, 'error': '缺少目标 AID'})
        inst = ensure_online(aid)
        agent_id = inst.agent_id
        session_id = agent_id.create_session(f'p2p_{int(time.time())}', 'ACP P2P')
        if not session_id:
            return jsonify({'success': False, 'error': '创建会话失败'})
        agent_id.invite_member(session_id, target_aid)
        get_or_create_session(aid, session_id, target_aid, 'outgoing')
        push_to_aid(aid, {'type': 'sessions_updated'})
        return jsonify({'success': True, 'sessionId': session_id, 'identifyingCode': ''})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/ws/send', methods=['POST'])
def api_ws_send():
    try:
        body = request.get_json(force=True)
        message = body.get('message', '')
        session_id = body.get('sessionId', '')
        aid = body.get('aid', '')
        if not message:
            return jsonify({'success': False, 'error': '消息不能为空'})
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        if not session_id:
            return jsonify({'success': False, 'error': '缺少 sessionId'})
        inst = ensure_online(aid)
        store = get_store(aid)
        session = store.get(session_id)
        if not session:
            return jsonify({'success': False, 'error': '会话不存在'})
        if session.get('closed'):
            return jsonify({'success': False, 'error': '该会话已关闭，请新建会话'})
        peer_aid = session['peer_aid']
        inst.agent_id.send_message_content(session_id, [peer_aid], message)
        msg = {'type': 'sent', 'content': message, 'to': peer_aid, 'timestamp': int(time.time() * 1000)}
        add_message_to_session(aid, session_id, msg)
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/ws/status')
def api_ws_status():
    aid = request.args.get('aid', '')
    inst = aid_instances.get(aid) if aid else None
    if not inst:
        return jsonify({'connected': False, 'status': 'disconnected'})
    return jsonify({'connected': inst.ws_connected, 'status': inst.ws_status})


# ============================================================
# Flask Routes - Sessions
# ============================================================
@app.route('/api/sessions')
def api_sessions():
    aid = request.args.get('aid', '')
    if not aid:
        return jsonify({'sessions': [], 'activeSessionId': None})
    return jsonify({'sessions': get_session_list(aid), 'activeSessionId': None})


@app.route('/api/messages')
def api_messages():
    aid = request.args.get('aid', '')
    session_id = request.args.get('sessionId', '')
    if not aid or not session_id:
        return jsonify({'messages': [], 'activeSessionId': None, 'closed': False})
    store = get_store(aid)
    session = store.get(session_id)
    return jsonify({
        'messages': session['messages'] if session else [],
        'activeSessionId': session_id,
        'closed': session.get('closed', False) if session else False
    })


@app.route('/api/sessions/active', methods=['POST'])
def api_sessions_active():
    try:
        body = request.get_json(force=True)
        session_id = body.get('sessionId', '')
        aid = body.get('aid', '')
        if not aid or not session_id:
            return jsonify({'success': False, 'error': '会话不存在'})
        store = get_store(aid)
        if session_id not in store:
            return jsonify({'success': False, 'error': '会话不存在'})
        push_to_aid(aid, {'type': 'set_active_session', 'sessionId': session_id})
        return jsonify({'success': True, 'activeSessionId': session_id})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/sessions/delete', methods=['POST'])
def api_sessions_delete():
    try:
        body = request.get_json(force=True)
        session_id = body.get('sessionId', '')
        aid = body.get('aid', '')
        if not session_id:
            return jsonify({'success': False, 'error': '会话ID不能为空'})
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        store = get_store(aid)
        if session_id in store:
            del store[session_id]
            # Clean up files
            try:
                msg_path = os.path.join(_sessions_dir(aid), f'{session_id}.jsonl')
                if os.path.exists(msg_path):
                    os.remove(msg_path)
            except Exception:
                pass
            _flush_index(aid)
            return jsonify({'success': True})
        return jsonify({'success': False, 'error': '会话不存在或删除失败'})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/peers/delete', methods=['POST'])
def api_peers_delete():
    try:
        body = request.get_json(force=True)
        peer_aid = body.get('peerAid', '')
        aid = body.get('aid', '')
        if not peer_aid:
            return jsonify({'success': False, 'error': 'AID不能为空'})
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        store = get_store(aid)
        count = 0
        to_del = [sid for sid, s in store.items() if s['peer_aid'] == peer_aid]
        for sid in to_del:
            del store[sid]
            try:
                msg_path = os.path.join(_sessions_dir(aid), f'{sid}.jsonl')
                if os.path.exists(msg_path):
                    os.remove(msg_path)
            except Exception:
                pass
            count += 1
        if to_del:
            _flush_index(aid)
        return jsonify({'success': True, 'count': count})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ============================================================
# Flask Routes - Group
# ============================================================
@app.route('/api/group/init', methods=['POST'])
def api_group_init():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        start_group_poll(aid)
        groups = _fetch_group_list(inst)
        active = inst.active_group_id if inst else None
        return jsonify({'success': True, 'targetAid': inst.group_target_aid, 'groups': groups, 'activeGroupId': active})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/create', methods=['POST'])
def api_group_create():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        name = body.get('name', '')
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        if not name:
            return jsonify({'success': False, 'error': '群组名称不能为空'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.create_group(
            name,
            visibility=body.get('visibility', ''),
            description=body.get('description', ''),
        )
        group_id = result.get('group_id', '')
        # Set duty mode if provided
        duty_mode = body.get('duty_mode', '')
        if duty_mode and group_id:
            try:
                inst.group_ops.update_duty_config(group_id, {'mode': duty_mode})
            except Exception:
                pass
        # Store locally
        if group_id:
            groups = group_list_stores.setdefault(aid, [])
            groups.append({'group_id': group_id, 'name': name})
        return jsonify({'success': True, **result})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/list')
def api_group_list():
    try:
        aid = request.args.get('aid', '')
        if not aid:
            return jsonify({'success': True, 'groups': [], 'activeGroupId': None})
        inst = aid_instances.get(aid)
        groups = _fetch_group_list(inst) if inst else []
        active = inst.active_group_id if inst else None
        return jsonify({'success': True, 'groups': groups, 'activeGroupId': active})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e), 'groups': []})


@app.route('/api/group/select', methods=['POST'])
def api_group_select():
    try:
        body = request.get_json(force=True)
        aid = body.get('aid', '')
        group_id = body.get('groupId', '')
        inst = aid_instances.get(aid)
        if inst:
            inst.active_group_id = group_id
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/info')
def api_group_info():
    try:
        group_id = request.args.get('groupId', '')
        aid = request.args.get('aid', '')
        if not group_id or not aid:
            return jsonify({'success': False, 'error': '缺少参数'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        info = inst.group_ops.get_group_info(group_id)
        return jsonify({'success': True, **info})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/send', methods=['POST'])
def api_group_send():
    try:
        body = request.get_json(force=True)
        group_id = body.get('groupId', '')
        message = body.get('message', '')
        aid = body.get('aid', '')
        if not group_id or not message:
            return jsonify({'success': False, 'error': '缺少 groupId 或 message'})
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.send_message(group_id, message)
        # Store locally
        with group_message_lock:
            msgs = group_message_stores.setdefault(group_id, [])
            msgs.append({
                'msg_id': result.get('msg_id', 0),
                'sender': aid,
                'content': message,
                'content_type': 'text',
                'timestamp': result.get('timestamp', int(time.time() * 1000)),
            })
        return jsonify({'success': True, **result})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/messages')
def api_group_messages():
    try:
        group_id = request.args.get('groupId', '')
        aid = request.args.get('aid', '')
        before_msg_id = int(request.args.get('before_msg_id', '0') or '0')
        limit = int(request.args.get('limit', '0') or '0')
        fetch_limit = limit if limit > 0 else 30
        if not aid or not group_id:
            return jsonify({'success': True, 'messages': [], 'has_more': False})
        inst = aid_instances.get(aid)
        with group_message_lock:
            store = group_message_stores.setdefault(group_id, [])

            if inst and inst.group_ops:
                if before_msg_id > 0:
                    # 向上翻页：拉 before_msg_id 之前的消息
                    local_oldest = min((m.get('msg_id', 0) for m in store), default=0) if store else 0
                    if not store or local_oldest >= before_msg_id:
                        try:
                            fetch_after = max(0, before_msg_id - fetch_limit - 1)
                            result = inst.group_ops.pull_messages(group_id, after_msg_id=fetch_after, limit=fetch_limit)
                            _merge_to_store(store, result.get('messages', []))
                        except Exception:
                            pass
                else:
                    # 初次加载：始终从服务端拉最新一页
                    try:
                        probe = inst.group_ops.pull_messages(group_id, after_msg_id=0, limit=1)
                        latest = probe.get('latest_msg_id', 0)
                        if latest > 0:
                            start = max(0, latest - fetch_limit)
                            result = inst.group_ops.pull_messages(group_id, after_msg_id=start, limit=fetch_limit)
                            _merge_to_store(store, result.get('messages', []))
                        else:
                            _merge_to_store(store, probe.get('messages', []))
                    except Exception:
                        pass

            # 分页返回
            msgs = list(store)
        if before_msg_id > 0:
            msgs = [m for m in msgs if m.get('msg_id', 0) < before_msg_id]
        has_more = False
        if len(msgs) > fetch_limit:
            has_more = True
            msgs = msgs[-fetch_limit:]
        elif msgs:
            # 返回的最早一条 msg_id > 1 说明服务端还有更早的消息
            has_more = msgs[0].get('msg_id', 0) > 1
        return jsonify({'success': True, 'messages': msgs, 'has_more': has_more})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e), 'messages': [], 'has_more': False})


def _merge_to_store(store: list, new_msgs: list):
    if not new_msgs:
        return
    existing_ids = {m.get('msg_id') for m in store}
    for m in new_msgs:
        if m.get('msg_id') and m['msg_id'] not in existing_ids:
            store.append(m)
            existing_ids.add(m['msg_id'])
    store.sort(key=lambda x: x.get('msg_id', 0))


@app.route('/api/group/invite-code', methods=['POST'])
def api_group_invite_code():
    try:
        body = request.get_json(force=True)
        group_id = body.get('groupId', '')
        aid = body.get('aid', '')
        if not group_id or not aid:
            return jsonify({'success': False, 'error': '缺少参数'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.create_invite_code(group_id)
        group_url = f'https://{inst.group_target_aid}/{group_id}'
        return jsonify({'success': True, **result, 'group_url': group_url})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


def _finalize_group_join(inst: AidInstance, group_id: str):
    """After joining a group, fetch name and store locally."""
    name = group_id
    try:
        info = inst.group_ops.get_group_info(group_id)
        name = info.get('name', group_id)
    except Exception:
        pass
    groups = group_list_stores.setdefault(inst.aid, [])
    if not any(g['group_id'] == group_id for g in groups):
        groups.append({'group_id': group_id, 'name': name})


@app.route('/api/group/join', methods=['POST'])
def api_group_join():
    try:
        body = request.get_json(force=True)
        group_url = body.get('groupUrl', '')
        code = body.get('code', '')
        aid = body.get('aid', '')
        if not group_url:
            return jsonify({'success': False, 'error': '缺少群聊链接'})
        if not aid:
            return jsonify({'success': False, 'error': '缺少 aid'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        target_aid, group_id = GroupOperations.parse_group_url(group_url)
        if code:
            inst.group_ops.use_invite_code(group_id, code)
            _finalize_group_join(inst, group_id)
            return jsonify({'success': True, 'group_id': group_id})
        else:
            result = inst.group_ops.request_join(group_id, body.get('message', ''))
            if result['status'] == 'joined':
                _finalize_group_join(inst, group_id)
                return jsonify({'success': True, 'group_id': group_id})
            else:
                return jsonify({'success': True, 'pending': True, 'request_id': result['request_id']})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/pending-requests')
def api_group_pending_requests():
    try:
        group_id = request.args.get('groupId', '')
        aid = request.args.get('aid', '')
        if not group_id or not aid:
            return jsonify({'success': True, 'requests': []})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.get_pending_requests(group_id)
        return jsonify({'success': True, **result})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e), 'requests': []})


@app.route('/api/group/review-join', methods=['POST'])
def api_group_review_join():
    try:
        body = request.get_json(force=True)
        group_id = body.get('groupId', '')
        agent_id = body.get('agentId', '')
        action = body.get('action', '')
        aid = body.get('aid', '')
        if not group_id or not agent_id or not action or not aid:
            return jsonify({'success': False, 'error': '缺少参数'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        inst.group_ops.review_join_request(group_id, agent_id, action, body.get('reason', ''))
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/members')
def api_group_members():
    try:
        group_id = request.args.get('groupId', '')
        aid = request.args.get('aid', '')
        if not group_id or not aid:
            return jsonify({'success': True, 'members': []})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.get_members(group_id)
        return jsonify({'success': True, **result})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e), 'members': []})


@app.route('/api/group/my-groups')
def api_group_my_groups():
    try:
        aid = request.args.get('aid', '')
        if not aid:
            return jsonify({'success': True, 'groups': [], 'total': 0})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.list_my_groups()
        groups = []
        for m in result.get('groups', []):
            name = m.get('group_id', '')
            try:
                info = inst.group_ops.get_group_info(m['group_id'])
                name = info.get('name', name)
            except Exception:
                pass
            groups.append({**m, 'name': name})
        return jsonify({'success': True, 'groups': groups, 'total': result.get('total', 0)})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e), 'groups': []})


@app.route('/api/group/leave', methods=['POST'])
def api_group_leave():
    try:
        body = request.get_json(force=True)
        group_id = body.get('groupId', '')
        aid = body.get('aid', '')
        if not group_id or not aid:
            return jsonify({'success': False, 'error': '缺少参数'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        inst.group_ops.leave_group(group_id)
        # Remove from local store
        groups = group_list_stores.get(aid, [])
        group_list_stores[aid] = [g for g in groups if g['group_id'] != group_id]
        group_message_stores.pop(group_id, None)
        if inst.active_group_id == group_id:
            inst.active_group_id = None
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/duty-status')
def api_group_duty_status():
    try:
        aid = request.args.get('aid', '')
        group_id = request.args.get('groupId', '')
        if not aid or not group_id:
            return jsonify({'success': True, 'config': {}, 'state': {}})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        result = inst.group_ops.get_duty_status(group_id)
        return jsonify({'success': True, **result})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


@app.route('/api/group/update-duty-config', methods=['POST'])
def api_group_update_duty_config():
    try:
        body = request.get_json(force=True)
        group_id = body.get('groupId', '')
        aid = body.get('aid', '')
        mode = body.get('mode', '')
        if not aid or not group_id or not mode:
            return jsonify({'success': False, 'error': '缺少参数'})
        inst = ensure_online(aid)
        ensure_group_client(inst)
        inst.group_ops.update_duty_config(group_id, {'mode': mode})
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})


# ============================================================
# Server Startup
# ============================================================
def start_server(port: int = 9527, data_dir: str = '', seed_password: str = '', open_browser: bool = True):
    global global_data_dir, global_seed_password
    global_data_dir = os.path.abspath(data_dir) if data_dir else os.path.join(os.getcwd(), 'acp-data')
    # 密码校验：参数 > 环境变量，都没有则拒绝启动
    if not seed_password:
        seed_password = os.environ.get('ACP_SEED_PASSWORD', '')
    if not seed_password:
        raise ValueError('seed_password 未设置，请通过参数传入或设置环境变量 ACP_SEED_PASSWORD')
    global_seed_password = seed_password
    os.makedirs(global_data_dir, exist_ok=True)
    load_aid_md_options()
    ws_port = port + 1
    print(f'ACP Web Server starting on http://localhost:{port}')
    print(f'WebSocket server starting on ws://localhost:{ws_port}')
    print(f'Data dir: {global_data_dir}')
    threading.Thread(target=_start_ws_server, args=(ws_port,), daemon=True).start()
    start_health_check()
    if open_browser:
        def _open_browser():
            import webbrowser
            time.sleep(1)
            webbrowser.open(f'http://localhost:{port}')
        threading.Thread(target=_open_browser, daemon=True).start()
    try:
        app.run(host='0.0.0.0', port=port, threaded=True)
    except OSError as e:
        if 'Address already in use' in str(e) or getattr(e, 'errno', 0) == 48:
            print(f'\n端口 {port} 已被占用，请使用 --port 指定其他端口，或先关闭占用该端口的进程')
        else:
            print(f'\n启动失败: {e}')
    except KeyboardInterrupt:
        print('\n正在退出...')
