#!/usr/bin/env python3
"""Hermes Web Chat Server - password-protected web chat interface.
Uses only Python standard library. No pip installs required.

Usage:
    python3 server.py

Then open http://<server-ip>:8888 in a browser.
Default password: Hermes2026!  (change it below)
"""

import http.server
import json
import urllib.parse
import hashlib
import os
import sys
import subprocess
import time
import threading

# ========== CONFIG ==========
PASSWORD = "Hermes2026!"
PORT = 8888
HOST = "0.0.0.0"
HERMES_CLI = "hermes"
USE_SSL = False  # Set True only if NOT behind Nginx SSL termination
SSL_CERT = "/etc/nginx/hermes.crt"
SSL_KEY = "/etc/nginx/hermes.key"
# ============================

sessions = {}
sessions_lock = threading.Lock()


def gen_session_id():
    return hashlib.sha256(os.urandom(32) + str(time.time()).encode()).hexdigest()[:32]


LOGIN_PAGE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermes Chat - 登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f23; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; }
h1 { font-size: 2.5em; color: #7c83fd; }
input { padding: 14px 20px; font-size: 16px; border: 2px solid #7c83fd; border-radius: 10px; background: #1a1a2e; color: #e0e0e0; width: 320px; outline: none; }
input:focus { border-color: #a5abff; }
button { padding: 14px 50px; font-size: 16px; background: #7c83fd; color: white; border: none; border-radius: 10px; cursor: pointer; }
button:hover { background: #6a73e0; }
.error { color: #ff6b6b; display: none; }
</style>
</head>
<body>
    <h1>🦉 Hermes Chat</h1>
    <p style="color:#888; margin-bottom:10px;">输入密码进入聊天</p>
    <input type="password" id="pw" placeholder="请输入密码" autofocus onkeydown="if(event.key==='Enter')go()">
    <button onclick="go()">进入</button>
    <p class="error" id="err">密码错误</p>
    <script>
    function go() {
        var pw = document.getElementById('pw').value;
        fetch('/api/login', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw})})
            .then(r => r.json())
            .then(d => { if(d.ok) location.reload(); else document.getElementById('err').style.display='block'; })
            .catch(() => document.getElementById('err').style.display='block');
    }
    </script>
</body>
</html>"""

CHAT_PAGE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermes Chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f23; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
#header { padding: 12px 20px; background: #1a1a2e; border-bottom: 1px solid #2a2a4a; display: flex; justify-content: space-between; align-items: center; }
#header h2 { color: #7c83fd; font-size: 1.2em; }
#logout-btn { padding: 6px 14px; background: #ff6b6b; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
#messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.message { max-width: 80%; padding: 10px 14px; border-radius: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 14px; }
.message.user { align-self: flex-end; background: #7c83fd; color: white; }
.message.assistant { align-self: flex-start; background: #1a1a2e; border: 1px solid #2a2a4a; }
.message.error { align-self: center; background: #2a1a1a; border: 1px solid #6b2020; color: #ff6b6b; }
#input-area { padding: 12px 16px; background: #1a1a2e; border-top: 1px solid #2a2a4a; display: flex; gap: 8px; }
#msg-input { flex: 1; padding: 10px 14px; font-size: 14px; border: 2px solid #2a2a4a; border-radius: 8px; background: #0f0f23; color: #e0e0e0; outline: none; resize: none; min-height: 42px; max-height: 100px; }
#msg-input:focus { border-color: #7c83fd; }
#send-btn { padding: 10px 20px; font-size: 14px; background: #7c83fd; color: white; border: none; border-radius: 8px; cursor: pointer; }
#send-btn:hover { background: #6a73e0; }
#send-btn:disabled { background: #444; cursor: not-allowed; }
.typing { display: flex; gap: 4px; padding: 10px 14px; }
.typing span { width: 7px; height: 7px; background: #7c83fd; border-radius: 50%; animation: bounce 1.4s infinite; }
.typing span:nth-child(2) { animation-delay: 0.2s; }
.typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0f0f23; }
::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
</style>
</head>
<body>
<div id="header">
    <h2>🦉 Hermes Chat</h2>
    <button id="logout-btn" onclick="logout()">退出</button>
</div>
<div id="messages">
    <div class="message assistant">你好！我是 Hermes Agent，有什么可以帮你的？</div>
</div>
<div id="input-area">
    <textarea id="msg-input" placeholder="输入消息... (Enter 发送, Shift+Enter 换行)" rows="1"></textarea>
    <button id="send-btn" onclick="send()">发送</button>
</div>
<script>
const input = document.getElementById('msg-input');
const btn = document.getElementById('send-btn');
const msgs = document.getElementById('messages');
input.addEventListener('keydown', e => { if(e.key==='Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 100) + 'px'; });
function addMsg(role, text) {
    const d = document.createElement('div');
    d.className = 'message ' + role;
    d.textContent = text;
    msgs.appendChild(d);
    msgs.scrollTop = msgs.scrollHeight;
}
function showTyping() {
    const d = document.createElement('div');
    d.className = 'message assistant typing';
    d.id = 'typing';
    d.innerHTML = '<span></span><span></span><span></span>';
    msgs.appendChild(d);
    msgs.scrollTop = msgs.scrollHeight;
}
function hideTyping() { const e = document.getElementById('typing'); if(e) e.remove(); }
async function send() {
    const text = input.value.trim();
    if(!text) return;
    addMsg('user', text);
    input.value = '';
    input.style.height = 'auto';
    btn.disabled = true;
    showTyping();
    try {
        const r = await fetch('/api/chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({message:text})});
        const d = await r.json();
        hideTyping();
        if(d.ok) addMsg('assistant', d.response);
        else addMsg('error', d.error || '未知错误');
    } catch(e) {
        hideTyping();
        addMsg('error', '连接失败: ' + e.message);
    }
    btn.disabled = false;
    input.focus();
}
function logout() { fetch('/api/logout', {method:'POST'}).then(() => location.reload()); }
</script>
</body>
</html>"""


class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass

    def get_session(self):
        cookie = self.headers.get('Cookie', '')
        for part in cookie.split(';'):
            part = part.strip()
            if part.startswith('sid='):
                sid = part[4:]
                with sessions_lock:
                    if sid in sessions:
                        return sid, sessions[sid]
        return None, None

    def set_session(self, sid, data):
        with sessions_lock:
            sessions[sid] = data

    def del_session(self, sid):
        with sessions_lock:
            sessions.pop(sid, None)

    def do_GET(self):
        sid, data = self.get_session()
        path = self.path.split('?')[0]
        if path == '/':
            if sid and data:
                self.send_response(200)
                self.send_header('Content-Type', 'text/html; charset=utf-8')
                self.end_headers()
                self.wfile.write(CHAT_PAGE.encode())
            else:
                self.send_response(200)
                self.send_header('Content-Type', 'text/html; charset=utf-8')
                self.end_headers()
                self.wfile.write(LOGIN_PAGE.encode())
        else:
            self.send_response(404)
            self.end_headers()

    def do_POST(self):
        path = self.path.split('?')[0]
        content_len = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_len) if content_len else b''

        if path == '/api/login':
            try:
                data = json.loads(body)
                if data.get('password') == PASSWORD:
                    sid = gen_session_id()
                    self.set_session(sid, {'created': time.time()})
                    self.send_response(200)
                    self.send_header('Content-Type', 'application/json')
                    self.send_header('Set-Cookie', f'sid={sid}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400')
                    self.end_headers()
                    self.wfile.write(json.dumps({'ok': True}).encode())
                else:
                    self.send_response(200)
                    self.send_header('Content-Type', 'application/json')
                    self.end_headers()
                    self.wfile.write(json.dumps({'ok': False, 'error': '密码错误'}).encode())
            except Exception as e:
                self.send_response(400)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': False, 'error': str(e)}).encode())

        elif path == '/api/logout':
            sid, _ = self.get_session()
            if sid:
                self.del_session(sid)
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.send_header('Set-Cookie', 'sid=; Path=/; Max-Age=0')
            self.end_headers()
            self.wfile.write(json.dumps({'ok': True}).encode())

        elif path == '/api/chat':
            sid, data = self.get_session()
            if not sid:
                self.send_response(401)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': False, 'error': '未登录'}).encode())
                return
            try:
                req = json.loads(body)
                message = req.get('message', '').strip()
                if not message:
                    self.send_response(200)
                    self.send_header('Content-Type', 'application/json')
                    self.end_headers()
                    self.wfile.write(json.dumps({'ok': False, 'error': '消息为空'}).encode())
                    return
                result = subprocess.run(
                    [HERMES_CLI, 'chat', '-Q', '-q', message, '--yolo'],
                    capture_output=True, text=True, timeout=120,
                    env={**os.environ, 'PATH': os.environ.get('PATH', '')}
                )
                response = result.stdout.strip()
                lines = [l for l in response.split('\n')
                         if l.strip() and not l.strip().startswith('session_id:')]
                response = '\n'.join(lines).strip()
                if result.returncode != 0 and not response:
                    response = f"Error: {result.stderr.strip()}" if result.stderr.strip() else "未知错误"
                self.send_response(200)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': True, 'response': response}).encode())
            except subprocess.TimeoutExpired:
                self.send_response(200)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': False, 'error': '请求超时，请重试'}).encode())
            except Exception as e:
                self.send_response(200)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps({'ok': False, 'error': str(e)}).encode())
        else:
            self.send_response(404)
            self.end_headers()


def main():
    server = http.server.HTTPServer((HOST, PORT), Handler)
    if USE_SSL:
        import ssl
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.load_cert_chain(SSL_CERT, SSL_KEY)
        server.socket = ctx.wrap_socket(server.socket, server_side=True)
        proto = "HTTPS"
    else:
        proto = "HTTP"
    print(f"Hermes Web Chat running on {proto}://{HOST}:{PORT}")
    print(f"Password: {PASSWORD}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down...")
        server.shutdown()


if __name__ == '__main__':
    main()
