#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
局域网聊天服务器 - Python + WebSocket
支持文字和图片 (Base64)
支持 Ctrl+V 粘贴图片发送
运行: python chat_server.py
访问: http://你的IP:8080
"""

import json
import asyncio
import websockets
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
import socket
import os
import time
import sys
import subprocess

# ---------- 配置 ----------
HTTP_PORT = 8080
WS_PORT = 8765
HOST = '0.0.0.0'

# 存储所有连接的客户端
connected_clients = set()
client_names = {}

# ---------- 获取本机局域网 IP ----------
def get_local_ip():
    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:
        return '127.0.0.1'

LOCAL_IP = get_local_ip()

# ---------- 自动添加防火墙规则 ----------
def add_firewall_rule():
    """自动添加防火墙入站规则"""
    try:
        import ctypes
        is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
        
        if not is_admin:
            print("⚠️ 当前不是管理员权限，防火墙规则可能无法自动添加")
            print("💡 请尝试以管理员身份重新运行: 右键 -> 以管理员身份运行")
            return False
        
        rule_name = "局域网聊天服务"
        ports = f"{HTTP_PORT},{WS_PORT}"
        
        subprocess.run(f'netsh advfirewall firewall delete rule name="{rule_name}"', 
                      shell=True, capture_output=True)
        
        cmd = f'netsh advfirewall firewall add rule name="{rule_name}" dir=in action=allow protocol=TCP localport={ports}'
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        
        if "确定" in result.stdout or "Ok" in result.stdout:
            print(f"✅ 防火墙规则已添加: {rule_name} (端口 {ports})")
            return True
        else:
            print(f"⚠️ 防火墙规则添加可能失败: {result.stdout}")
            return False
            
    except Exception as e:
        print(f"⚠️ 无法自动配置防火墙: {e}")
        return False

# ---------- WebSocket 服务器 ----------
async def ws_handler(websocket):
    connected_clients.add(websocket)
    client_names[websocket] = f"用户{len(connected_clients)}"
    print(f"✅ 新客户端连接，当前在线: {len(connected_clients)}")
    
    try:
        async for message in websocket:
            try:
                data = json.loads(message)
                if data.get('type') == 'set_name':
                    client_names[websocket] = data.get('name', '匿名')
                    continue
                if data.get('senderId'):
                    data['senderName'] = client_names.get(websocket, '匿名')
                    for client in list(connected_clients):
                        if client != websocket:
                            try:
                                await client.send(json.dumps(data))
                            except:
                                pass
                    print(f"📨 转发消息: {data.get('senderName')} -> {len(connected_clients)-1} 个客户端")
            except json.JSONDecodeError:
                pass
    except websockets.exceptions.ConnectionClosed:
        pass
    finally:
        connected_clients.remove(websocket)
        if websocket in client_names:
            del client_names[websocket]
        print(f"❌ 客户端断开，当前在线: {len(connected_clients)}")

# ---------- HTTP 服务 ----------
class ChatHTTPHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.path = '/index.html'
        return super().do_GET()
    def log_message(self, format, *args):
        pass

def start_http_server():
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    httpd = HTTPServer((HOST, HTTP_PORT), ChatHTTPHandler)
    print(f"🌐 HTTP 服务运行在: http://{LOCAL_IP}:{HTTP_PORT}")
    httpd.serve_forever()

# ---------- 生成 HTML ----------
def create_html_file():
    html_content = f'''<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>局域网聊天 - 支持粘贴图片</title>
    <style>
        * {{ box-sizing: border-box; margin: 0; padding: 0; }}
        body {{
            font-family: system-ui, -apple-system, sans-serif;
            background: #1e1f22;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 16px;
        }}
        .chat-app {{
            max-width: 800px;
            width: 100%;
            height: 90vh;
            max-height: 750px;
            background: #2b2d31;
            border-radius: 32px;
            display: flex;
            flex-direction: column;
            overflow: hidden;
            border: 1px solid #3f4248;
        }}
        .chat-header {{
            padding: 18px 24px;
            background: #1e1f22;
            border-bottom: 1px solid #3f4248;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
        }}
        .chat-header h1 {{
            font-size: 1.25rem;
            font-weight: 600;
            color: #f2f3f5;
            display: flex;
            align-items: center;
            gap: 10px;
        }}
        .chat-header h1 small {{
            font-size: 0.7rem;
            color: #949aa4;
            font-weight: 400;
        }}
        .status-badge {{
            display: flex;
            align-items: center;
            gap: 8px;
            background: #2b2d31;
            padding: 6px 14px;
            border-radius: 40px;
            border: 1px solid #3f4248;
            color: #b5bac1;
            font-size: 0.8rem;
        }}
        .status-dot {{
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: #3ba55d;
            display: inline-block;
            animation: pulse 2s infinite;
        }}
        @keyframes pulse {{ 0%, 100% {{ opacity: 1; }} 50% {{ opacity: 0.4; }} }}
        .message-area {{
            flex: 1;
            overflow-y: auto;
            padding: 16px 20px;
            display: flex;
            flex-direction: column;
            gap: 12px;
            background: #2b2d31;
        }}
        .message-area::-webkit-scrollbar {{ width: 6px; }}
        .message-area::-webkit-scrollbar-thumb {{ background: #4e5058; border-radius: 8px; }}
        .message-item {{
            display: flex;
            flex-direction: column;
            max-width: 80%;
            animation: fadeIn 0.2s ease;
        }}
        .message-item.self {{ align-self: flex-end; align-items: flex-end; }}
        .message-item.other {{ align-self: flex-start; align-items: flex-start; }}
        .bubble {{
            background: #3f4248;
            padding: 10px 14px;
            border-radius: 20px;
            border-bottom-left-radius: 6px;
            color: #e3e5e8;
            word-break: break-word;
            line-height: 1.5;
            max-width: 100%;
        }}
        .message-item.self .bubble {{
            background: #5865f2;
            border-bottom-left-radius: 20px;
            border-bottom-right-radius: 6px;
            color: white;
        }}
        .bubble img {{
            max-width: 240px;
            max-height: 300px;
            border-radius: 16px;
            display: block;
            margin: 4px 0;
            border: 1px solid #4e5058;
            object-fit: contain;
        }}
        .msg-meta {{
            font-size: 0.7rem;
            color: #949aa4;
            margin: 4px 8px 2px 8px;
        }}
        .message-item.self .msg-meta {{ text-align: right; }}
        .sender-name {{
            font-weight: 500;
            color: #b9bec6;
            font-size: 0.8rem;
            padding-left: 6px;
            margin-bottom: 2px;
        }}
        .input-panel {{
            background: #1e1f22;
            padding: 14px 20px 20px 20px;
            border-top: 1px solid #3f4248;
            flex-shrink: 0;
        }}
        .input-row {{
            display: flex;
            gap: 10px;
            align-items: flex-end;
        }}
        .input-row textarea {{
            flex: 1;
            background: #2b2d31;
            border: 1px solid #3f4248;
            border-radius: 24px;
            padding: 12px 16px;
            color: #f2f3f5;
            font-size: 0.95rem;
            resize: none;
            min-height: 48px;
            max-height: 120px;
            outline: none;
            font-family: inherit;
            line-height: 1.4;
        }}
        .input-row textarea:focus {{
            border-color: #5865f2;
            box-shadow: 0 0 0 2px #5865f244;
        }}
        .input-row textarea::placeholder {{
            color: #6d717a;
        }}
        .action-buttons {{
            display: flex;
            gap: 8px;
            flex-shrink: 0;
        }}
        .btn {{
            background: #3f4248;
            border: none;
            border-radius: 40px;
            padding: 10px 18px;
            color: #e3e5e8;
            font-weight: 500;
            font-size: 0.9rem;
            cursor: pointer;
            transition: 0.15s;
            height: 48px;
            min-width: 48px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
        }}
        .btn-primary {{
            background: #5865f2;
            color: white;
            padding: 10px 22px;
        }}
        .btn-primary:hover {{ background: #4752c4; transform: scale(0.97); }}
        .btn-outline {{
            background: transparent;
            border: 1px solid #4e5058;
            color: #b5bac1;
        }}
        .btn-outline:hover {{ background: #3f4248; border-color: #6d717a; }}
        .file-input-wrap {{
            position: relative;
            overflow: hidden;
            display: inline-flex;
        }}
        .file-input-wrap input[type="file"] {{
            position: absolute;
            left: 0;
            top: 0;
            opacity: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
        }}
        .empty-state {{
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100%;
            color: #6d717a;
        }}
        .empty-state span {{
            background: #3f4248;
            padding: 8px 20px;
            border-radius: 30px;
            font-size: 0.9rem;
        }}
        .paste-hint {{
            font-size: 0.7rem;
            color: #6d717a;
            padding: 4px 8px;
            background: #1e1f22;
            border-radius: 12px;
            display: inline-block;
            margin-top: 6px;
        }}
        @keyframes fadeIn {{
            from {{ opacity: 0.4; transform: translateY(6px); }}
            to {{ opacity: 1; transform: translateY(0); }}
        }}
        @media (max-width: 480px) {{
            .chat-app {{ height: 95vh; max-height: 95vh; border-radius: 24px; }}
            .bubble img {{ max-width: 180px; }}
        }}
    </style>
</head>
<body>
<div class="chat-app">
    <div class="chat-header">
        <h1>
            💬 局域网聊
            <small>📋 Ctrl+V 贴图</small>
        </h1>
        <div class="status-badge">
            <span class="status-dot"></span>
            <span id="statusText">连接中...</span>
            <span id="onlineCount" style="margin-left:4px;color:#6d717a;"></span>
        </div>
    </div>
    <div class="message-area" id="messageArea">
        <div class="empty-state" id="emptyState">
            <span>👋 欢迎加入聊天</span>
            <div style="font-size:0.8rem; color:#4e5058; margin-top:12px;">
                发送文字 · 选择图片 · <strong>Ctrl+V 粘贴图片</strong>
            </div>
        </div>
    </div>
    <div class="input-panel">
        <div class="input-row">
            <textarea id="msgInput" rows="1" placeholder="输入文字… 或 Ctrl+V 粘贴图片" maxlength="2000"></textarea>
            <div class="action-buttons">
                <div class="file-input-wrap btn btn-outline" title="选择图片">
                    🖼️
                    <input type="file" id="fileInput" accept="image/*">
                </div>
                <button class="btn btn-primary" id="sendBtn">发送</button>
            </div>
        </div>
        <div class="paste-hint">💡 Ctrl+V 粘贴图片 · Enter 发送</div>
    </div>
</div>
<script>
    const WS_PORT = {WS_PORT};
    const WS_URL = `ws://${{window.location.hostname}}:${{WS_PORT}}`;
    const msgArea = document.getElementById('messageArea');
    const emptyState = document.getElementById('emptyState');
    const msgInput = document.getElementById('msgInput');
    const sendBtn = document.getElementById('sendBtn');
    const fileInput = document.getElementById('fileInput');
    const statusText = document.getElementById('statusText');
    const onlineCount = document.getElementById('onlineCount');
    const myId = 'user-' + Math.random().toString(36).substring(2, 8);
    let myName = '我';
    let ws = null;
    let reconnectTimer = null;
    let pendingImage = null;
    let isConnected = false;

    // ---------- WebSocket 连接 ----------
    function connect() {{
        if (ws && ws.readyState === WebSocket.OPEN) return;
        statusText.textContent = '连接中...';
        statusText.style.color = '#faa81a';
        try {{
            ws = new WebSocket(WS_URL);
            ws.onopen = () => {{
                isConnected = true;
                statusText.textContent = '已连接';
                statusText.style.color = '#3ba55d';
                ws.send(JSON.stringify({{ type: 'set_name', name: myName }}));
                updateOnlineCount();
            }};
            ws.onmessage = (event) => {{
                try {{
                    const data = JSON.parse(event.data);
                    if (data.senderId === myId) return;
                    addMessageToUI(data);
                }} catch (e) {{}}
            }};
            ws.onclose = () => {{
                isConnected = false;
                statusText.textContent = '断开连接';
                statusText.style.color = '#ed4245';
                onlineCount.textContent = '';
                clearTimeout(reconnectTimer);
                reconnectTimer = setTimeout(connect, 3000);
            }};
            ws.onerror = () => {{}};
        }} catch (e) {{
            setTimeout(connect, 3000);
        }}
    }}

    function updateOnlineCount() {{
        // 简单显示在线状态
        onlineCount.textContent = '●';
    }}

    // ---------- 消息渲染 ----------
    function addMessageToUI({{ senderId, senderName, text, image, timestamp }}) {{
        if (emptyState) emptyState.style.display = 'none';
        const isSelf = (senderId === myId);
        const msgDiv = document.createElement('div');
        msgDiv.className = `message-item ${{isSelf ? 'self' : 'other'}}`;
        if (!isSelf && senderName) {{
            const nameSpan = document.createElement('div');
            nameSpan.className = 'sender-name';
            nameSpan.textContent = senderName;
            msgDiv.appendChild(nameSpan);
        }}
        const bubble = document.createElement('div');
        bubble.className = 'bubble';
        if (image && image.startsWith('data:image')) {{
            const img = document.createElement('img');
            img.src = image;
            img.alt = '图片';
            img.loading = 'lazy';
            bubble.appendChild(img);
        }}
        if (text && text.trim().length > 0) {{
            const textNode = document.createElement('div');
            textNode.textContent = text;
            bubble.appendChild(textNode);
        }}
        if (!text && !image) bubble.textContent = '📭 空消息';
        msgDiv.appendChild(bubble);
        const meta = document.createElement('div');
        meta.className = 'msg-meta';
        const timeStr = timestamp ? new Date(timestamp).toLocaleTimeString('zh-CN', {{ hour: '2-digit', minute: '2-digit' }}) : '刚刚';
        meta.textContent = timeStr;
        if (isSelf) meta.textContent += ' · 我';
        msgDiv.appendChild(meta);
        msgArea.appendChild(msgDiv);
        msgArea.scrollTop = msgArea.scrollHeight;
    }}

    // ---------- 发送消息 ----------
    function sendMessage(text, imageData) {{
        if (!ws || ws.readyState !== WebSocket.OPEN) {{
            alert('未连接到服务器，请检查网络');
            return false;
        }}
        const payload = {{
            senderId: myId,
            senderName: myName,
            text: text || '',
            image: imageData || null,
            timestamp: Date.now()
        }};
        addMessageToUI(payload);
        try {{
            ws.send(JSON.stringify(payload));
            return true;
        }} catch (e) {{
            return false;
        }}
    }}

    // ---------- 图片处理 ----------
    function processImage(dataUrl) {{
        pendingImage = dataUrl;
        msgInput.value = (msgInput.value.trim() ? msgInput.value + ' ' : '') + '📎 图片已添加';
        msgInput.focus();
        autoResize(msgInput);
    }}

    function handleFileSelect(file) {{
        if (!file || !file.type.startsWith('image/')) {{
            alert('请选择图片文件');
            fileInput.value = '';
            return;
        }}
        if (file.size > 5 * 1024 * 1024) {{
            alert('图片大小超过5MB');
            fileInput.value = '';
            return;
        }}
        const reader = new FileReader();
        reader.onload = (e) => processImage(e.target.result);
        reader.onerror = () => {{ alert('读取图片失败'); fileInput.value = ''; }};
        reader.readAsDataURL(file);
    }}

    // ---------- Ctrl+V 粘贴图片 ----------
    function handlePaste(e) {{
        const items = e.clipboardData && e.clipboardData.items;
        if (!items) return;
        
        for (let i = 0; i < items.length; i++) {{
            const item = items[i];
            if (item.type.startsWith('image/')) {{
                e.preventDefault();  // 阻止默认粘贴行为
                const file = item.getAsFile();
                if (file.size > 5 * 1024 * 1024) {{
                    alert('图片大小超过5MB');
                    return;
                }}
                const reader = new FileReader();
                reader.onload = (ev) => {{
                    processImage(ev.target.result);
                    // 清空剪贴板内容，避免残留
                }};
                reader.readAsDataURL(file);
                return;
            }}
        }}
        // 如果不是图片，允许默认粘贴文字
    }}

    // ---------- 辅助函数 ----------
    function autoResize(el) {{
        el.style.height = 'auto';
        el.style.height = Math.min(el.scrollHeight, 120) + 'px';
    }}

    function handleSend() {{
        const rawText = msgInput.value.trim();
        // 如果只有图片提示文字，但实际没有图片，清除提示
        if (rawText === '📎 图片已添加' && !pendingImage) {{
            msgInput.value = '';
            return;
        }}
        if (!rawText && !pendingImage) return;
        
        const text = (rawText === '📎 图片已添加' && pendingImage) ? '' : rawText;
        sendMessage(text, pendingImage);
        
        msgInput.value = '';
        pendingImage = null;
        fileInput.value = '';
        msgInput.style.height = 'auto';
        msgInput.focus();
    }}

    // ---------- 事件绑定 ----------
    sendBtn.addEventListener('click', handleSend);
    
    msgInput.addEventListener('keydown', (e) => {{
        if (e.key === 'Enter' && !e.shiftKey) {{
            e.preventDefault();
            handleSend();
        }}
    }});
    
    msgInput.addEventListener('input', () => autoResize(msgInput));
    
    // 🔥 核心：粘贴事件监听
    document.addEventListener('paste', handlePaste);
    
    fileInput.addEventListener('change', (e) => {{
        const file = e.target.files[0];
        if (file) handleFileSelect(file);
    }});

    // ---------- 启动 ----------
    const nameInput = prompt('请输入您的昵称:', '用户' + myId.slice(-4));
    if (nameInput && nameInput.trim()) myName = nameInput.trim();
    
    autoResize(msgInput);
    connect();
    
    addMessageToUI({{
        senderId: 'system',
        senderName: '💡 系统',
        text: `欢迎，${{myName}}！试试 Ctrl+V 粘贴图片吧 🖼️`,
        timestamp: Date.now(),
        image: null
    }});
    
    console.log('✅ 聊天客户端已启动，支持 Ctrl+V 粘贴图片');
</script>
</body>
</html>'''
    
    with open('index.html', 'w', encoding='utf-8') as f:
        f.write(html_content)
    print("📄 已生成 index.html (支持 Ctrl+V 粘贴图片)")

# ---------- 主程序 ----------
async def main():
    create_html_file()
    
    # 尝试自动添加防火墙规则
    add_firewall_rule()
    
    # 显示网络信息
    print("\n" + "="*60)
    print(f"📡 服务器地址: http://{LOCAL_IP}:{HTTP_PORT}")
    print(f"📡 本机访问: http://127.0.0.1:{HTTP_PORT}")
    print(f"🔌 WebSocket: ws://{LOCAL_IP}:{WS_PORT}")
    print("="*60 + "\n")
    
    print("💡 新功能: 在输入框按 Ctrl+V 直接粘贴图片发送！")
    print("💡 如果其他设备无法访问，请关闭 Windows 防火墙\n")
    
    # 启动 HTTP 服务器
    http_thread = threading.Thread(target=start_http_server, daemon=True)
    http_thread.start()
    time.sleep(0.5)
    
    # 启动 WebSocket
    print("📡 等待客户端连接...")
    print("按 Ctrl+C 停止服务器\n")
    
    async with websockets.serve(ws_handler, HOST, WS_PORT):
        await asyncio.Future()

if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n🛑 服务器已停止")
        try:
            os.remove('index.html')
        except:
            pass
        sys.exit(0)
    except Exception as e:
        print(f"\n❌ 错误: {e}")
        print("\n💡 可能的解决方案:")
        print("   1. 更换端口: 修改脚本开头的 HTTP_PORT 和 WS_PORT")
        print("   2. 以管理员身份运行")
        print("   3. 关闭防火墙后重试")
        input("\n按 Enter 退出...")