#!/usr/bin/env python3
"""轻量 CORS 代理服务 - 转发推送请求到目标 API + 单组件重新生成"""

import json
import os
import cgi
import sys
import time
import logging
import threading
import traceback
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path

import requests

UPLOAD_DIR = os.path.expanduser("~/.openclaw/workspace-xiaoban/tmp/uploads")

UPLOAD_HTML = r"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件到大麦</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;padding:20px}
.container{max-width:600px;margin:0 auto;text-align:center}
h1{font-size:24px;margin:16px 0 4px}
.sub{color:#94a3b8;margin-bottom:24px;font-size:14px}
.upload-box{background:#1e293b;border:2px dashed #475569;border-radius:16px;padding:40px 20px;margin-bottom:16px}
.upload-box p{font-size:15px;color:#94a3b8;margin-bottom:16px}
.btn{display:inline-block;background:#6366f1;color:#fff;border:none;padding:12px 32px;border-radius:8px;font-size:16px;cursor:pointer;transition:background .2s}
.btn:active{background:#4f46e5}
.btn-wrap{position:relative;overflow:hidden}
.btn-wrap input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}
#progress{display:none;margin:12px 0}
.bar-bg{background:#334155;border-radius:8px;height:8px;overflow:hidden}
.bar{background:linear-gradient(90deg,#6366f1,#a855f7);height:100%;width:0;transition:width .3s}
#status{font-size:13px;color:#94a3b8;margin-bottom:8px}
#result{margin:12px 0;font-size:14px;line-height:1.8}
.success{color:#4ade80}
.error{color:#f87171}
.recent{margin-top:24px;text-align:left;max-height:180px;overflow-y:auto}
.recent h3{font-size:14px;color:#64748b;margin-bottom:8px}
.recent li{font-size:12px;color:#94a3b8;padding:4px 0;border-bottom:1px solid #1e293b}
</style>
</head>
<body>
<div class="container">
<h1>📤 上传文件到大麦</h1>
<p class="sub">点击下方按钮选择文件，支持多选</p>
<div class="upload-box">
<p>📁 选择视频、压缩包等文件上传</p>
<div class="btn-wrap">
<span class="btn">选择文件</span>
<input type="file" id="file" multiple>
</div>
</div>
<div id="progress">
<p id="status">上传中...</p>
<div class="bar-bg"><div class="bar" id="bar"></div></div>
</div>
<div id="result"></div>
<div class="recent" id="recent"><h3>📋 最近上传</h3><ul id="filelist"></ul></div>
</div>
<script>
document.getElementById('file').addEventListener('change', function(){
  if(this.files.length) upload(this.files);
});

async function upload(files){
  var progress = document.getElementById('progress');
  var bar = document.getElementById('bar');
  var status = document.getElementById('status');
  var result = document.getElementById('result');
  progress.style.display='block'; bar.style.width='0'; result.innerHTML='';

  var ok=0, fail=0;
  for(var i=0;i<files.length;i++){
    var f = files[i];
    status.textContent = '正在上传: ' + f.name + ' (' + (f.size/1024/1024).toFixed(1) + 'MB)...';
    bar.style.width = ((i/files.length)*100)+'%';
    var fd = new FormData(); fd.append('file', f);
    try{
      var r = await fetch('/upload', {method:'POST', body:fd});
      var t = await r.json();
      if(r.ok){ ok++; } else { fail++; result.innerHTML+='<div class="error">❌ '+f.name+': '+(t.error||'失败')+'</div>'; }
    }catch(e){ fail++; result.innerHTML+='<div class="error">❌ '+f.name+': 网络错误</div>'; }
  }
  bar.style.width='100%';
  if(fail===0) result.innerHTML+='<div class="success">✅ 全部 '+ok+' 个文件上传成功!</div>';
  else result.innerHTML+='<div class="success">✅ '+ok+' 个成功</div><div class="error">❌ '+fail+' 个失败</div>';
  status.textContent='上传完成!';
  loadFiles();
  setTimeout(function(){ progress.style.display='none'; result.innerHTML=''; }, 5000);
}

async function loadFiles(){
  try{
    var r = await fetch('/upload/files');
    var files = await r.json();
    document.getElementById('filelist').innerHTML = files.map(function(f){
      return '<li>📄 '+f.name+' <span style="color:#64748b">'+(f.size/1024/1024).toFixed(1)+'MB · '+f.time+'</span></li>';
    }).join('');
  }catch(e){}
}
loadFiles();
</script>
</body>
</html>"""

PROJECT_ROOT = Path(__file__).resolve().parent.parent
SCRIPTS_DIR = Path(__file__).resolve().parent
LOG_DIR = PROJECT_ROOT / 'logs'
LOG_DIR.mkdir(exist_ok=True)

file_handler = TimedRotatingFileHandler(LOG_DIR / 'proxy.log', when='midnight', backupCount=30, encoding='utf-8')
file_handler.suffix = '%Y-%m-%d'
file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))

logging.basicConfig(level=logging.INFO, handlers=[file_handler, stream_handler])
logger = logging.getLogger(__name__)

CONFIG_PATH = PROJECT_ROOT / 'config.json'

def load_config():
    with open(CONFIG_PATH, 'r') as f:
        return json.load(f)['proxy']

CONFIG = load_config()

VALID_BODY_KEYS = {'componentData', 'pushRelationKp', 'pushType'}

# --- Rate Limiter ---
class RateLimiter:
    def __init__(self, max_per_minute):
        self.max_per_minute = max_per_minute
        self.records = {}
        self.lock = threading.Lock()

    def is_allowed(self, ip):
        now = time.time()
        with self.lock:
            timestamps = self.records.get(ip, [])
            timestamps = [t for t in timestamps if now - t < 60]
            if len(timestamps) >= self.max_per_minute:
                self.records[ip] = timestamps
                return False
            timestamps.append(now)
            self.records[ip] = timestamps
            return True

rate_limiter = RateLimiter(CONFIG['rate_limit_per_minute'])


# --- Regenerate Handler ---
def _do_regenerate(body_data):
    """调用 generate_component 或 generate_kp_info 重新生成单个组件/知识点"""
    if str(SCRIPTS_DIR) not in sys.path:
        sys.path.insert(0, str(SCRIPTS_DIR))

    mode = body_data.get("mode", "component")
    type_name = body_data.get("type_name", "")
    cId = body_data.get("cId", "")
    level = body_data.get("level") or None

    if not type_name:
        return 400, {"error": "缺少 type_name"}

    if mode == "kp":
        # 仅重新生成知识点
        from generate_json import generate_kp_info
        from match_component import match_component_type
        from llm_client import LLMClient

        knowledge_text = body_data.get("knowledge_text", "")
        if not knowledge_text.strip():
            return 400, {"error": "缺少 knowledge_text"}

        try:
            type_info = match_component_type(type_name)
            cType = type_info["cType"]
        except ValueError as e:
            return 400, {"error": str(e)}

        try:
            llm_client = LLMClient()
            kp_info = generate_kp_info(cType, cId, knowledge_text, llm_client, level=level)
            return 200, {"kpInfo": kp_info, "cType": cType}
        except Exception as e:
            logger.error(f"Regenerate KP failed: {traceback.format_exc()}")
            return 500, {"error": str(e)}
    else:
        # 仅重新生成组件配置（不生成 kpInfo）
        from generate_json import generate_component
        from llm_client import LLMClient

        teaching_config = body_data.get("teaching_config", "")
        character_map = body_data.get("character_map") or {}
        section_char_map = body_data.get("section_char_map") or []

        if not teaching_config:
            return 400, {"error": "缺少 teaching_config"}

        component = {
            "type_name": type_name,
            "cId": cId,
            "teaching_config": teaching_config,
            "has_image": body_data.get("has_image", False),
            "knowledge_text": "",  # 强制为空，跳过 kpInfo 生成
            "config_info": body_data.get("config_info", ""),
        }

        try:
            llm_client = LLMClient()
            result = generate_component(component, character_map=character_map, section_char_map=section_char_map, llm_client=llm_client, level=level)
            result["kpInfo"] = None  # 确保不返回 kpInfo
            return 200, result
        except Exception as e:
            logger.error(f"Regenerate failed: {traceback.format_exc()}")
            return 500, {"error": str(e)}


class ProxyHandler(BaseHTTPRequestHandler):

    def _send_json(self, code, data, extra_headers=None):
        body = json.dumps(data, ensure_ascii=False).encode('utf-8')
        self.send_response(code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS, GET')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        if extra_headers:
            for k, v in extra_headers.items():
                self.send_header(k, v)
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _get_client_ip(self):
        forwarded = self.headers.get('X-Forwarded-For')
        if forwarded:
            return forwarded.split(',')[0].strip()
        return self.client_address[0]

    def do_OPTIONS(self):
        if self.path in ('/api/push', '/api/regenerate'):
            self.send_response(204)
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
            self.send_header('Access-Control-Allow-Headers', 'Content-Type')
            self.send_header('Access-Control-Max-Age', '86400')
            self.end_headers()
        else:
            self.send_error(404)

    def do_GET(self):
        if self.path == '/health':
            self._send_json(200, {'status': 'ok'})
        elif self.path == '/upload':
            self._serve_upload_page()
        elif self.path == '/upload/files':
            self._list_uploaded_files()
        else:
            self.send_error(404)

    def do_POST(self):
        if self.path == '/api/push':
            self._handle_push()
        elif self.path == '/api/regenerate':
            self._handle_regenerate()
        elif self.path == '/upload':
            self._handle_upload()
        else:
            self.send_error(404)

    def _read_body(self, max_size=2 * 1024 * 1024):
        content_length = int(self.headers.get('Content-Length', 0))
        if content_length == 0:
            return None, (400, {'error': 'empty body'})
        if content_length > max_size:
            return None, (413, {'error': 'body too large'})
        raw_body = self.rfile.read(content_length)
        try:
            return json.loads(raw_body), None
        except json.JSONDecodeError:
            return None, (400, {'error': 'invalid JSON'})

    def _handle_push(self):
        client_ip = self._get_client_ip()
        if not rate_limiter.is_allowed(client_ip):
            self._send_json(429, {'error': 'rate limit exceeded'})
            logger.warning(f'Rate limit hit: {client_ip}')
            return

        content_length = int(self.headers.get('Content-Length', 0))
        if content_length == 0:
            self._send_json(400, {'error': 'empty body'})
            return
        if content_length > 1024 * 1024:
            self._send_json(413, {'error': 'body too large'})
            return

        raw_body = self.rfile.read(content_length)
        try:
            body_data = json.loads(raw_body)
        except json.JSONDecodeError:
            self._send_json(400, {'error': 'invalid JSON'})
            logger.warning(f'[{client_ip}] Invalid JSON body: {raw_body[:200]}')
            return

        if not isinstance(body_data, dict) or not VALID_BODY_KEYS.intersection(body_data.keys()):
            self._send_json(400, {'error': f'body must contain one of: {", ".join(VALID_BODY_KEYS)}'})
            logger.warning(f'[{client_ip}] Invalid body keys: {list(body_data.keys()) if isinstance(body_data, dict) else type(body_data)}')
            return

        logger.info(f'[{client_ip}] POST /api/push | keys={list(body_data.keys())} | size={content_length}B')
        logger.info(f'[{client_ip}] >>> body: {raw_body.decode("utf-8", errors="replace")[:2000]}')

        try:
            resp = requests.post(
                CONFIG['target_url'],
                json=body_data,
                headers={
                    'Content-Type': 'application/json',
                    'Origin': CONFIG['target_origin'],
                },
                timeout=15,
            )
            resp_body = resp.text[:1000]
            self._send_json(resp.status_code, resp.json() if resp.headers.get('content-type', '').startswith('application/json') else {'raw': resp_body})
            logger.info(f'[{client_ip}] -> upstream {resp.status_code} | resp={resp_body[:300]}')
        except requests.RequestException as e:
            self._send_json(502, {'error': f'upstream error: {str(e)}'})
            logger.error(f'[{client_ip}] Forward failed: {e}')

    def _handle_regenerate(self):
        client_ip = self._get_client_ip()
        logger.info(f'[{client_ip}] POST /api/regenerate')

        body_data, err = self._read_body()
        if err:
            self._send_json(err[0], err[1])
            return

        if not isinstance(body_data, dict) or not body_data.get('type_name'):
            self._send_json(400, {'error': 'body must contain type_name'})
            return

        logger.info(f'[{client_ip}] Regenerating: type_name={body_data.get("type_name")}, cId={body_data.get("cId")}')

        code, result = _do_regenerate(body_data)
        self._send_json(code, result)
        logger.info(f'[{client_ip}] Regenerate done: status={code}')

    def _serve_upload_page(self):
        html = UPLOAD_HTML.encode('utf-8')
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.send_header('Content-Length', str(len(html)))
        self.end_headers()
        self.wfile.write(html)

    def _list_uploaded_files(self):
        import time as _time
        os.makedirs(UPLOAD_DIR, exist_ok=True)
        files = []
        for name in sorted(os.listdir(UPLOAD_DIR), reverse=True):
            path = os.path.join(UPLOAD_DIR, name)
            if os.path.isfile(path):
                st = os.stat(path)
                files.append({
                    'name': name,
                    'size': st.st_size,
                    'time': _time.strftime('%m-%d %H:%M', _time.localtime(st.st_mtime))
                })
        self._send_json(200, files[:20])

    def _handle_upload(self):
        content_type = self.headers.get('Content-Type', '')
        if 'multipart/form-data' not in content_type:
            self._send_json(400, {'error': '需要 multipart/form-data'})
            return

        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': content_type}
        )

        file_item = form['file']
        if file_item is None or not file_item.filename:
            self._send_json(400, {'error': '未选择文件'})
            return

        safe_name = os.path.basename(file_item.filename)
        os.makedirs(UPLOAD_DIR, exist_ok=True)
        path = os.path.join(UPLOAD_DIR, safe_name)

        base, ext = os.path.splitext(safe_name)
        counter = 1
        while os.path.exists(path):
            path = os.path.join(UPLOAD_DIR, f"{base}_{counter}{ext}")
            counter += 1

        with open(path, 'wb') as f:
            f.write(file_item.file.read())

        self._send_json(200, {'ok': True, 'filename': os.path.basename(path)})

    def log_message(self, format, *args):
        logger.debug(f'{self.client_address[0]} - {format % args}')


def main():
    port = CONFIG['port']
    server = ThreadingHTTPServer(('0.0.0.0', port), ProxyHandler)
    logger.info(f'Proxy server started on 0.0.0.0:{port}')
    logger.info(f'Target: {CONFIG["target_url"]}')
    logger.info(f'Rate limit: {CONFIG["rate_limit_per_minute"]} req/min per IP')
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        logger.info('Shutting down...')
        server.shutdown()


if __name__ == '__main__':
    main()
