#!/usr/bin/env python3
"""Hermes Web Chat Server - 与终端共享记忆，保持登录"""

import http.server
import json
import hashlib
import os
import time
import threading
import urllib.request
import urllib.error

# 配置
PORT = 8888
HOST = "0.0.0.0"
API_SERVER_URL = "http://127.0.0.1:8080"
API_KEY = "2ecbb693e174732519b42cc6b7417c313bac1eaf003cafeb757d9f10306b07b6"
WEB_PASSWORD = "Hermes2026!"

# 固定 session ID - 网页和终端共享（与终端保持一致）
SHARED_SESSION_ID = "hermes-main-session"

# Session 存储（仅用于对话历史缓存）
sessions = {}
sessions_lock = threading.Lock()
MAX_HISTORY = 30

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

def load_memory_context():
    """加载终端 agent 的记忆文件"""
    context_parts = []
    memories_dir = os.path.expanduser("~/.hermes/memories")
    
    user_md = os.path.join(memories_dir, "USER.md")
    if os.path.exists(user_md):
        try:
            with open(user_md, 'r') as f:
                content = f.read().strip()
                if content:
                    context_parts.append(f"## 用户信息\n{content}")
        except:
            pass
    
    memory_md = os.path.join(memories_dir, "MEMORY.md")
    if os.path.exists(memory_md):
        try:
            with open(memory_md, 'r') as f:
                content = f.read().strip()
                if content:
                    if len(content) > 2000:
                        content = "...(truncated)\n" + content[-2000:]
                    context_parts.append(f"## 记忆\n{content}")
        except:
            pass
    
    if context_parts:
        return "\n\n".join(context_parts)
    return None

def call_hermes_api(messages, session_id=SHARED_SESSION_ID, max_tokens=2000):
    """调用 Hermes API Server"""
    memory_context = load_memory_context()
    if memory_context:
        messages = [{"role": "system", "content": memory_context}] + messages
    
    payload = json.dumps({
        "model": "hermes-agent",
        "messages": messages,
        "max_tokens": max_tokens
    }).encode()

    req = urllib.request.Request(
        f"{API_SERVER_URL}/v1/chat/completions",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {API_KEY}",
            "X-Hermes-Session-Id": session_id
        },
        method="POST"
    )
    try:
        with urllib.request.urlopen(req, timeout=300) as resp:
            data = json.loads(resp.read().decode())
            content = data["choices"][0]["message"]["content"]
            return True, content
    except urllib.error.HTTPError as e:
        body = e.read().decode() if e.fp else ""
        return False, f"API Error {e.code}: {body}"
    except Exception as e:
        return False, str(e)


# ========== HTML ==========

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;align-items:center;justify-content:center}
.login-box{background:#1a1a2e;border:1px solid #2a2a4a;border-radius:16px;padding:40px;width:360px;text-align:center}
.login-box h2{color:#7c83fd;margin-bottom:8px;font-size:1.5em}
.login-box p{color:#888;margin-bottom:24px;font-size:14px}
.login-box input{width:100%;padding:12px 16px;font-size:15px;border:2px solid #2a2a4a;border-radius:8px;background:#0f0f23;color:#e0e0e0;outline:none;text-align:center;letter-spacing:2px}
.login-box input:focus{border-color:#7c83fd}
.login-box button{width:100%;padding:12px;font-size:15px;background:#7c83fd;color:white;border:none;border-radius:8px;cursor:pointer;margin-top:16px}
.login-box button:hover{background:#6a73e0}
.err{color:#ff6b6b;font-size:13px;margin-top:12px;display:none}
</style>
</head>
<body>
<div class="login-box">
 <h2>🦉 Hermes Chat</h2>
 <p>请输入密码进入聊天</p>
 <input type="password" id="pwd" placeholder="密码" autofocus onkeydown="if(e.key==='Enter')login()">
 <button onclick="login()">进入</button>
 <div class="err" id="err">密码错误，请重试</div>
</div>
<script>
async function login(){
 const pwd=document.getElementById('pwd').value;
 const r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pwd})});
 const d=await r.json();
 if(d.ok){window.location.href='/';}
 else{document.getElementById('err').style.display='block';document.getElementById('pwd').value='';}
}
</script>
</body>
</html>"""

VIDEO_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</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;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px}
h1{color:#7c83fd;margin:20px 0;font-size:1.5em}
.video-container{background:#1a1a2e;border:1px solid #2a2a4a;border-radius:12px;padding:16px;max-width:900px;width:100%}
video{width:100%;border-radius:8px;display:block}
.info{margin-top:16px;padding:12px;background:#1a1a2e;border-radius:8px;color:#888;font-size:14px}
.back{display:inline-block;margin-top:20px;padding:10px 20px;background:#7c83fd;color:white;text-decoration:none;border-radius:8px}
.back:hover{background:#6a73e0}
</style>
</head>
<body>
<h1>📺 AI 新闻播报</h1>
<div class="video-container">
<video controls autoplay playsinline>
<source src="/static/news-broadcast.mp4" type="video/mp4">
您的浏览器不支持视频播放
</video>
</div>
<div class="info">
<p>🎬 这是由 AI 自动生成的新闻播报视频</p>
<p>📐 分辨率: 1280x720 | ⏱ 时长: 96秒 | 📦 大小: 2.2MB</p>
</div>
<a href="/" class="back">← 返回聊天</a>
</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}
#header-btns{display:flex;gap:8px}
#clear-btn{padding:6px 14px;background:#444;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}
.message.system{align-self:center;background:#2a2a4a;font-size:.85em;color:#888}
#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:.2s}
.typing span:nth-child(3){animation-delay:.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>
 <div id="header-btns">
  <button id="clear-btn" onclick="clearChat()">清空对话</button>
 </div>
</div>
<div id="messages">
 <div class="message system">已连接到 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 clearChat(){fetch('/api/clear',{method:'POST'}).then(()=>{msgs.innerHTML='<div class="message system">对话已清空。</div>'})}
</script>
</body>
</html>"""


# ========== Handler ==========

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

    def is_authenticated(self):
        cookie = self.headers.get('Cookie', '')
        for part in cookie.split(';'):
            part = part.strip()
            if part.startswith('auth='):
                token = part[5:]
                expected = hashlib.sha256(WEB_PASSWORD.encode()).hexdigest()
                if token == expected:
                    return True
        return False

    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 serve_file(self, filepath, content_type):
        """Serve a static file"""
        try:
            with open(filepath, 'rb') as f:
                data = f.read()
            self.send_response(200)
            self.send_header('Content-Type', content_type)
            self.send_header('Content-Length', str(len(data)))
            self.send_header('Accept-Ranges', 'bytes')
            self.end_headers()
            self.wfile.write(data)
        except FileNotFoundError:
            self.send_response(404)
            self.end_headers()

    def do_GET(self):
        # 未登录则显示登录页
        if not self.is_authenticated():
            # 允许访问静态视频文件（用于已登录用户的直接链接）
            if self.path == '/video' or self.path == '/video/':
                pass  # fall through to login
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(LOGIN_PAGE.encode())
            return

        # 视频页面
        if self.path == '/video' or self.path == '/video/':
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.end_headers()
            self.wfile.write(VIDEO_PAGE.encode())
            return

        # 静态视频文件
        if self.path == '/static/news-broadcast.mp4':
            self.serve_file('/root/news-broadcast.mp4', 'video/mp4')
            return

        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(CHAT_PAGE.encode())

    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:
                req = json.loads(body)
                password = req.get('password', '')
                if password == WEB_PASSWORD:
                    token = hashlib.sha256(WEB_PASSWORD.encode()).hexdigest()
                    self.send_response(200)
                    self.send_header('Content-Type', 'application/json')
                    self.send_header('Set-Cookie', f'auth={token}; Path=/; HttpOnly; SameSite=Strict')
                    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}).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())

        elif path == '/api/clear':
            sid, data = self.get_session()
            if sid and data:
                data['history'] = []
                self.set_session(sid, data)
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'ok': True}).encode())

        elif path == '/api/chat':
            if not self.is_authenticated():
                self.send_response(403)
                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

                # 获取或创建 session
                sid, data = self.get_session()
                if not sid:
                    sid = gen_session_id()
                    data = {'created': time.time(), 'history': []}
                    self.set_session(sid, data)

                # 构建 messages
                history = data.get('history', [])
                messages = []
                for h in history[-MAX_HISTORY:]:
                    messages.append({"role": h["role"], "content": h["content"]})
                messages.append({"role": "user", "content": message})

                # 调用 Hermes API Server（共享 session）
                ok, response = call_hermes_api(messages, session_id=SHARED_SESSION_ID)

                if ok:
                    history.append({"role": "user", "content": message})
                    history.append({"role": "assistant", "content": response})
                    data['history'] = history
                    self.set_session(sid, data)

                self.send_response(200)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                if ok:
                    self.wfile.write(json.dumps({'ok': True, 'response': response}).encode())
                else:
                    self.wfile.write(json.dumps({'ok': False, 'error': response}).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)
    print(f"Hermes Web Chat on http://{HOST}:{PORT}")
    print(f"Shared session: {SHARED_SESSION_ID}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down...")
        server.shutdown()


if __name__ == '__main__':
    main()
