from flask import Flask, render_template_string, request, jsonify, send_from_directory
from flask_socketio import SocketIO, emit
import os
import sys
import json
import urllib.parse
import socket
import webbrowser
import threading

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret-host-key'

# --- パス設定 (Class Post仕様: system/server.py → ROOT_DIR は1つ上) ---
SYSTEM_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(SYSTEM_DIR)
SHARED_DIR = os.path.join(ROOT_DIR, "配布資料")
SUBMIT_DIR = os.path.join(ROOT_DIR, "提出ボックス")
CONFIG_FILE = os.path.join(SYSTEM_DIR, 'config.json')
LOG_FILE = os.path.join(SYSTEM_DIR, 'server.log')


os.makedirs(SHARED_DIR, exist_ok=True)
os.makedirs(SUBMIT_DIR, exist_ok=True)

socketio = SocketIO(app, cors_allowed_origins="*", max_http_buffer_size=10000000)

# メモリ上で状態を管理
clients_data = {}  # sid -> {'name': str, 'answer': str}
judgments_data = {} # sid -> 'correct' or 'incorrect'

# --- 設定管理 (公開ファイルリスト方式に変更) ---
def load_config():
    default = {"public_files": []}
    if not os.path.exists(CONFIG_FILE):
        return default
    try:
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            config = json.load(f)
        if 'public_files' not in config:
            config['public_files'] = []
        return config
    except Exception:
        return default

def save_config(config):
    with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=4)

# ... (中略) ...

@app.route('/api/shared_files', methods=['GET'])
def list_shared():
    config = load_config()
    public_files = set(config.get('public_files', []))
    files = []
    if os.path.exists(SHARED_DIR):
        for f in sorted(os.listdir(SHARED_DIR)):
            if f.startswith('.'): continue
            # public_files に含まれていれば公開、それ以外は非公開
            is_public = f in public_files
            files.append({
                'name': f,
                'locked': not is_public  # クライアント互換性のため locked = !is_public
            })
    return jsonify(files)


@app.route('/api/toggle_permission', methods=['POST'])
def toggle_permission():
    data = request.json
    filename = data.get('filename')
    if not filename:
        return jsonify({'error': 'Filename required'}), 400
        
    config = load_config()
    public_files = config.get('public_files', [])
    
    if filename in public_files:
        public_files.remove(filename) # 公開 → 非公開
    else:
        public_files.append(filename) # 非公開 → 公開
        
    config['public_files'] = public_files
    save_config(config)
    return jsonify({'success': True})


@app.route('/download/submitted/<filename>')
def download_submitted(filename):
    # パストラバーサル防止
    safe_name = os.path.basename(filename)
    file_path = os.path.join(SUBMIT_DIR, safe_name)
    if not os.path.exists(file_path) or not os.path.isfile(file_path):
        return "File not found", 404
    if os.path.commonpath([SUBMIT_DIR, os.path.abspath(file_path)]) != SUBMIT_DIR:
        return "Forbidden", 403
    # RFC 5987 形式で日本語ファイル名を安全に返す
    encoded_name = urllib.parse.quote(safe_name)
    response = send_from_directory(SUBMIT_DIR, safe_name)
    response.headers['Content-Disposition'] = f"attachment; filename=\"{encoded_name}\"; filename*=UTF-8''{encoded_name}"
    return response

@app.route('/download/shared/<filename>')
def download_shared(filename):
    config = load_config()
    safe_name = os.path.basename(filename)
    
    # 公開リストにないファイルはダウンロード拒否
    if safe_name not in config.get('public_files', []):
        return "このファイルは現在非公開（準備中）です。", 403
        
    file_path = os.path.join(SHARED_DIR, safe_name)
    if not os.path.exists(file_path) or not os.path.isfile(file_path):
        return "File not found", 404
        
    # RFC 5987 形式で日本語ファイル名を安全に返す
    encoded_name = urllib.parse.quote(safe_name)
    response = send_from_directory(SHARED_DIR, safe_name)
    response.headers['Content-Disposition'] = f"attachment; filename=\"{encoded_name}\"; filename*=UTF-8''{encoded_name}"
    return response

def get_ip_address():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except Exception:
        IP = '127.0.0.1'
    finally:
        s.close()
    return IP

def update_student_shortcut(ip, port):
    """生徒用のショートカットHTMLを自動生成する"""
    html_content = f"""<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>PC管理システム 接続</title>
    <style>
        body {{ font-family: 'Segoe UI', 'Meiryo', sans-serif; background: #f0f2f5; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }}
        .card {{ background: white; padding: 40px; border-radius: 20px; text-align: center; max-width: 420px; width: 90%; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }}
        h1 {{ color: #2c3e50; font-size: 24px; margin: 0 0 8px 0; }}
        .subtitle {{ color: #7f8c8d; font-size: 14px; margin: 0 0 28px 0; }}
        .url-preview {{ background: #f8f9fa; padding: 10px; border-radius: 8px; font-family: monospace; font-size: 14px; color: #3498db; margin-bottom: 20px; word-break: break-all; }}
        .btn {{ display: block; width: 100%; padding: 14px; background: #3498db; color: white; border: none; border-radius: 50px; font-size: 18px; font-weight: 600; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 12px rgba(52,152,219,0.3); text-decoration: none; }}
        .btn:hover {{ background: #2980b9; transform: translateY(-1px); box-shadow: 0 6px 16px rgba(52,152,219,0.4); }}
    </style>
</head>
<body>
    <div class="card">
        <h1>PC管理システム</h1>
        <p class="subtitle">以下のボタンを押してシステムに参加してください</p>
        <div class="url-preview">http://{ip}:{port}/client</div>
        <a href="http://{ip}:{port}/client" class="btn">システムに参加する</a>
        <div style="margin-top: 20px; font-size: 12px; color: #e74c3c; background: #fdf2f2; padding: 10px; border-radius: 8px;">※ 先生と同じWi-Fiに接続している必要があります</div>
    </div>
</body>
</html>"""
    try:
        with open(os.path.join(ROOT_DIR, '生徒用ショートカット.html'), 'w', encoding='utf-8') as f:
            f.write(html_content)
    except Exception:
        pass


# クライアント用の画面 (HTML/JS/CSS を1ファイルに埋め込み)
CLIENT_HTML = """
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PC管理システム</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.1/socket.io.js"></script>
    <style>
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
        body { background: #fff; color: #111; font-family: 'Segoe UI', 'Hiragino Sans', 'Meiryo', sans-serif; font-size: 15px; line-height: 1.6; }
        .wrap { max-width: 960px; margin: 0 auto; padding: 24px 20px; }

        /* -- Login -- */
        #login-ui { display: flex; align-items: center; justify-content: center; min-height: 80vh; }
        .login-box { width: 100%; max-width: 380px; }
        .login-box h1 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
        .login-box .sub { color: #888; font-size: 13px; margin-bottom: 28px; }
        .login-box label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 6px; color: #333; }
        .login-box input[type="text"] { width: 100%; padding: 10px 12px; border: 2px solid #ddd; border-radius: 4px; font-size: 16px; outline: none; transition: border-color .15s; }
        .login-box input[type="text"]:focus { border-color: #0077B6; }
        .login-box .hint { font-size: 12px; color: #999; margin-top: 4px; }
        .login-box .btn-join { display: block; width: 100%; margin-top: 20px; padding: 12px; background: #0077B6; color: #fff; border: none; border-radius: 4px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background .15s; }
        .login-box .btn-join:hover { background: #005f8f; }

        /* -- Main UI -- */
        #main-ui { display: none; }
        .top-bar { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e5e5; padding-bottom: 16px; margin-bottom: 24px; }
        .top-bar h2 { font-size: 17px; font-weight: 700; }
        .top-bar .user-name { font-size: 13px; color: #666; background: #f5f5f5; padding: 4px 12px; border-radius: 3px; }

        .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
        @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }

        /* -- Section cards -- */
        .section { margin-bottom: 24px; }
        .section-head { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: #0077B6; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 2px solid #0077B6; }
        .section-body { }
        .section p.desc { font-size: 13px; color: #777; margin-bottom: 12px; }

        /* -- Buttons -- */
        .btn { display: inline-block; padding: 8px 18px; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s; }
        .btn-accent { background: #0077B6; color: #fff; }
        .btn-accent:hover { background: #005f8f; }
        .btn-accent:disabled { background: #aaa; cursor: default; }
        .btn-send { background: #111; color: #fff; width: 100%; padding: 10px; margin-top: 10px; }
        .btn-send:hover { background: #333; }
        .btn-outline { background: transparent; border: 1.5px solid #ddd; color: #333; }
        .btn-outline:hover { border-color: #0077B6; color: #0077B6; }
        .btn-sm { padding: 5px 12px; font-size: 12px; }

        .status-on { color: #0077B6; font-weight: 600; font-size: 13px; }
        .status-off { color: #E86C5A; font-weight: 600; font-size: 13px; }

        /* -- Canvas tools -- */
        .tool-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
        .tool-bar .tool-btn { padding: 4px 10px; border: 1.5px solid #ddd; background: #fff; border-radius: 3px; font-size: 12px; cursor: pointer; transition: all .12s; }
        .tool-bar .tool-btn.active { background: #111; color: #fff; border-color: #111; }
        .tool-bar input[type="range"] { width: 80px; accent-color: #0077B6; }

        #draw-canvas { cursor: crosshair; touch-action: none; width: 100%; border: 1px solid #ddd; border-radius: 3px; background: #fff; }

        /* -- Fullscreen Canvas Mode -- */
        .canvas-container.fullscreen {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: #fff; z-index: 5000; padding: 10px;
            display: flex; flex-direction: column; box-sizing: border-box;
        }
        .canvas-container.fullscreen .tool-bar {
            padding: 10px 0; border-bottom: 1px solid #eee; margin-bottom: 10px; justify-content: center;
        }
        .canvas-container.fullscreen canvas {
            flex: 1; width: 100%; height: auto; border: 1px solid #ccc; touch-action: none;
        }
        /* 通常時のキャンバス親要素 */
        .canvas-container { width: 100%; }

        /* -- File list -- */
        .file-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border: 1px solid #eee; border-radius: 3px; margin-bottom: 6px; font-size: 13px; }
        .file-item.locked { color: #aaa; background: #fafafa; }
        .file-item .badge { font-size: 11px; padding: 2px 8px; border-radius: 3px; font-weight: 600; }
        .badge-locked { background: #f0f0f0; color: #999; }
        .dl-link { color: #0077B6; text-decoration: none; font-weight: 600; font-size: 13px; }
        .dl-link:hover { text-decoration: underline; }

        /* -- File upload -- */
        input[type="file"] { width: 100%; padding: 8px; border: 1.5px dashed #ddd; border-radius: 4px; font-size: 13px; background: #fafafa; }

        /* -- Result overlay -- */
        .result-overlay {
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 2000;
            display: flex; align-items: center; justify-content: center; flex-direction: column;
            font-size: 48px; font-weight: 800; color: #fff;
            animation: resultAnim 3s forwards;
        }
        .result-correct { background: rgba(200, 50, 30, 0.92); }
        .result-incorrect { background: rgba(0, 90, 140, 0.92); }
        @keyframes resultAnim {
            0% { opacity: 0; transform: scale(0.7); }
            12% { opacity: 1; transform: scale(1.05); }
            20% { transform: scale(1); }
            75% { opacity: 1; }
            100% { opacity: 0; }
        }
    </style>
</head>
<body>
    <div id="login-ui">
        <div class="login-box">
            <h1>PC管理システム</h1>
            <p class="sub">参加するには氏名を入力してください</p>
            <label>氏名</label>
            <input type="text" id="username" placeholder="例: 山田 太郎" required>
            <p class="hint">※ 提出物のファイル名に自動で追加されます</p>
            <button class="btn-join" onclick="joinSystem()">参加する</button>
        </div>
    </div>

    <div class="wrap" id="main-ui">
        <div class="top-bar">
            <h2>PC管理システム</h2>
            <span class="user-name"><span id="display-name"></span></span>
        </div>

        <div class="grid">
            <div>
                <div class="section">
                    <div class="section-head">フリップ解答</div>
                    <p class="desc">ペンで回答を書いて送信してください。</p>
                    
                    <div class="canvas-container" id="canvas-wrapper">
                        <div class="tool-bar">
                            <button class="tool-btn active" id="btn-pen" onclick="setTool('pen'); this.classList.add('active'); document.getElementById('btn-eraser').classList.remove('active');">ペン</button>
                            <button class="tool-btn" id="btn-eraser" onclick="setTool('eraser'); this.classList.add('active'); document.getElementById('btn-pen').classList.remove('active');">消しゴム</button>
                            <span style="font-size:12px;color:#888;margin-left:4px;">太さ</span>
                            <input type="range" id="pen-weight" min="1" max="20" value="5" oninput="updateWeight()">
                            <button class="btn btn-outline btn-sm" onclick="clearCanvas()">全消去</button>
                            <button class="btn btn-outline btn-sm" id="btn-maximize" onclick="toggleFullscreen()">⛶ 最大化</button>
                        </div>
                        <canvas id="draw-canvas" width="600" height="300"></canvas>
                        <button class="btn btn-send" onclick="sendAnswer()">解答を送信する</button>
                    </div>
                </div>
            </div>

            <div>
                <div class="section">
                    <div class="section-head">ファイル提出</div>
                    <p class="desc">ファイルを選択して提出してください。名前が自動的にファイル名に付与されます。</p>
                    <input type="file" id="file-upload">
                    <button class="btn btn-send" onclick="uploadFile()" style="margin-top:8px;">提出する</button>
                </div>

                <div class="section">
                    <div class="section-head" style="display:flex;justify-content:space-between;align-items:center;">
                        配布資料
                        <button class="btn btn-outline btn-sm" onclick="loadSharedFiles()">更新</button>
                    </div>
                    <ul id="shared-list" style="list-style:none;padding:0;margin:0;"></ul>
                </div>
            </div>
        </div>
    </div>

    <script>
        const socket = io();
        
        // --- キャンバス関連変数 ---
        const canvas = document.getElementById('draw-canvas');
        const canvasWrapper = document.getElementById('canvas-wrapper');
        const ctx = canvas.getContext('2d');
        let isDrawing = false;
        let currentTool = 'pen';
        let currentWeight = 5;
        let isFullscreen = false;

        function joinSystem() {
            const name = document.getElementById('username').value.trim();
            if(!name) { alert("名前を入力してください"); return; }
            
            socket.emit('register', {name: name});
            document.getElementById('display-name').innerText = name;
            document.getElementById('login-ui').style.display = 'none';
            document.getElementById('main-ui').style.display = 'block';
            loadSharedFiles();
            
            // キャンバスを白で初期化
            setTimeout(clearCanvas, 100);
        }

        // --- お絵かきキャンバス処理 ---
        function clearCanvas() {
            ctx.fillStyle = "white";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }

        function setTool(tool) { currentTool = tool; }
        function updateWeight() { currentWeight = document.getElementById('pen-weight').value; }

        function startDrawing(e) { isDrawing = true; draw(e); }
        function stopDrawing() { isDrawing = false; ctx.beginPath(); }
        
        function draw(e) {
            if (!isDrawing) return;
            e.preventDefault(); // タッチ時のスクロールを防止
            
            const rect = canvas.getBoundingClientRect();
            // 見た目のサイズと内部サイズの比率を計算
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;
            
            let clientX = e.clientX; let clientY = e.clientY;
            if (e.touches && e.touches.length > 0) {
                clientX = e.touches[0].clientX; clientY = e.touches[0].clientY;
            }
            
            const x = (clientX - rect.left) * scaleX;
            const y = (clientY - rect.top) * scaleY;

            ctx.lineWidth = currentWeight;
            ctx.lineCap = "round";
            // 消しゴムの時は白で塗る
            ctx.strokeStyle = (currentTool === 'eraser') ? "white" : "black";

            ctx.lineTo(x, y);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(x, y);
        }

        canvas.addEventListener('mousedown', startDrawing);
        canvas.addEventListener('mousemove', draw);
        canvas.addEventListener('mouseup', stopDrawing);
        canvas.addEventListener('mouseout', stopDrawing);
        // スマホ・タブレット用タッチイベント
        canvas.addEventListener('touchstart', startDrawing, {passive: false});
        canvas.addEventListener('touchmove', draw, {passive: false});
        canvas.addEventListener('touchend', stopDrawing);
        canvas.addEventListener('touchcancel', stopDrawing);
        
        // --- 最大化・最小化処理 ---
        function toggleFullscreen() {
            const btn = document.getElementById('btn-maximize');
            
            // 現在の描画内容を保存
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = canvas.width;
            tempCanvas.height = canvas.height;
            tempCanvas.getContext('2d').drawImage(canvas, 0, 0);

            if (!isFullscreen) {
                // 最大化モードへ
                canvasWrapper.classList.add('fullscreen');
                btn.innerText = "縮小";
                btn.classList.add('btn-accent');
                
                // 画面サイズに合わせてキャンバスサイズを変更
                const rect = canvas.getBoundingClientRect();
                // CSSで flex:1 になっているので、その実際のピクセルサイズを取得して内部解像度に反映
                // 少し遅延させないとレイアウト確定前のサイズを取ってしまうことがある
                setTimeout(() => {
                    canvas.width = canvas.clientWidth;
                    canvas.height = canvas.clientHeight;
                    
                    // 保存した画像を拡大して描画（アスペクト比無視で全体に引き伸ばす）
                    // ユーザー体験として「描いたものが消えない」「位置がずれない」を優先
                    ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
                    
                    // ペン設定がリセットされるので戻す
                    ctx.lineWidth = currentWeight;
                    ctx.lineCap = "round";
                }, 50);
                
                isFullscreen = true;
            } else {
                // 通常モードへ戻る
                canvasWrapper.classList.remove('fullscreen');
                btn.innerText = "⛶ 最大化";
                btn.classList.remove('btn-accent');
                
                // 固定サイズに戻す
                canvas.width = 600;
                canvas.height = 300;
                
                // 画像を縮小して戻す
                ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
                ctx.lineWidth = currentWeight;
                ctx.lineCap = "round";
                
                isFullscreen = false;
            }
        }

        function sendAnswer() {
            // キャンバスに描いた絵をWebP画像として取得して送信 (画質0.8)
            const dataUrl = canvas.toDataURL('image/webp', 0.8);
            socket.emit('send_answer', {answer: dataUrl});
            
            const btn = document.querySelector('.btn-send');
            const originalText = btn.innerText;
            btn.innerText = "送信完了！";
            setTimeout(() => { btn.innerText = originalText; }, 1500);
            
            // もし最大化中なら戻す？いや、続けて書きたいかもしれないのでそのままにする
        }

        // --- 結果発表の受信処理 ---
        socket.on('result_reveal', function(data) {
            const result = data.result; // 'correct' or 'incorrect'
            const overlay = document.createElement('div');
            overlay.className = 'result-overlay ' + (result === 'correct' ? 'result-correct' : 'result-incorrect');
            overlay.innerText = result === 'correct' ? '🎉 正解！' : '❌ 不正解';
            document.body.appendChild(overlay);
            setTimeout(() => overlay.remove(), 3000);
        });

        // --- 回答リセットの受信処理 ---
        socket.on('clear_canvas', function() {
            clearCanvas();
        });

        function loadSharedFiles() {
            fetch('/api/shared_files')
                .then(r => r.json())
                .then(files => {
                    const list = document.getElementById('shared-list');
                    list.innerHTML = '';
                    if (files.length === 0) {
                        list.innerHTML = '<li style="color:#999;font-size:13px;padding:8px 0;">配布資料はありません</li>';
                    }
                    files.forEach(f => {
                        if (f.locked) {
                            list.innerHTML += `
                                <li class="file-item locked">
                                    <span>${f.name} <span class="badge badge-locked">準備中</span></span>
                                </li>`;
                        } else {
                            list.innerHTML += `
                                <li class="file-item">
                                    <span>${f.name}</span>
                                    <a href="/download/shared/${encodeURIComponent(f.name)}" target="_blank" class="dl-link">DL</a>
                                </li>`;
                        }
                    });
                });
        }

        function uploadFile() {
            const fileInput = document.getElementById('file-upload');
            if (fileInput.files.length === 0) {
                alert("ファイルを選択してください。");
                return;
            }
            const formData = new FormData();
            formData.append('file', fileInput.files[0]);
            // ログイン時に入力した名前を送信する
            formData.append('student_name', document.getElementById('username').value.trim());

            fetch('/api/upload', { method: 'POST', body: formData })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    alert("提出が完了しました！");
                    fileInput.value = ""; 
                } else {
                    alert("エラーが発生しました。");
                }
            })
            .catch(err => alert("通信エラー"));
        }
    </script>
</body>
</html>
"""

# ホスト用の管理画面
HOST_HTML = """
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>PC管理システム - ホスト</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.1/socket.io.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
    <style>
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
        body { background: #fff; color: #111; font-family: 'Segoe UI', 'Hiragino Sans', 'Meiryo', sans-serif; font-size: 15px; line-height: 1.6; }
        .wrap { max-width: 1200px; margin: 0 auto; padding: 24px 20px; }

        .top-bar { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16px; margin-bottom: 8px; border-bottom: 1px solid #e5e5e5; }
        .top-bar h1 { font-size: 18px; font-weight: 700; }
        .admin-tag { font-size: 11px; font-weight: 700; color: #E86C5A; background: #FEF2F0; padding: 4px 10px; border-radius: 3px; letter-spacing: 0.3px; }

        .info-bar { font-size: 13px; color: #555; background: #f8f8f8; padding: 10px 14px; border-radius: 4px; margin-bottom: 20px; border-left: 3px solid #0077B6; }
        .info-bar a { color: #0077B6; font-weight: 600; text-decoration: none; }
        .info-bar a:hover { text-decoration: underline; }
        .info-bar small { display: block; color: #999; margin-top: 2px; }

        /* Tabs */
        .tabs { display: flex; gap: 0; border-bottom: 2px solid #e5e5e5; margin-bottom: 24px; }
        .tab-btn { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #888; background: none; border: none; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all .15s; }
        .tab-btn:hover { color: #333; }
        .tab-btn.active { color: #0077B6; border-bottom-color: #0077B6; }
        .tab-panel { display: none; }
        .tab-panel.active { display: block; }

        /* Controls bar */
        .controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; padding: 12px 14px; background: #fafafa; border: 1px solid #eee; border-radius: 4px; }
        .btn { display: inline-block; padding: 8px 18px; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s; }
        .btn-accent { background: #0077B6; color: #fff; }
        .btn-accent:hover { background: #005f8f; }
        .btn-warn { background: #E86C5A; color: #fff; }
        .btn-warn:hover { background: #d05a49; }
        .btn-dark { background: #111; color: #fff; }
        .btn-dark:hover { background: #333; }
        .btn-outline { background: transparent; border: 1.5px solid #ddd; color: #333; }
        .btn-outline:hover { border-color: #0077B6; color: #0077B6; }
        .btn-sm { padding: 5px 12px; font-size: 12px; }
        select { padding: 7px 10px; border: 1.5px solid #ddd; border-radius: 4px; font-size: 13px; outline: none; }
        select:focus { border-color: #0077B6; }
        label.ctrl-label { font-size: 12px; font-weight: 600; color: #666; }

        /* Screenshot grid */
        .screen-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
        .screen-card { border: 1px solid #eee; border-radius: 4px; overflow: hidden; }
        .screen-card .name { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #eee; }
        .screen-card img { width: 100%; display: block; min-height: 120px; object-fit: contain; background: #f5f5f5; }

        /* Flip grid */
        .flip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; }
        .flip-card { 
            border: 1px solid #eee; border-radius: 4px; overflow: hidden; transition: all .2s; 
            cursor: pointer; position: relative;
        }
        .flip-card:hover { border-color: #bbb; }
        
        /* Selection visual - Default (Visible) */
        .flip-card.selected { 
            border-color: #FFC107; box-shadow: 0 0 0 4px rgba(255, 193, 7, 0.4); 
        }
        .flip-card.selected::after {
            content: "✓"; position: absolute; top: 8px; right: 8px;
            background: #FFC107; color: #000; width: 24px; height: 24px;
            border-radius: 50%; font-weight: bold; font-size: 14px;
            display: flex; align-items: center; justify-content: center;
        }

        /* Selection visual - Hidden (Stealth Mode) */
        .flip-card.selected-hidden {
            /* ほぼ見えないが、ホストがギリギリわかる程度の目印（左下の極小ドットなど） */
            border-color: #eee;
        }
        .flip-card.selected-hidden::after {
            content: ""; position: absolute; bottom: 2px; right: 2px;
            background: rgba(0,0,0,0.1); width: 4px; height: 4px; border-radius: 50%;
        }

        .flip-card .name { font-size: 12px; font-weight: 600; color: #666; padding: 6px 10px; border-bottom: 1px solid #eee; text-align: center; }
        .flip-card .answer-area { min-height: 120px; display: flex; align-items: center; justify-content: center; background: #fafafa; padding: 4px; }
        .flip-card .answer-area img { max-height: 120px; width: 100%; object-fit: contain; background: #fff; border-radius: 2px; }
        .flip-card .no-answer { color: #bbb; font-size: 13px; font-weight: 600; }
        
        .layer-correct { background: rgba(200, 50, 30, 0.15) !important; border-color: #C8321E !important; }
        .layer-incorrect { background: rgba(0, 90, 140, 0.15) !important; border-color: #005A8C !important; }

        /* File list */
        .file-list { list-style: none; padding: 0; }
        .file-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border: 1px solid #eee; border-radius: 3px; margin-bottom: 6px; font-size: 13px; }
        .file-item.locked { background: #FFFBEB; border-color: #F5E6B8; }
        .badge { font-size: 11px; padding: 2px 8px; border-radius: 3px; font-weight: 600; display: inline-block; margin-left: 8px; }
        .badge-pub { background: #E6F9EE; color: #157A3B; }
        .badge-lock { background: #FEF3CD; color: #92710C; }
        .dl-link { color: #0077B6; text-decoration: none; font-weight: 600; font-size: 13px; }
        .dl-link:hover { text-decoration: underline; }

        .section-head { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: #0077B6; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 2px solid #0077B6; display: flex; justify-content: space-between; align-items: center; }
        .col-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
        @media (max-width: 700px) { .col-grid { grid-template-columns: 1fr; } }
        .section { margin-bottom: 24px; }
        p.note { font-size: 12px; color: #999; margin-bottom: 10px; }
        
        /* QR Modal */
        #qr-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 3000; align-items: center; justify-content: center; }
        #qr-box { background: #fff; padding: 30px; border-radius: 12px; text-align: center; box-shadow: 0 10px 25px rgba(0,0,0,0.2); }
        #qrcode { margin: 20px auto; }
        .qr-title { font-weight: 700; margin-bottom: 8px; color: #333; }
        .qr-sub { font-size: 12px; color: #666; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="wrap">
        <div class="top-bar">
            <h1>PC管理システム — ホスト</h1>
            <div>
                <button class="btn btn-outline btn-sm" onclick="toggleQR()">📱 スマホ用QR</button>
                <span class="admin-tag" style="margin-left:8px;">ADMIN</span>
            </div>
        </div>

        <div class="info-bar">
            参加者URL: <a href="/client" target="_blank" id="client-url"></a>
            <small>※ 参加者にはこのURLを共有するか「生徒用ショートカット.html」を配布してください。</small>
        </div>

        <div class="tabs">
            <button class="tab-btn active" onclick="switchTab('flip', this)">フリップ解答</button>
            <button class="tab-btn" onclick="switchTab('files', this)">ファイル共有</button>
        </div>

        <!-- フリップ解答 -->
        <div class="tab-panel active" id="panel-flip">
            <div class="controls">
                <div style="margin-right:auto;display:flex;align-items:center;gap:10px;">
                    <span style="font-size:12px;font-weight:bold;">正解者を選択:</span>
                    <label style="font-size:12px;cursor:pointer;display:flex;align-items:center;">
                        <input type="checkbox" id="stealth-mode" onchange="updateSelectionVisuals()"> 選択状態を隠す
                    </label>
                </div>
                <button class="btn btn-warn" onclick="clearAnswers()">回答リセット</button>
                <button class="btn btn-dark" onclick="showResults()">結果発表 (選択した人を正解に)</button>
            </div>
            <div class="flip-grid" id="flips-container"></div>
        </div>

        <!-- ファイル共有 -->
        <div class="tab-panel" id="panel-files">
            <div class="col-grid">
                <div class="section">
                    <div class="section-head">
                        配布資料
                        <button class="btn btn-outline btn-sm" onclick="loadSharedFiles()">更新</button>
                    </div>
                    <p class="note">保存場所: <code>配布資料</code> フォルダ</p>
                    <ul id="shared-list" class="file-list"></ul>
                </div>
                <div class="section">
                    <div class="section-head">
                        提出物
                        <button class="btn btn-outline btn-sm" onclick="loadSubmittedFiles()">更新</button>
                    </div>
                    <p class="note">保存場所: <code>提出ボックス</code> フォルダ</p>
                    <ul id="submitted-list" class="file-list"></ul>
                </div>
            </div>
        </div>
    </div>

    <!-- QR Modal -->
    <div id="qr-modal" onclick="toggleQR()">
        <div id="qr-box" onclick="event.stopPropagation()">
            <div class="qr-title">ホスト画面（先生用）</div>
            <p class="qr-sub">スマホ・タブレットで読み取ってください</p>
            <div id="qrcode"></div>
            <!-- Jinja2埋め込みではなくJSで制御するように変更 -->
            <p id="qr-text-display" style="font-size:12px;color:#0077B6;word-break:break-all;margin-top:10px;"></p>
            <button class="btn btn-dark btn-sm" onclick="toggleQR()" style="margin-top:15px;width:100%;">閉じる</button>
        </div>
    </div>

    <script>
        const hostUrl = window.location.origin;
        document.getElementById('client-url').href = hostUrl + '/client';
        document.getElementById('client-url').innerText = hostUrl + '/client';

        // サーバーから渡された正確なLAN内URL
        let lanHostUrl = "{{ full_host_url }}"; 
        
        if (lanHostUrl && !lanHostUrl.startsWith('http')) {
             lanHostUrl = 'http://' + lanHostUrl;
        }

        const qrTextDisplay = document.getElementById('qr-text-display');
        if(qrTextDisplay) qrTextDisplay.innerText = lanHostUrl;
        
        let qrGenerated = false;
        function toggleQR() {
            const modal = document.getElementById('qr-modal');
            if (modal.style.display === 'flex') {
                modal.style.display = 'none';
            } else {
                modal.style.display = 'flex';
                if (!qrGenerated) {
                    new QRCode(document.getElementById("qrcode"), {
                        text: lanHostUrl,
                        width: 200,
                        height: 200
                    });
                    qrGenerated = true;
                }
            }
        }

        function switchTab(name, el) {
            document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
            document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
            document.getElementById('panel-' + name).classList.add('active');
            el.classList.add('active');
        }

        const socket = io();

        // ローカルで管理する「選択された生徒」のIDリスト
        let selectedSids = new Set();
        let lastClientsData = {};

        socket.on('update_clients', function(data) {
            lastClientsData = data;
            updateFlips(data);
        });

        // 判定結果受信（再接続や他ホスト同期用）
        socket.on('results_revealed_host', function(serverJudgments) {
            // サーバー側で確定した正解/不正解を反映
            for (const [sid, result] of Object.entries(serverJudgments)) {
                const card = document.getElementById('flip-card-' + sid);
                if (card) {
                    card.classList.remove('layer-correct', 'layer-incorrect', 'selected', 'selected-hidden');
                    if (result === 'correct') card.classList.add('layer-correct');
                    else if (result === 'incorrect') card.classList.add('layer-incorrect');
                }
                // 確定後は選択状態を解除（またはサーバー状態に合わせるならここで再セット）
                if (selectedSids.has(sid)) selectedSids.delete(sid);
            }
        });
        
        function toggleSelection(sid) {
            const card = document.getElementById('flip-card-' + sid);
            if (!card) return;

            // 結果が既に発表されている場合は選択操作を無効化（リセット推奨）
            if (card.classList.contains('layer-correct') || card.classList.contains('layer-incorrect')) {
                // 再選択したい場合はリセットを押してもらう運用
                return; 
            }

            if (selectedSids.has(sid)) {
                selectedSids.delete(sid);
            } else {
                selectedSids.add(sid);
            }
            updateSelectionVisuals();
        }

        function updateSelectionVisuals() {
            const isStealth = document.getElementById('stealth-mode').checked;
            
            for (const sid in lastClientsData) {
                const card = document.getElementById('flip-card-' + sid);
                if (card) {
                    // まず選択クラスを消す
                    card.classList.remove('selected', 'selected-hidden');
                    
                    // 選択されているならクラス付与
                    if (selectedSids.has(sid)) {
                        if (isStealth) {
                            card.classList.add('selected-hidden');
                        } else {
                            card.classList.add('selected');
                        }
                    }
                }
            }
        }

        function showResults() {
            // サーバーに現在の選択状態を送信し、一斉判定を要求
            const judgmentsMap = {};
            for (const sid in lastClientsData) {
                if (selectedSids.has(sid)) {
                    judgmentsMap[sid] = 'correct';
                } else {
                    judgmentsMap[sid] = 'incorrect';
                }
            }
            socket.emit('show_results', judgmentsMap);
            // 選択状態はクリアする（結果色で上書きされるため）
            selectedSids.clear();
        }
        
        function clearAnswers() { 
            socket.emit('clear_answers'); 
            selectedSids.clear();
        }

        socket.on('clear_judge_all', function() {
            selectedSids.clear();
            document.querySelectorAll('.flip-card').forEach(card => {
                card.classList.remove('layer-correct', 'layer-incorrect', 'selected', 'selected-hidden');
            });
        });

        function updateFlips(clients) {
            const container = document.getElementById('flips-container');
            // DOMを全再構築するとクリックイベントなどが切れるが、簡易実装として再構築する。
            // ただし選択状態(selectedSids)は維持する。
            
            container.innerHTML = '';
            for (const sid in clients) {
                const c = clients[sid];
                let answerContent = '<span class="no-answer">未回答</span>';
                if (c.answer) {
                    answerContent = `<img src="${c.answer}">`;
                }
                
                // カード生成
                const card = document.createElement('div');
                card.className = 'flip-card';
                card.id = 'flip-card-' + sid;
                card.onclick = function() { toggleSelection(sid); };
                
                card.innerHTML = `
                    <div class="name">${c.name}</div>
                    <div class="answer-area">${answerContent}</div>
                `;
                container.appendChild(card);
            }
            // 選択状態の復元
            updateSelectionVisuals();
        }

        function loadSubmittedFiles() {
            fetch('/api/submitted_files')
                .then(r => r.json())
                .then(files => {
                    const list = document.getElementById('submitted-list');
                    list.innerHTML = '';
                    if (files.length === 0) list.innerHTML = '<li style="color:#999;font-size:13px;padding:8px 0;">提出物はまだありません</li>';
                    files.forEach(f => {
                        list.innerHTML += `
                            <li class="file-item">
                                <span>${f}</span>
                                <a href="/download/submitted/${encodeURIComponent(f)}" target="_blank" class="dl-link">DL</a>
                            </li>`;
                    });
                });
        }
        
        function loadSharedFiles() {
            fetch('/api/shared_files')
                .then(r => r.json())
                .then(files => {
                    const list = document.getElementById('shared-list');
                    list.innerHTML = '';
                    if (files.length === 0) list.innerHTML = '<li style="color:#999;font-size:13px;padding:8px 0;">ファイルがありません</li>';
                    files.forEach(f => {
                        const btnText = f.locked ? '公開する' : '非公開にする';
                        const btnClass = f.locked ? 'btn-accent' : 'btn-outline';
                        const badge = f.locked ? '<span class="badge badge-lock">非公開</span>' : '<span class="badge badge-pub">公開中</span>';
                        
                        list.innerHTML += `
                            <li class="file-item ${f.locked ? 'locked' : ''}">
                                <span>${f.name} ${badge}</span>
                                <button class="btn ${btnClass} btn-sm" onclick="togglePermission('${f.name}')">${btnText}</button>
                            </li>`;
                    });
                });
        }

        function togglePermission(filename) {
            fetch('/api/toggle_permission', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ filename: filename })
            }).then(r => r.json()).then(data => { if (data.success) loadSharedFiles(); });
        }
        
        loadSubmittedFiles();
        loadSharedFiles();
    </script>
    
    <!-- QR Modal -->
    <div id="qr-modal" onclick="toggleQR()">
        <div id="qr-box" onclick="event.stopPropagation()">
            <div class="qr-title">ホスト画面（先生用）</div>
            <p class="qr-sub">スマホ・タブレットで読み取ってください</p>
            <div id="qrcode"></div>
            <p style="font-size:12px;color:#0077B6;word-break:break-all;">{{ full_host_url }}</p>
            <button class="btn btn-dark btn-sm" onclick="toggleQR()" style="margin-top:15px;width:100%;">閉じる</button>
        </div>
    </div>
</body>
</html>
"""

@app.route('/')
def index():
    # サーバーの実際のIPアドレスを使ったURLを生成して渡す
    ip = app.config.get('SERVER_IP', 'localhost')
    port = app.config.get('SERVER_PORT', 5000)
    full_host_url = f"http://{ip}:{port}"
    return render_template_string(HOST_HTML, full_host_url=full_host_url)

@app.route('/client')
def client_page():
    return render_template_string(CLIENT_HTML)

# --- WebSocket 処理 ---
@socketio.on('connect')
def handle_connect(): pass

@socketio.on('register')
def handle_register(data):
    sid = request.sid
    clients_data[sid] = {'name': data.get('name', '名無しPC'), 'answer': ''}
    emit('update_clients', clients_data, broadcast=True)

@socketio.on('disconnect')
def handle_disconnect():
    sid = request.sid
    if sid in clients_data:
        del clients_data[sid]
        emit('update_clients', clients_data, broadcast=True)

@socketio.on('send_answer')
def handle_answer(data):
    sid = request.sid
    if sid in clients_data:
        clients_data[sid]['answer'] = data['answer']
        emit('update_clients', clients_data, broadcast=True)

@socketio.on('set_judge')
def handle_set_judge(data):
    """ホストが個別に判定した結果をサーバーに保存し、他のホスト画面にも同期する"""
    # 以前の個別判定ロジック。今は一斉判定がメインだが、互換性のため残すか、使用しないなら削除可
    sid = data.get('sid')
    result = data.get('result') 
    if sid:
        judgments_data[sid] = result
        emit('judge_updated', {'sid': sid, 'result': result}, broadcast=True)

@socketio.on('show_results')
def handle_show_results(data):
    """ホストが結果発表→全クライアント＆全ホストに正解/不正解を通知"""
    global judgments_data
    # data はクライアントから送られてきた {sid: 'correct'/'incorrect'} のマップ
    # これをサーバーの正状態として保存
    if isinstance(data, dict):
        judgments_data = data
    
    # 1. 生徒への通知 (オーバーレイ表示)
    for sid, result in judgments_data.items():
        if sid in clients_data:
            emit('result_reveal', {'result': result}, room=sid)
            
    # 2. ホスト画面全体への通知（正解・不正解の色付けを確定）
    emit('results_revealed_host', judgments_data, broadcast=True)

@socketio.on('clear_answers')
def handle_clear_answers():
    global judgments_data
    judgments_data = {} # 判定データもリセット
    for sid in clients_data:
        clients_data[sid]['answer'] = ''
    emit('update_clients', clients_data, broadcast=True)
    emit('clear_judge_all', broadcast=True) # クライアント側の判定表示もクリア
    emit('clear_canvas', broadcast=True)

# --- ファイル共有API (Class Post仕様を統合) ---
@app.route('/api/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part'}), 400
    
    file = request.files['file']
    student_name = request.form.get('student_name', '名無し')
    
    if file.filename == '' or file.filename is None:
        return jsonify({'error': 'No selected file'}), 400
    
    # 元のファイル名を取得（安全化）
    original_filename = os.path.basename(file.filename)
    
    # ファイル名が空や不正な場合のフォールバック
    if not original_filename or original_filename.strip() == '':
        import mimetypes
        from datetime import datetime
        ext = mimetypes.guess_extension(file.content_type or '') or ''
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        original_filename = f"uploaded_{timestamp}{ext}"
    
    # 名付け規則「名前_ファイル名」を適用
    save_name = f"{student_name}_{original_filename}"
    file.save(os.path.join(SUBMIT_DIR, save_name))
    
    return jsonify({'success': True, 'filename': save_name})

@app.route('/api/submitted_files', methods=['GET'])
def list_submitted():
    files = [f for f in sorted(os.listdir(SUBMIT_DIR)) if not f.startswith('.')]
    return jsonify(files)




# --- stderr退避 (Windows日本語クラッシュ防止) ---
try:
    sys.stderr = open(LOG_FILE, 'a', encoding='utf-8')
except Exception:
    pass

def safe_print(msg):
    """コンソールに安全に表示する。"""
    try:
        safe_msg = str(msg).encode('ascii', errors='replace').decode('ascii')
        sys.stdout.write(safe_msg + '\n')
        sys.stdout.flush()
    except Exception:
        pass

if __name__ == '__main__':
    DEFAULT_PORT = 5000
    ip = get_ip_address()
    port = DEFAULT_PORT

    # Flaskアプリに設定を保存（ルートハンドラで参照するため）
    app.config['SERVER_IP'] = ip

    # ポート自動インクリメント
    while port < DEFAULT_PORT + 100:
        try:
            # テストバインド
            test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            test_sock.bind(('0.0.0.0', port))
            test_sock.close()
            break
        except OSError:
            port += 1
            
    app.config['SERVER_PORT'] = port

    # 起動時に生徒用のショートカットHTMLを生成
    update_student_shortcut(ip, port)

    safe_print("=" * 60)
    safe_print("  PC Management System - Started!")
    safe_print("")
    safe_print(f"  Port: {port}")
    safe_print(f"  Host: http://localhost:{port}")
    safe_print(f"  Client: http://{ip}:{port}/client")
    safe_print("")
    safe_print("  (Close this window to stop)")
    safe_print("=" * 60)

    # サーバー起動後にブラウザで管理画面を自動で開く
    def open_browser():
        try:
            webbrowser.open(f'http://localhost:{port}')
        except Exception:
            pass
    threading.Timer(1.5, open_browser).start()

    socketio.run(app, host='0.0.0.0', port=port)