#!/usr/bin/env python3
"""
Five-Star Deathmatch: Flask app packaged for installation.

Usage (after poetry install):
    poetry run deathmatch [IMAGE_DIR]

If IMAGE_DIR is omitted, the current working directory is used.
"""
from __future__ import annotations

import argparse
import logging
import os
import sys
import threading
from pathlib import Path
from typing import Any, Dict, List

from flask import Flask, abort, jsonify, render_template_string, request, send_file

# Configuration constants
SUPPORTED_EXTENSIONS = (".png", ".jpg", ".jpeg")
RANK_RANGE = (-1, 5)  # -1 for rejected, 0-5 for star ratings
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 5000

# Logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

# Globals initialized in create_app
BASE_DIR: Path
RANK_FILE: Path
state_lock: threading.Lock
files: List[str]
meta: Dict[str, Dict[str, Any]]

INDEX_HTML = r"""
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Five-Star Deathmatch</title>
<style>
body { 
    margin: 0; 
    font-family: sans-serif; 
    display: flex; 
    flex-direction: column; 
    height: 100vh; 
}

.topbar, .bottombar { 
    background: #eee; 
    padding: 4px; 
    display: flex; 
    justify-content: space-between; 
    font-size: 20px; 
}

.central { 
    position: relative; 
    flex: 1; 
    background: #999; 
    display: flex; 
    justify-content: center; 
    align-items: center; 
    overflow: hidden; 
}

.central img { 
    max-width: 100%; 
    max-height: 100%; 
    object-fit: contain; 
}

.central.zoom { 
    overflow: auto; 
    justify-content: flex-start; 
    align-items: flex-start; 
}

.central.zoom img { 
    max-width: none; 
    max-height: none; 
    width: auto; 
    height: auto; 
    display: block; 
}

#help { 
    position: absolute; 
    top: 10px; 
    right: 10px; 
    cursor: pointer; 
    border: 1px solid #666; 
    border-radius: 8px; 
    margin: 2px; 
    padding: 6px; 
    background: #f9f9f9; 
    text-align: center; 
}

#helpPanel { 
    display: none; 
    position: absolute; 
    top: 40px; 
    right: 10px; 
    background: #fff; 
    color: #000; 
    font-size: 16px; 
    padding: 10px 15px; 
    border-radius: 10px; 
    box-shadow: 0 2px 8px rgba(0,0,0,0.3); 
}

#helpPanel ul { 
    margin: 0; 
    padding-left: 20px; 
}

#helpPanel li { 
    margin: 4px 0; 
}

.bottombar { 
    flex-direction: column; 
    font-size: 14px; 
}

.row { 
    display: flex; 
    flex: 1; 
}

.cell { 
    flex: 1; 
    border: 1px solid #666; 
    border-radius: 8px; 
    margin: 2px; 
    text-align: center; 
    padding: 6px; 
    cursor: pointer; 
    display: flex; 
    flex-direction: column; 
    justify-content: center; 
    align-items: center; 
    background: #f9f9f9; 
}

.cell.active { 
    background: #ccc; 
}
</style>
</head>
<body>
<div class="topbar">
    <div id="filename"></div>
    <div id="rankflags"></div>
    <div id="pos"></div>
</div>

<div class="central" id="central">
    <img id="mainimg"/>
    <div id="help">❓</div>
    <div id="helpPanel"></div>
</div>

<div class="bottombar">
    <div class="row" id="rankrow"></div>
    <div class="row" id="flagrow"></div>
</div>

<script>
/**
 * Five-Star Deathmatch Frontend JavaScript
 * 
 * Handles the interactive image ranking interface including:
 * - Navigation between images
 * - Ranking and flagging functionality
 * - Filtering and display controls
 * - Keyboard shortcuts
 */

// Application state
let state = { files: [], meta: {} };
let index = 0;
let filters = { ranks: new Set(), flags: new Set() };
let zoom = false;

// DOM elements
const mainimg = document.getElementById("mainimg");
const filenameDiv = document.getElementById("filename");
const rankflagsDiv = document.getElementById("rankflags");
const posDiv = document.getElementById("pos");
const central = document.getElementById("central");
const help = document.getElementById("help");
const helpPanel = document.getElementById("helpPanel");

/**
 * Fetch the current state from the server
 */
function fetchState() {
    fetch("/api/state")
        .then(r => r.json())
        .then(js => {
            state = js;
            render();
            buildBottom();
        });
}

/**
 * Build the bottom control panel with rank and flag filters
 */
function buildBottom() {
    buildRankRow();
    buildFlagRow();
}

/**
 * Build the rank filter row
 */
function buildRankRow() {
    const rr = document.getElementById("rankrow");
    rr.innerHTML = "";
    
    const ranks = [-1, 0, 1, 2, 3, 4, 5];
    let counts = {};
    for (let r of ranks) counts[r] = 0;
    
    let total = 0;
    for (let f of state.files) {
        let m = state.meta[f] || { rank: 0, flags: [] };
        if (m.rank > -1) total++;
        counts[m.rank]++;
    }
    
    for (let r of ranks) {
        let icon = (r == -1 ? "❌" : (r == 0 ? "⚪️" : "⭐️".repeat(r)));
        let pct = (r >= 0 && total > 0) ? Math.round(100 * counts[r] / total) + "%" : "";
        
        const d = document.createElement("div");
        d.className = "cell";
        if (filters.ranks.has(r)) d.classList.add("active");
        
        d.innerHTML = "<div>" + icon + "</div><div>" + counts[r] + (pct ? " (" + pct + ")" : "") + "</div>";
        d.onclick = () => {
            if (filters.ranks.has(r)) {
                filters.ranks.delete(r);
            } else {
                filters.ranks.add(r);
            }
            render();
            buildBottom();
        };
        rr.appendChild(d);
    }
}

/**
 * Build the flag filter row
 */
function buildFlagRow() {
    const fr = document.getElementById("flagrow");
    fr.innerHTML = "";
    
    const flags = ["A", "B", "C", "D", "E", "F"];
    let flagCounts = {};
    for (let fl of flags) flagCounts[fl] = 0;
    
    let unflagged = 0;
    for (let f of state.files) {
        let m = state.meta[f] || { rank: 0, flags: [] };
        if (m.flags.length == 0) unflagged++;
        for (let fl of m.flags) flagCounts[fl]++;
    }
    
    // Add flag buttons
    for (let fl of flags) {
        const d = document.createElement("div");
        d.className = "cell";
        if (filters.flags.has(fl)) d.classList.add("active");
        
        d.innerHTML = "<div>" + fl + "</div><div>" + flagCounts[fl] + "</div>";
        d.onclick = () => {
            if (filters.flags.has(fl)) {
                filters.flags.delete(fl);
            } else {
                filters.flags.add(fl);
            }
            render();
            buildBottom();
        };
        fr.appendChild(d);
    }
    
    // Add unflagged button
    const d = document.createElement("div");
    d.className = "cell";
    if (filters.flags.has("UNFLAG")) d.classList.add("active");
    
    d.innerHTML = "<div>🚫</div><div>" + unflagged + "</div>";
    d.onclick = () => {
        if (filters.flags.has("UNFLAG")) {
            filters.flags.delete("UNFLAG");
        } else {
            filters.flags.add("UNFLAG");
        }
        render();
        buildBottom();
    };
    fr.appendChild(d);
}

/**
 * Get filtered list of files based on current filters
 */
function filteredFiles() {
    return state.files.filter(f => {
        let m = state.meta[f] || { rank: 0, flags: [] };
        
        // Check rank filters
        if (filters.ranks.has(m.rank)) return false;
        
        // Check flag filters
        for (let fl of m.flags) {
            if (filters.flags.has(fl)) return false;
        }
        
        // Check unflagged filter
        if (m.flags.length == 0 && filters.flags.has("UNFLAG")) return false;
        
        return true;
    });
}

/**
 * Copy the current filtered list of filenames to the clipboard, LF-delimited.
 */
function copyListToClipboard() {
    const list = filteredFiles();
    const text = list.join("\n") + "\n";
    if (!text) {
        return; // nothing to copy
    }
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).catch(() => fallbackClipboardCopy(text));
    } else {
        fallbackClipboardCopy(text);
    }
}

/**
 * Fallback clipboard copy using a temporary textarea element.
 */
function fallbackClipboardCopy(text) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.setAttribute('readonly', '');
    ta.style.position = 'absolute';
    ta.style.left = '-9999px';
    document.body.appendChild(ta);
    ta.select();
    try { document.execCommand('copy'); } catch (e) { /* ignore */ }
    document.body.removeChild(ta);
}

/**
 * Render the current image and update display
 */
function render() {
    let list = filteredFiles();
    
    if (list.length == 0) {
        mainimg.src = "";
        filenameDiv.textContent = "";
        rankflagsDiv.textContent = "";
        posDiv.textContent = "0/0";
        return;
    }
    
    if (index >= list.length) index = 0;
    
    let fname = list[index];
    mainimg.src = "/image/" + encodeURIComponent(fname);
    filenameDiv.textContent = fname;
    
    let m = state.meta[fname] || { rank: 0, flags: [] };
    let rankDisp = (m.rank == -1 ? "❌" : (m.rank == 0 ? "⚪️" : "⭐️".repeat(m.rank)));
    rankflagsDiv.textContent = rankDisp + " " + m.flags.sort().join("");
    posDiv.textContent = (index + 1) + " / " + list.length;
}

/**
 * Find the next valid index after a file update
 */
function nextValidIndex(oldfname) {
    let list = filteredFiles();
    let i = list.indexOf(oldfname);
    
    if (i == -1) {
        if (index >= list.length) index = list.length - 1;
    } else {
        index = i + 1;
    }
}

/**
 * Update the rank of the current image
 */
function updateRank(r) {
    let fname = filteredFiles()[index];
    
    fetch("/api/update", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ file: fname, rank: r })
    })
    .then(() => fetch("/api/state"))
    .then(r => r.json())
    .then(js => {
        state = js;
        nextValidIndex(fname);
        render();
        buildBottom();
    });
}

/**
 * Toggle a flag on the current image
 */
function toggleFlag(fl) {
    let fname = filteredFiles()[index];
    
    fetch("/api/update", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ file: fname, toggle_flag: fl })
    })
    .then(() => fetch("/api/state"))
    .then(r => r.json())
    .then(js => {
        state = js;
        render();
        buildBottom();
    });
}

/**
 * Handle keyboard shortcuts
 */
document.addEventListener("keydown", ev => {
    let list = filteredFiles();
    if (list.length == 0) return;
    
    switch (ev.key) {
        case "ArrowLeft":
            index = (index - 1 + list.length) % list.length;
            render();
            break;
            
        case "ArrowRight":
            index = (index + 1) % list.length;
            render();
            break;
            
        case "ArrowUp":
            index = 0;
            render();
            break;
            
        case "ArrowDown":
            index = list.length - 1;
            render();
            break;
            
        case "x":
        case "X":
            updateRank(-1);
            break;
            
        case "0":
        case "1":
        case "2":
        case "3":
        case "4":
        case "5":
            updateRank(parseInt(ev.key));
            break;
            
        case "r":
        case "R":
            filters = { ranks: new Set(), flags: new Set() };
            render();
            buildBottom();
            break;
            
        case "z":
        case "Z":
            zoom = !zoom;
            if (zoom) {
                central.classList.add("zoom");
                central.scrollTop = 0;
                central.scrollLeft = 0;
            } else {
                central.classList.remove("zoom");
            }
            break;
            
        case " ":
            fetch("/api/reload", { method: "POST" })
                .then(() => fetchState());
            break;
            
        case "l":
        case "L":
            copyListToClipboard();
            break;

        default:
            if ("ABCDEF".includes(ev.key.toUpperCase())) {
                toggleFlag(ev.key.toUpperCase());
            }
            break;
    }
});

/**
 * Show help panel
 */
help.onclick = () => {
    helpPanel.style.display = "block";
    helpPanel.innerHTML = "<ul>" +
        "<li>← / → : Previous / Next image</li>" +
        "<li>↑ / ↓ : First / Last image</li>" +
        "<li>0–5 : Set rank</li>" +
        "<li>X : Rank -1 (❌)</li>" +
        "<li>A–F : Toggle flags</li>" +
        "<li>R : Reset filters</li>" +
        "<li>Z : Toggle zoom</li>" +
        "<li>L : Copy filtered list to clipboard</li>" +
        "<li>Space : Reload state</li>" +
        "</ul>";
};

/**
 * Hide help panel
 */
helpPanel.onclick = () => {
    helpPanel.style.display = "none";
};

// Initialize the application
fetchState();

</script>
</body>
</html>
"""


def create_app(base_dir: Path) -> Flask:
    """Application factory. Sets up Flask with proper folders and routes."""
    global BASE_DIR, RANK_FILE, state_lock, files, meta

    BASE_DIR = base_dir.resolve()
    RANK_FILE = BASE_DIR / "_rank.txt"
    state_lock = threading.Lock()
    files = []
    meta = {}

    here = Path(__file__).parent.resolve()
    app = Flask(
        __name__,
        template_folder=str(here / "templates"),
        static_folder=str(here / "static"),
    )

    # ---------------- State management ----------------
    def load_state() -> None:
        """Load files and metadata from disk and rank file."""
        global files, meta
        files = sorted(
            [f.name for f in BASE_DIR.iterdir() if f.suffix.lower() in SUPPORTED_EXTENSIONS],
            key=lambda fn: (BASE_DIR / fn).stat().st_mtime,
        )
        meta = {}
        if RANK_FILE.exists():
            try:
                for line in RANK_FILE.read_text().splitlines():
                    parts = line.split("\t")
                    if not parts:
                        continue
                    fname = parts[0]
                    try:
                        rank = int(parts[1]) if len(parts) > 1 and parts[1] else 0
                    except ValueError:
                        logger.warning("Invalid rank for %s; defaulting to 0", fname)
                        rank = 0
                    flags = set(parts[2].split(",")) if len(parts) > 2 and parts[2] else set()
                    meta[fname] = {"rank": rank, "flags": flags}
            except Exception as e:
                logger.error("Error reading rank file: %s", e)
                meta = {}
        for f in files:
            if f not in meta:
                meta[f] = {"rank": 0, "flags": set()}

    def save_state() -> None:
        try:
            with open(RANK_FILE, "w") as fp:
                for fname in files:
                    entry = meta.get(fname, {"rank": 0, "flags": set()})
                    flags_str = ",".join(sorted(entry["flags"])) if entry["flags"] else ""
                    fp.write(f"{fname}\t{entry['rank']}\t{flags_str}\n")
        except Exception as e:
            logger.error("Error saving rank file: %s", e)
            raise

    # ---------------- Routes ----------------
    @app.route("/")
    def index():
        return render_template_string(INDEX_HTML)

    @app.route("/image/<path:fname>")
    def get_image(fname: str):
        if not fname:
            abort(404)
        target = (BASE_DIR / fname).resolve()
        try:
            _ = target.relative_to(BASE_DIR)
        except ValueError:
            abort(403)
        if not target.exists():
            abort(404)
        return send_file(str(target))

    @app.route("/api/state")
    def api_state():
        with state_lock:
            safe_meta = {
                fn: {"rank": e.get("rank", 0), "flags": sorted(e.get("flags", []))}
                for fn, e in meta.items()
            }
            return jsonify({"files": files, "meta": safe_meta})

    @app.route("/api/update", methods=["POST"])
    def api_update():
        data = request.json or {}
        fname = data.get("file")
        if fname not in meta:
            abort(400)
        with state_lock:
            if "rank" in data:
                meta[fname]["rank"] = data["rank"]
            if "toggle_flag" in data:
                fl = data["toggle_flag"]
                if fl in meta[fname]["flags"]:
                    meta[fname]["flags"].remove(fl)
                else:
                    meta[fname]["flags"].add(fl)
            save_state()
        return jsonify(success=True)

    @app.route("/api/reload", methods=["POST"])
    def api_reload():
        with state_lock:
            load_state()
        return jsonify(success=True)

    # Initial load
    load_state()

    return app


def main(argv: List[str] | None = None) -> None:
    """CLI entry point for the deathmatch app."""
    argv = list(argv) if argv is not None else sys.argv[1:]

    parser = argparse.ArgumentParser(description="Five-Star Deathmatch Flask app")
    parser.add_argument(
        "directory",
        nargs="?",
        default=os.getcwd(),
        help="Image directory to manage (default: current working directory)",
    )
    parser.add_argument("--host", default=DEFAULT_HOST, help="Host to bind (default: 127.0.0.1)")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port (default: 5000)")
    parser.add_argument("--debug", action="store_true", help="Enable Flask debug mode")

    args = parser.parse_args(argv)

    base_dir = Path(args.directory).resolve()
    if not base_dir.exists() or not base_dir.is_dir():
        print(f"Error: directory not found: {base_dir}", file=sys.stderr)
        sys.exit(2)

    logger.info("Starting Five-Star Deathmatch")
    logger.info("Base directory: %s", base_dir)

    app = create_app(base_dir)
    app.run(args.host, args.port, debug=args.debug)


if __name__ == "__main__":  # pragma: no cover
    main()
