import io
import json
import mimetypes
import os
import shutil
import socket
import socketserver  # 引入 socketserver 用于多线程
import urllib.parse
import zipfile
from http.server import BaseHTTPRequestHandler, HTTPServer

# --- 配置 ---
PORT = 8000
BASE_DIR = os.path.abspath(os.getcwd())
BUFFER_SIZE = 4 * 1024 * 1024


def parse_byte_range(range_header, file_size):
    if not range_header or not range_header.startswith("bytes=") or file_size <= 0:
        return None

    range_value = range_header.removeprefix("bytes=").split(",", 1)[0].strip()
    if "-" not in range_value:
        return None

    start_text, end_text = range_value.split("-", 1)
    try:
        if start_text == "":
            suffix_length = int(end_text)
            if suffix_length <= 0:
                return None
            start = max(file_size - suffix_length, 0)
            end = file_size - 1
        else:
            start = int(start_text)
            end = int(end_text) if end_text else file_size - 1
    except ValueError:
        return None

    if start < 0 or end < start or start >= file_size:
        return None

    return start, min(end, file_size - 1)


def copy_file_range(source, target, length):
    remaining = length
    while remaining > 0:
        chunk = source.read(min(BUFFER_SIZE, remaining))
        if not chunk:
            break
        target.write(chunk)
        remaining -= len(chunk)


# --- 前端 HTML/CSS/JS ---
HTML_CONTENT = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python 极速文件服务器</title>
    <style>
        :root {
            --primary: #10B981; --primary-hover: #059669; --primary-light: #D1FAE5; --primary-text: #047857;
            --bg: #F1F5F9; --card: #FFFFFF;
            --text-main: #0F172A; --text-muted: #64748B; --text-light: #94A3B8;
            --border: #E2E8F0; --hover: #F8FAFC;
            --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
            --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
            --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px;
        }
        body { font-family: 'Inter', -apple-system, sans-serif; background-color: var(--bg); color: var(--text-main); margin: 0; padding: 40px 20px; line-height: 1.5; }
        .container { max-width: 960px; margin: 0 auto; }
        .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
        .header-left { display: flex; align-items: center; gap: 16px; }
        .header h1 { font-size: 22px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 8px; }
        .header h1 svg { color: var(--primary); }
        .status-badge { background-color: var(--primary-light); color: var(--primary-text); padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; }
        .breadcrumb { background: var(--card); padding: 6px; border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); border: 1px solid var(--border); display: flex; align-items: center; gap: 6px; margin-bottom: 24px; }
        .btn-icon { background: transparent; border: none; padding: 8px; border-radius: var(--radius-md); cursor: pointer; color: var(--text-muted); transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
        .btn-icon:hover { background: var(--hover); color: var(--text-main); }
        .path-input { flex: 1; font-family: monospace; font-size: 14px; color: var(--text-main); padding: 10px 12px; border: 1px solid transparent; border-radius: var(--radius-md); background: var(--hover); outline: none; transition: all 0.2s; }
        .path-input:focus { background: var(--card); border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
        .upload-area { background: var(--card); border: 2px dashed #CBD5E1; border-radius: var(--radius-xl); padding: 32px 20px; text-align: center; transition: all 0.2s; margin-bottom: 24px; cursor: pointer; }
        .upload-area:hover, .upload-area.dragover { border-color: var(--primary); background: #F0FDF4; }
        .upload-icon { width: 40px; height: 40px; color: var(--primary); margin-bottom: 12px; opacity: 0.8; }
        .upload-title { font-weight: 600; font-size: 16px; margin: 0 0 4px 0; }
        .upload-subtitle { color: var(--text-muted); font-size: 13px; margin: 0; }
        .action-bar { display: none; background: var(--text-main); color: white; padding: 12px 20px; border-radius: var(--radius-lg); margin-bottom: 16px; justify-content: space-between; align-items: center; box-shadow: var(--shadow-md); }
        .file-card { background: var(--card); border-radius: var(--radius-lg); border: 1px solid var(--border); box-shadow: var(--shadow-md); overflow: auto; max-height: 600px; }
        .file-table { width: 100%; border-collapse: collapse; text-align: left; }
        .file-table th { position: sticky; top: 0; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(4px); padding: 14px 20px; font-size: 12px; font-weight: 600; color: var(--text-muted); border-bottom: 1px solid var(--border); z-index: 10; }
        .file-item td { padding: 14px 20px; border-bottom: 1px solid var(--hover); transition: background 0.2s; }
        .file-item:last-child td { border-bottom: none; }
        .file-item:hover td { background: var(--hover); }
        .file-info { display: flex; align-items: center; gap: 12px; cursor: pointer; }
        .file-name { font-weight: 500; font-size: 14px; word-break: break-all; }
        .file-size { color: var(--text-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
        .btn-primary { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: var(--radius-md); cursor: pointer; font-weight: 600; font-size: 14px; transition: background 0.2s; }
        .btn-primary:hover { background: var(--primary-hover); }
        .btn-white { background: white; color: var(--text-main); border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 13px; }
        .btn-white:hover { background: var(--hover); }
        input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }
        #progress-container { position: fixed; bottom: 30px; right: 30px; width: 320px; background: var(--card); padding: 20px; border-radius: var(--radius-lg); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1); display: none; z-index: 100; border: 1px solid var(--border); }
        .prog-bar { height: 6px; background: var(--hover); border-radius: 3px; overflow: hidden; margin-top: 12px; }
        .prog-fill { height: 100%; background: var(--primary); width: 0%; transition: width 0.1s linear; }
        .svg-icon { width: 20px; height: 20px; flex-shrink: 0; }
        .icon-folder { color: #F59E0B; fill: #FEF3C7; }
        .icon-file { color: #3B82F6; fill: #EFF6FF; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <div class="header-left">
                <h1><svg class="svg-icon" style="width:28px;height:28px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path></svg> 极速文件服务</h1>
                <span class="status-badge">无限制模式已开启</span>
            </div>
        </div>

        <div class="breadcrumb">
            <button class="btn-icon" onclick="goBack()" title="返回上级">
                <svg class="svg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
            </button>
            <input type="text" class="path-input" id="path-input" onkeypress="handlePathEnter(event)" placeholder="输入绝对路径...">
            <button class="btn-primary" onclick="goToPath()">跳转</button>
        </div>

        <div class="upload-area" id="drop-zone">
            <svg class="upload-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
            <p class="upload-title">点击选择 或 拖拽多个文件到此处</p>
            <p class="upload-subtitle">支持大文件极速流式上传，无限大小</p>
            <input type="file" id="file-input" hidden multiple>
        </div>

        <div class="action-bar" id="batch-bar">
            <span id="selected-count" style="font-weight: 500; font-size: 14px;">已选中 0 项</span>
            <button class="btn-white" onclick="downloadSelected()">打包下载 (ZIP)</button>
        </div>

        <div class="file-card">
            <table class="file-table">
                <thead>
                    <tr>
                        <th style="width: 40px; padding-right: 0;"><input type="checkbox" id="select-all" onclick="toggleAll()"></th>
                        <th>文件名称</th>
                        <th style="width: 120px; text-align: right;">大小</th>
                        <th style="text-align: center; width: 80px;">操作</th>
                    </tr>
                </thead>
                <tbody id="file-list"></tbody>
            </table>
        </div>
    </div>

    <div id="progress-container">
        <div style="display: flex; justify-content: space-between; font-size: 14px; font-weight: 500;">
            <span id="prog-title" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 75%;">正在准备...</span>
            <span id="prog-percent" style="color: var(--primary);">0%</span>
        </div>
        <div class="prog-bar"><div class="prog-fill" id="prog-fill"></div></div>
    </div>

    <script>
        // 核心修复 2：去掉周围的引号，因为 Python 会直接注入一个带有双引号的安全 JSON 字符串，防止特殊字符导致前端崩溃
        let currentPath = {{INITIAL_PATH}};

        const icons = {
            folder: `<svg class="svg-icon icon-folder" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`,
            file: `<svg class="svg-icon icon-file" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>`,
            download: `<svg class="svg-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`
        };

        function handlePathEnter(e) { if (e.key === 'Enter') goToPath(); }

        function goToPath() {
            let newPath = document.getElementById('path-input').value.trim();
            if (!newPath) return;
            currentPath = newPath;
            loadList();
        }

        async function loadList() {
            const tbody = document.getElementById('file-list');
            document.getElementById('path-input').value = currentPath;
            // 增加视觉反馈：防止用户以为页面卡死
            tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: var(--text-muted);">⏳ 正在加载目录内容...</td></tr>';

            try {
                const res = await fetch(`/api/list?path=${encodeURIComponent(currentPath)}`);
                const data = await res.json();

                if (data.error) {
                    tbody.innerHTML = `<tr><td colspan="4" style="text-align:center; padding: 40px; color: #ef4444;">❌ ${data.error}</td></tr>`;
                    return;
                }

                currentPath = data.path;
                document.getElementById('path-input').value = currentPath;
                tbody.innerHTML = '';
                document.getElementById('select-all').checked = false;
                updateBatchBar();

                if (data.files.length === 0) {
                    tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: var(--text-light);">文件夹为空</td></tr>';
                    return;
                }

                data.files.forEach((file) => {
                    const tr = document.createElement('tr');
                    tr.className = 'file-item';
                    const encodedDownloadPath = encodeURIComponent(joinPath(currentPath, file.name));

                    tr.innerHTML = `
                        <td style="padding-right: 0;"><input type="checkbox" class="file-check" data-name="${file.name}" onclick="updateBatchBar(event)"></td>
                        <td>
                            <div class="file-info" onclick="${file.is_dir ? `enterDir('${file.name.replace(/'/g, "\\'")}')` : ''}">
                                ${file.is_dir ? icons.folder : icons.file}
                                <span class="file-name">${file.name}</span>
                            </div>
                        </td>
                        <td class="file-size" style="text-align: right;">${file.size || '--'}</td>
                        <td style="text-align: center;">
                            ${!file.is_dir ? `<a href="/api/download?path=${encodedDownloadPath}" class="btn-icon" style="margin: 0 auto;" download title="下载">${icons.download}</a>` : ''}
                        </td>
                    `;
                    tbody.appendChild(tr);
                });
            } catch (err) {
                console.error("加载失败", err);
                tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #ef4444;">❌ 网络请求失败，请检查终端日志</td></tr>';
            }
        }

        const joinPath = (p1, p2) => {
            p1 = p1.replace(/\\\\/g, '/');
            return p1.endsWith('/') ? p1 + p2 : p1 + '/' + p2;
        };

        function enterDir(name) { currentPath = joinPath(currentPath, name); loadList(); }

        function goBack() {
            let normalized = currentPath.replace(/\\\\/g, '/');
            if (normalized === '/' || normalized.match(/^[a-zA-Z]:\\/?$/) || normalized.match(/^[a-zA-Z]:\\/$/)) return;
            const parts = normalized.split('/').filter(Boolean);
            parts.pop();
            if (parts.length === 0) currentPath = '/';
            else if (parts.length === 1 && parts[0].match(/^[a-zA-Z]:$/)) currentPath = parts[0] + '/';
            else currentPath = parts.join('/');
            loadList();
        }

        function toggleAll() {
            const master = document.getElementById('select-all');
            document.querySelectorAll('.file-check').forEach(c => c.checked = master.checked);
            updateBatchBar();
        }

        function updateBatchBar(e) {
            if(e) e.stopPropagation();
            const selected = document.querySelectorAll('.file-check:checked');
            const bar = document.getElementById('batch-bar');
            document.getElementById('selected-count').innerText = `已选中 ${selected.length} 项`;
            bar.style.display = selected.length > 0 ? 'flex' : 'none';
        }

        function downloadSelected() {
            const selected = Array.from(document.querySelectorAll('.file-check:checked')).map(cb => cb.getAttribute('data-name'));
            if (selected.length === 0) return;
            const url = `/api/zip?path=${encodeURIComponent(currentPath)}&files=${encodeURIComponent(selected.join(','))}`;
            window.location.href = url;
            document.getElementById('select-all').checked = false;
            toggleAll();
        }

        const dropZone = document.getElementById('drop-zone');
        const fileInput = document.getElementById('file-input');
        dropZone.onclick = () => fileInput.click();
        dropZone.ondragover = (e) => { e.preventDefault(); dropZone.classList.add('dragover'); };
        dropZone.ondragleave = () => dropZone.classList.remove('dragover');
        dropZone.ondrop = (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); handleUpload(e.dataTransfer.files); };
        fileInput.onchange = (e) => handleUpload(e.target.files);

        async function handleUpload(files) {
            if (files.length === 0) return;
            const container = document.getElementById('progress-container');
            const fill = document.getElementById('prog-fill');
            const percent = document.getElementById('prog-percent');
            container.style.display = 'block';

            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                document.getElementById('prog-title').innerText = `(${i+1}/${files.length}) ${file.name}`;
                await new Promise((resolve) => {
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', '/api/upload');
                    xhr.setRequestHeader('X-File-Name', encodeURIComponent(file.name));
                    xhr.setRequestHeader('X-Target-Dir', encodeURIComponent(currentPath));
                    xhr.upload.onprogress = (e) => {
                        if (e.lengthComputable) {
                            const p = Math.round((e.loaded / e.total) * 100);
                            fill.style.width = p + '%';
                            percent.innerText = p + '%';
                        }
                    };
                    xhr.onload = () => resolve();
                    xhr.send(file);
                });
            }
            setTimeout(() => { container.style.display = 'none'; fill.style.width = '0%'; loadList(); }, 500);
            fileInput.value = '';
        }

        loadList();
    </script>
</body>
</html>
"""


# 核心修复 1: 引入 socketserver.ThreadingMixIn，实现多线程 HTTP 服务
# 彻底解决现代浏览器建立多个连接导致的"单线程阻塞假死"问题
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
    daemon_threads = True  # 确保主进程退出时，子线程自动结束


class FastHTTPServer(ThreadingHTTPServer):
    def server_bind(self):
        super().server_bind()
        # 核心修复 3: 增加容错，避免部分 OS 拒绝底层网络配置导致服务器启动失败
        try:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8 * 1024 * 1024)
            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        except Exception:
            pass  # 如果系统不支持强行接管 TCP 栈，则静默忽略，不影响核心功能


class SimpleFileManager(BaseHTTPRequestHandler):
    def get_real_path(self, req_path):
        decoded_path = urllib.parse.unquote(req_path)
        decoded_path = os.path.normpath(decoded_path)
        if os.path.isabs(decoded_path):
            return decoded_path
        return os.path.abspath(os.path.join(BASE_DIR, decoded_path))

    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        query = urllib.parse.parse_qs(parsed.query)

        if parsed.path == "/":
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()

            # 使用 json.dumps 安全转义路径，防止路径中的单双引号导致 JS 崩溃
            safe_initial_path = json.dumps(BASE_DIR.replace("\\", "/"))
            init_html = HTML_CONTENT.replace("{{INITIAL_PATH}}", safe_initial_path)
            self.wfile.write(init_html.encode("utf-8"))

        elif parsed.path == "/api/list":
            target_dir = self.get_real_path(query.get("path", ["/"])[0])
            if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
                self.send_json({"error": "该目录不存在或无法访问"})
                return

            files = []
            try:
                for item in os.listdir(target_dir):
                    if item.startswith("."):
                        continue
                    full = os.path.join(target_dir, item)
                    is_dir = os.path.isdir(full)
                    size = os.path.getsize(full) if not is_dir else 0
                    files.append(
                        {
                            "name": item,
                            "is_dir": is_dir,
                            "size": self.human_size(size) if not is_dir else "",
                        }
                    )
                files.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
                self.send_json({"path": target_dir.replace("\\", "/"), "files": files})
            except Exception as e:
                self.send_json({"error": f"没有权限读取该目录: {str(e)}"})

        elif parsed.path == "/api/download":
            file_path = self.get_real_path(query.get("path", [""])[0])
            self.serve_file(file_path)

        elif parsed.path == "/api/zip":
            base_path = self.get_real_path(query.get("path", ["/"])[0])
            file_names = query.get("files", [""])[0].split(",")
            zip_buffer = io.BytesIO()
            with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
                for name in file_names:
                    full_path = os.path.join(base_path, name)
                    if os.path.isfile(full_path):
                        zf.write(full_path, name)
            zip_buffer.seek(0)
            self.send_response(200)
            self.send_header("Content-Type", "application/zip")
            self.send_header(
                "Content-Disposition", 'attachment; filename="batch_download.zip"'
            )
            self.end_headers()
            self.wfile.write(zip_buffer.read())

        else:
            # 补齐缺漏的路由回退，防止游览器请求如 /favicon.ico 时连接挂起
            self.send_error(404, "Not Found")

    def do_POST(self):
        if self.path == "/api/upload":
            try:
                name = urllib.parse.unquote(self.headers.get("X-File-Name", "unknown"))
                target_dir = self.get_real_path(self.headers.get("X-Target-Dir", "/"))
                os.makedirs(target_dir, exist_ok=True)
                save_path = os.path.join(target_dir, name)
                content_length = int(self.headers.get("Content-Length", 0))

                with open(save_path, "wb") as f:
                    remaining = content_length
                    while remaining > 0:
                        chunk = self.rfile.read(min(remaining, BUFFER_SIZE))
                        if not chunk:
                            break
                        f.write(chunk)
                        remaining -= len(chunk)
                self.send_json({"status": "ok"})
            except Exception as e:
                print(f"上传异常: {e}")
                self.send_error(500)

    def serve_file(self, path):
        if os.path.isfile(path):
            file_size = os.path.getsize(path)
            byte_range = parse_byte_range(self.headers.get("Range"), file_size)
            status_code = 206 if byte_range else 200
            start, end = byte_range if byte_range else (0, file_size - 1)
            content_length = end - start + 1 if file_size else 0

            self.send_response(status_code)
            mime, _ = mimetypes.guess_type(path)
            self.send_header("Content-Type", mime or "application/octet-stream")
            self.send_header("Accept-Ranges", "bytes")
            self.send_header("Content-Length", str(content_length))
            if byte_range:
                self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
            safe_filename = urllib.parse.quote(os.path.basename(path))
            self.send_header(
                "Content-Disposition", f"attachment; filename*=UTF-8''{safe_filename}"
            )
            self.end_headers()
            try:
                with open(path, "rb") as f:
                    if byte_range:
                        f.seek(start)
                        copy_file_range(f, self.wfile, content_length)
                    else:
                        shutil.copyfileobj(f, self.wfile, length=BUFFER_SIZE)
            except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
                return
        else:
            self.send_error(404)

    def send_json(self, data):
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode("utf-8"))

    def human_size(self, n):
        for unit in ["B", "KB", "MB", "GB"]:
            if n < 1024:
                return f"{n:.1f} {unit}"
            n /= 1024
        return f"{n:.1f} TB"

    def log_message(self, format, *args):
        pass


if __name__ == "__main__":
    hostname = socket.gethostname()
    try:
        local_ip = socket.gethostbyname(hostname)
    except:
        local_ip = "127.0.0.1"

    server_address = ("0.0.0.0", PORT)
    httpd = FastHTTPServer(server_address, SimpleFileManager)

    print("=" * 45)
    print(f"🚀 Python 极速文件服务器 (多线程版) 已启动！")
    print(f"🔗 本机访问: http://localhost:{PORT}")
    print(f"🌐 局域网访问: http://{local_ip}:{PORT}")
    print(f"🛑 终止服务请在终端按 Ctrl+C")
    print("=" * 45)

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n👋 服务已停止。")
