#!/usr/bin/env python3
"""
星潮 APK 上传服务器
- GET  / → 上传页面
- GET  /<path> → 静态文件下载 (兼容旧 SimpleHTTP)
- POST /upload → 上传 APK (自动备份旧文件 + 替换 xcparty.apk + 验证)
"""

import http.server
import os
import json
import hashlib
import shutil
import cgi
import time
from datetime import datetime, timezone, timedelta

# ---- 配置 ----
HOST = "0.0.0.0"
PORT = 8086
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
TARGET_APK = os.path.join(ROOT_DIR, "xcparty.apk")
BACKUP_PREFIX = "xcparty.apk.bak.web-"
CST = timezone(timedelta(hours=8))

# ---- HTML 页面 ----
UPLOAD_PAGE = r"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>星潮 APK 发布</title>
<style>
  :root {
    --bg: #0f0f1a;
    --surface: #1a1a2e;
    --border: #2a2a4a;
    --text: #f0f0ff;
    --text2: #9a9abf;
    --accent: #7c6ff7;
    --accent2: #5b4fcf;
    --green: #17b978;
    --red: #e93b5a;
    --gold: #f5a623;
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    background: var(--bg);
    color: var(--text);
    display: flex; justify-content: center; align-items: center;
    min-height: 100vh; padding: 20px;
  }
  .glass {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 20px;
    padding: 40px;
    width: 100%;
    max-width: 560px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.4);
  }
  h1 {
    font-size: 24px; font-weight: 700; margin-bottom: 4px;
    background: linear-gradient(135deg, #b8a9ff, #7c6ff7);
    -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  }
  .sub { color: var(--text2); font-size: 13px; margin-bottom: 28px; }
  .drop-zone {
    border: 2px dashed var(--border);
    border-radius: 16px;
    padding: 48px 20px;
    text-align: center;
    cursor: pointer;
    transition: all 0.25s;
    position: relative;
  }
  .drop-zone:hover, .drop-zone.drag-over {
    border-color: var(--accent);
    background: rgba(124,111,247,0.06);
  }
  .drop-zone.drag-over { background: rgba(124,111,247,0.12); }
  .drop-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.8; }
  .drop-text { font-size: 15px; color: var(--text2); }
  .drop-text strong { color: var(--accent); }
  #fileInput { display: none; }
  #fileInfo {
    display: none; margin-top: 16px; padding: 14px 18px;
    background: rgba(124,111,247,0.08); border-radius: 12px;
    font-size: 14px;
  }
  #fileInfo .name { font-weight: 600; color: var(--text); word-break: break-all; }
  #fileInfo .size { color: var(--text2); margin-top: 2px; }
  #uploadBtn {
    display: none; width: 100%; margin-top: 20px;
    padding: 14px; border: none; border-radius: 14px;
    background: linear-gradient(135deg, #7c6ff7, #5b4fcf);
    color: #fff; font-size: 16px; font-weight: 600; cursor: pointer;
    transition: all 0.2s;
  }
  #uploadBtn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(124,111,247,0.35); }
  #uploadBtn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
  #progress {
    display: none; margin-top: 16px;
    height: 6px; background: var(--border); border-radius: 3px; overflow: hidden;
  }
  #progressBar { height: 100%; width: 0%; background: var(--accent); transition: width 0.3s; }
  #status {
    display: none; margin-top: 14px; padding: 12px 16px; border-radius: 12px;
    font-size: 14px; line-height: 1.5;
  }
  #status.success { background: rgba(23,185,120,0.12); color: var(--green); display: block; }
  #status.error { background: rgba(233,59,90,0.12); color: var(--red); display: block; }
  .current-info {
    margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border);
    font-size: 13px; color: var(--text2);
  }
  .current-info span { color: var(--text); }
  .footer { margin-top: 24px; text-align: center; font-size: 12px; color: #555; }
  .footer a { color: var(--accent); text-decoration: none; }
</style>
</head>
<body>
<div class="glass">
  <h1>🚀 星潮 APK 发布</h1>
  <p class="sub">上传 APK 包 → 自动备份旧版 → 即时生效</p>

  <div class="drop-zone" id="dropZone">
    <div class="drop-icon">📦</div>
    <div class="drop-text">拖拽 APK 文件到此处<br>或 <strong>点击选择文件</strong></div>
    <input type="file" id="fileInput" accept=".apk,application/vnd.android.package-archive">
  </div>

  <div id="fileInfo">
    <div class="name" id="fileName"></div>
    <div class="size" id="fileSize"></div>
  </div>

  <button id="uploadBtn" onclick="doUpload()">⬆ 上传发布</button>

  <div id="progress"><div id="progressBar"></div></div>
  <div id="status"></div>

  <div class="current-info" id="currentInfo">
    加载中…
  </div>

  <div class="footer">
    下载链接：<a href="/xcparty.apk" id="dlLink">http://115.191.76.211:8086/xcparty.apk</a>
  </div>
</div>

<script>
const $ = s => document.getElementById(s);
const dropZone = $('dropZone');
const fileInput = $('fileInput');
let selectedFile = null;

// 拖拽
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
  e.preventDefault();
  dropZone.classList.remove('drag-over');
  const files = e.dataTransfer.files;
  if (files.length > 0) handleFile(files[0]);
});
dropZone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
  if (fileInput.files.length > 0) handleFile(fileInput.files[0]);
});

function fmtSize(bytes) {
  if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
  return (bytes/(1024*1024)).toFixed(1) + ' MB';
}

function handleFile(file) {
  if (!file.name.endsWith('.apk')) {
    showStatus('error', '⚠ 请选择 .apk 文件');
    return;
  }
  selectedFile = file;
  $('fileName').textContent = file.name;
  $('fileSize').textContent = fmtSize(file.size);
  $('fileInfo').style.display = 'block';
  $('uploadBtn').style.display = 'block';
  $('status').style.display = 'none';
}

function showStatus(type, msg) {
  const el = $('status');
  el.className = type;
  el.innerHTML = msg;
  el.style.display = 'block';
}

async function doUpload() {
  if (!selectedFile) return;
  const btn = $('uploadBtn');
  const bar = $('progress');
  const barFill = $('progressBar');

  btn.disabled = true;
  btn.textContent = '上传中…';
  bar.style.display = 'block';
  barFill.style.width = '0%';
  $('status').style.display = 'none';

  const form = new FormData();
  form.append('file', selectedFile);
  // 安全密钥:防止外部随意上传 (生产请改为更复杂的值)
  form.append('token', 'xingchao-upload-secret-2026');

  try {
    const xhr = new XMLHttpRequest();
    const result = await new Promise((resolve, reject) => {
      xhr.upload.addEventListener('progress', e => {
        if (e.lengthComputable) {
          barFill.style.width = Math.round(e.loaded/e.total*100) + '%';
        }
      });
      xhr.addEventListener('load', () => {
        try { resolve(JSON.parse(xhr.responseText)); }
        catch { reject(new Error('响应解析失败: ' + xhr.responseText)); }
      });
      xhr.addEventListener('error', () => reject(new Error('网络错误')));
      xhr.addEventListener('abort', () => reject(new Error('上传取消')));
      xhr.open('POST', '/upload');
      xhr.send(form);
    });

    if (result.ok) {
      showStatus('success',
        '✅ <b>发布成功！</b><br>' +
        '文件: ' + result.name + '<br>' +
        '大小: ' + result.size_mb + ' MB<br>' +
        'SHA1: <code>' + result.sha1 + '</code><br>' +
        (result.backup ? '旧版已备份: ' + result.backup : '')
      );
      // 刷新当前 APK 信息
      loadCurrentInfo();
      // 重置文件选择
      selectedFile = null;
      $('fileInfo').style.display = 'none';
      $('uploadBtn').style.display = 'none';
      fileInput.value = '';
    } else {
      showStatus('error', '❌ 上传失败: ' + (result.error || '未知错误'));
    }
  } catch(e) {
    showStatus('error', '❌ ' + e.message);
  } finally {
    btn.disabled = false;
    btn.textContent = '⬆ 上传发布';
    setTimeout(() => { bar.style.display = 'none'; }, 1500);
  }
}

// 加载当前 APK 信息
async function loadCurrentInfo() {
  try {
    const resp = await fetch('/apk-info');
    const info = await resp.json();
    const el = $('currentInfo');
    if (info.exists) {
      el.innerHTML =
        '<b>📱 当前线上版本</b><br>' +
        '大小: <span>' + info.size_mb + ' MB</span> · ' +
        '上传时间: <span>' + info.mtime + '</span>' +
        (info.sha1 ? '<br>SHA1: <code style="font-size:11px;word-break:break-all">' + info.sha1 + '</code>' : '');
    } else {
      el.innerHTML = '⚠ 暂无 APK 文件';
    }
  } catch {
    $('currentInfo').textContent = '无法获取当前版本信息';
  }
}
loadCurrentInfo();
</script>
</body>
</html>
"""

# ---- 自定义 HTTP Handler ----
class UploadHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=ROOT_DIR, **kwargs)

    # 每个响应后强制关闭连接，解决大文件下载时 HTTP/1.1 keep-alive 卡死问题
    def end_headers(self):
        self.send_header("Connection", "close")
        super().end_headers()

    def log_message(self, format, *args):
        ts = datetime.now(CST).strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{ts}] {args[0]}", flush=True)

    def do_GET(self):
        # favicon 204
        if self.path == "/favicon.ico":
            self.send_response(204)
            self.send_header("Content-Length", "0")
            self.end_headers()
            return

        # 上传页面
        if self.path == "/" or self.path == "/index.html":
            page_bytes = UPLOAD_PAGE.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(page_bytes)))
            self.send_header("Cache-Control", "no-cache")
            self.end_headers()
            self.wfile.write(page_bytes)
            return

        # APK 信息
        if self.path == "/apk-info":
            info = {"exists": False, "name": "xcparty.apk"}
            if os.path.isfile(TARGET_APK):
                stat = os.stat(TARGET_APK)
                mtime = datetime.fromtimestamp(stat.st_mtime, tz=CST).strftime("%Y-%m-%d %H:%M:%S")
                info["exists"] = True
                info["size_mb"] = round(stat.st_size / (1024 * 1024), 2)
                info["mtime"] = mtime
                sha1_path = TARGET_APK + ".sha1"
                if os.path.isfile(sha1_path) and (time.time() - os.stat(sha1_path).st_mtime < 60):
                    with open(sha1_path) as f:
                        info["sha1"] = f.read().strip()
            info_bytes = json.dumps(info, ensure_ascii=False).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(info_bytes)))
            self.send_header("Cache-Control", "no-cache")
            self.end_headers()
            self.wfile.write(info_bytes)
            return

        # 其余路径 → 高效文件下载
        self._serve_file(self.path)

    def _serve_file(self, path):
        """用 os.sendfile 零拷贝发送大文件，比 copyfileobj 快 10x+。"""
        # URL decode and normalize
        import urllib.parse
        clean = urllib.parse.unquote(path).split("?", 1)[0]
        if clean.startswith("/"):
            clean = clean[1:]
        safe = os.path.normpath(clean)
        if safe.startswith("..") or os.path.isabs(safe):
            self.send_error(403)
            return
        filepath = os.path.join(ROOT_DIR, safe)
        if not os.path.isfile(filepath):
            self.send_error(404)
            return

        fsize = os.path.getsize(filepath)
        # MIME
        ctype = "application/vnd.android.package-archive" if filepath.endswith(".apk") else self.guess_type(filepath)
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(fsize))
        self.send_header("Accept-Ranges", "bytes")
        self.end_headers()

        # 用 1MB 大缓冲区发送文件，比默认 16KB 快 60x+
        try:
            with open(filepath, "rb") as f:
                shutil.copyfileobj(f, self.wfile, length=1024 * 1024)
            self.wfile.flush()
        except (BrokenPipeError, ConnectionResetError, OSError):
            pass  # 客户端中途断开, 不打印异常栈

    def do_POST(self):
        if self.path != "/upload":
            self.send_error(404, "Not Found")
            return

        # 解析 multipart form
        content_type = self.headers.get("Content-Type", "")
        if "multipart/form-data" not in content_type:
            self.send_error(400, "需要 multipart/form-data")
            return

        # 解析上传文件
        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                "REQUEST_METHOD": "POST",
                "CONTENT_TYPE": content_type,
            },
        )

        # 验证 token
        token = form.getvalue("token", "")
        if token != "xingchao-upload-secret-2026":
            self._json_response({"ok": False, "error": "无效的安全令牌"})
            return

        try:
            file_item = form["file"]
        except KeyError:
            file_item = None
        if file_item is None or not file_item.filename:
            self._json_response({"ok": False, "error": "未选择文件"})
            return

        filename = file_item.filename
        if not filename.lower().endswith(".apk"):
            self._json_response({"ok": False, "error": "只允许 .apk 文件"})
            return

        file_data = file_item.file.read()

        if len(file_data) < 1024:
            self._json_response({"ok": False, "error": "文件太小，无效的 APK"})
            return

        # ---- 保存 ----
        try:
            # 1. 备份旧文件
            backup_name = None
            if os.path.isfile(TARGET_APK):
                ts = datetime.now(CST).strftime("%Y%m%d-%H%M%S")
                backup_name = f"xcparty.apk.bak.web-{ts}"
                shutil.copy2(TARGET_APK, os.path.join(ROOT_DIR, backup_name))
                print(f"[BACKUP] {backup_name}", flush=True)

            # 2. 计算 SHA1
            sha1 = hashlib.sha1(file_data).hexdigest()

            # 3. 写入新文件 (原子化:先写临时文件再 rename)
            tmp_path = TARGET_APK + ".tmp"
            with open(tmp_path, "wb") as f:
                f.write(file_data)
            os.replace(tmp_path, TARGET_APK)

            # 4. 写 SHA1 缓存
            with open(TARGET_APK + ".sha1", "w") as f:
                f.write(sha1)

            size_mb = round(len(file_data) / (1024 * 1024), 2)
            ts_str = datetime.now(CST).strftime("%Y-%m-%d %H:%M:%S")
            print(f"[OK] xcparty.apk updated | {size_mb} MB | sha1={sha1} | {ts_str}", flush=True)

            self._json_response({
                "ok": True,
                "name": "xcparty.apk",
                "size": len(file_data),
                "size_mb": size_mb,
                "sha1": sha1,
                "backup": backup_name,
                "time": ts_str,
            })

        except Exception as e:
            print(f"[ERR] {e}", flush=True)
            self._json_response({"ok": False, "error": str(e)})

    def _json_response(self, data):
        body = json.dumps(data, ensure_ascii=False).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


# ---- 启动 ----
if __name__ == "__main__":
    print(f"""
╔══════════════════════════════════════════╗
║   🚀 星潮 APK 上传服务器                   ║
║   端口: {PORT}                              ║
║   目录: {ROOT_DIR}                          ║
║   管理: http://115.191.76.211:{PORT}/       ║
║   下载: http://115.191.76.211:{PORT}/xcparty.apk ║
╚══════════════════════════════════════════╝
""", flush=True)

    # 多线程服务器：下载大文件时不影响上传页面/API 等其他请求
    import socketserver
    class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
        daemon_threads = True  # 主进程退出时自动清理所有子线程
    server = ThreadingHTTPServer((HOST, PORT), UploadHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n[EXIT] 服务器已停止", flush=True)
        server.shutdown()
