use axum::{
    routing::{get, post},
    Router,
    response::{Html, Redirect},
    extract::{Form, State, Path, Query},
    http::StatusCode,
};
use sqlx::SqlitePool;
use pickleball_elo::db;
use pickleball_elo::elo::{EloRating, EloCalculator, calculate_weighted_score, calculate_effective_opponent_rating};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

const DB_PATH: &str = "/Users/split/Projects/pickleball-elo/pickleball.db";

#[derive(Clone)]
struct AppState {
    pool: SqlitePool,
}

#[derive(Deserialize)]
struct NewPlayer {
    name: String,
    email: Option<String>,
}

#[derive(Deserialize)]
struct EditPlayer {
    name: String,
    #[serde(default, deserialize_with = "empty_string_as_none_string")]
    email: Option<String>,
    singles_rating: f64,
    doubles_rating: f64,
}

fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: Option<String> = Option::deserialize(deserializer)?;
    match s {
        None => Ok(None),
        Some(s) if s.is_empty() => Ok(None),
        Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
    }
}

fn empty_string_as_none_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: Option<String> = Option::deserialize(deserializer)?;
    match s {
        None => Ok(None),
        Some(s) if s.trim().is_empty() => Ok(None),
        Some(s) => Ok(Some(s)),
    }
}

#[derive(Deserialize)]
struct NewMatch {
    match_type: String,
    team1_player1: i64,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    team1_player2: Option<i64>,
    team2_player1: i64,
    #[serde(default, deserialize_with = "empty_string_as_none")]
    team2_player2: Option<i64>,
    team1_score: i32,
    team2_score: i32,
}

#[derive(Deserialize)]
struct BalanceQuery {
    p1: Option<i64>,
    p2: Option<i64>,
    p3: Option<i64>,
    p4: Option<i64>,
}

#[derive(Serialize)]
struct PlayerJson {
    id: i64,
    name: String,
    singles_rating: f64,
    doubles_rating: f64,
}

// Common CSS used across pages - Pitt colors (Blue #003594, Gold #FFB81C)
const COMMON_CSS: &str = r#"
    body { 
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; 
        background: linear-gradient(135deg, #003594 0%, #001a4d 100%);
        padding: 20px;
        margin: 0;
        min-height: 100vh;
    }
    .container { 
        max-width: 1000px; 
        margin: 0 auto; 
        background: white; 
        padding: 30px; 
        border-radius: 12px; 
        box-shadow: 0 20px 60px rgba(0,0,0,0.3);
    }
    h1 { color: #003594; text-align: center; }
    h2 { color: #003594; border-bottom: 3px solid #FFB81C; padding-bottom: 10px; }
    a { color: #003594; text-decoration: none; }
    a:hover { text-decoration: underline; color: #FFB81C; }
    .btn {
        display: inline-block;
        padding: 10px 20px;
        background: #003594;
        color: white !important;
        text-decoration: none !important;
        border-radius: 6px;
        font-weight: bold;
        border: none;
        cursor: pointer;
        transition: background 0.3s;
    }
    .btn:hover { background: #FFB81C; color: #003594 !important; }
    .btn-success { background: #28a745; }
    .btn-success:hover { background: #218838; }
    .btn-danger { background: #dc3545; }
    .btn-danger:hover { background: #c82333; }
    .btn-sm { padding: 5px 10px; font-size: 12px; }
    .back-link {
        display: block;
        text-align: center;
        margin-top: 20px;
    }
    .nav {
        display: flex;
        justify-content: center;
        gap: 10px;
        margin-bottom: 30px;
        flex-wrap: wrap;
    }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #003594; color: white; }
    tr:hover { background: #f9f9f9; }
    .badge {
        display: inline-block;
        padding: 3px 8px;
        border-radius: 12px;
        font-size: 11px;
        font-weight: bold;
    }
    .badge-win { background: #d4edda; color: #155724; }
    .badge-loss { background: #f8d7da; color: #721c24; }
    .stats-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        gap: 15px;
        margin: 20px 0;
    }
    .stat-card {
        background: #f0f5ff;
        padding: 20px;
        border-radius: 8px;
        text-align: center;
    }
    .stat-value { font-size: 32px; font-weight: bold; color: #003594; }
    .stat-label { font-size: 14px; color: #666; margin-top: 5px; }
    .achievement {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 12px;
        background: #fff3cd;
        border-radius: 20px;
        margin: 4px;
        font-size: 13px;
    }
    .achievement.locked { background: #e9ecef; opacity: 0.6; }
    .rating-up { color: #28a745; }
    .rating-down { color: #dc3545; }
"#;

#[tokio::main]
async fn main() {
    println!("🏓 Pickleball ELO Tracker v3.0");
    println!("==============================\n");
    
    run_server().await;
}

async fn run_server() {
    println!("Starting Pickleball ELO Tracker Server on port 3000...\n");
    
    let pool = db::create_pool(DB_PATH)
        .await
        .expect("Failed to create database pool");
    
    let state = AppState { pool };
    
    let app = Router::new()
        .route("/", get(index_handler))
        .route("/leaderboard", get(leaderboard_handler))
        .route("/players", get(players_list_handler))
        .route("/players/new", get(new_player_form).post(create_player))
        .route("/players/:id", get(player_profile_handler))
        .route("/players/:id/edit", get(edit_player_form).post(update_player))
        .route("/matches", get(match_history_handler))
        .route("/matches/new", get(new_match_form).post(create_match))
        .route("/matches/:id/delete", post(delete_match))
        .route("/balance", get(team_balancer_handler))
        .route("/sessions", get(sessions_list_handler))
        .route("/sessions/:id/preview", get(session_preview_handler))
        .route("/sessions/:id/send", post(send_session_email))
        .route("/daily", get(daily_summary_handler))
        .route("/daily/public", get(daily_public_handler))
        .route("/daily/send", post(send_daily_summary))
        .route("/api/leaderboard", get(api_leaderboard_handler))
        .route("/api/players", get(api_players_handler))
        .route("/about", get(about_handler))
        .with_state(state);
    
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    
    println!("✅ Server running at http://localhost:3000");
    println!("📊 Leaderboard: http://localhost:3000/leaderboard");
    println!("📜 Match History: http://localhost:3000/matches");
    println!("👥 Players: http://localhost:3000/players");
    println!("⚖️  Team Balancer: http://localhost:3000/balance");
    println!("➕ Add Player: http://localhost:3000/players/new");
    println!("🎾 Record Match: http://localhost:3000/matches/new\n");
    
    axum::serve(listener, app).await.unwrap();
}

fn nav_html() -> &'static str {
    r#"
    <div class="nav">
        <a href="/" class="btn">🏠 Home</a>
        <a href="/leaderboard" class="btn">📊 Leaderboard</a>
        <a href="/matches" class="btn">📜 History</a>
        <a href="/players" class="btn">👥 Players</a>
        <a href="/balance" class="btn">⚖️ Balance</a>
        <a href="/daily" class="btn">📧 Daily</a>
        <a href="/about" class="btn">❓ About</a>
        <a href="/matches/new" class="btn btn-success">🎾 Record</a>
    </div>
    "#
}

/// Serves the home page dashboard
///
/// **Endpoint:** `GET /`
///
/// **Description:** Displays the main dashboard with statistics about total matches, players, and sessions.
///
/// **Parameters:** None
///
/// **Returns:** HTML page with dashboard stats and navigation menu
async fn index_handler(State(state): State<AppState>) -> Html<String> {
    let player_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM players")
        .fetch_one(&state.pool).await.unwrap_or(0);
    let match_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM matches")
        .fetch_one(&state.pool).await.unwrap_or(0);
    let session_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM sessions")
        .fetch_one(&state.pool).await.unwrap_or(0);
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Pickleball ELO Tracker</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container" style="max-width: 700px; text-align: center;">
            <h1>🏓 Pickleball ELO Tracker</h1>
            <p style="color: #666;">Pure ELO Rating System v3.0</p>
            
            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Matches</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Players</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Sessions</div>
                </div>
            </div>
            
            {}
            
            <div style="margin-top: 40px; text-align: left; background: #f9f9f9; padding: 25px; border-radius: 12px;">
                <h2 style="color: #003594; margin-top: 0;">📊 How Ratings Work</h2>
                
                <p><strong>One unified rating</strong> — Singles and doubles matches both contribute to a single ELO rating. Everyone starts at 1500.</p>
                
                <p><strong>Per-point scoring</strong> — Your rating change depends on your actual point performance (points won ÷ total points), not just whether you won or lost. Winning 11-2 earns more than winning 11-9.</p>
                
                <p><strong>Smart doubles scoring</strong> — In doubles, we calculate your "effective opponent" using:<br>
                <code style="background: #e9e9e9; padding: 2px 6px; border-radius: 4px;">Effective Opponent = Opp1 + Opp2 - Teammate</code></p>
                
                <p>This means:</p>
                <ul style="margin-left: 20px;">
                    <li><strong>Strong teammate</strong> → lower effective opponent → less credit for winning</li>
                    <li><strong>Weak teammate</strong> → higher effective opponent → more credit for winning</li>
                </ul>
                
                <p><strong>The formula:</strong><br>
                <code style="background: #e9e9e9; padding: 4px 8px; border-radius: 4px; display: inline-block; margin-top: 5px;">
                Rating Change = 32 × (Actual Performance - Expected Performance)
                </code></p>
                
                <p style="color: #666; font-size: 13px; margin-bottom: 0;">Fair, transparent, and no mysterious "volatility" numbers. Just skill vs. expectations.</p>
            </div>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, match_count, player_count, session_count, nav_html());
    
    Html(html)
}

/// Displays the match history page with recent matches and results
///
/// **Endpoint:** `GET /matches`
///
/// **Description:** Shows the 50 most recent matches with participant names, scores, team results, rating changes, and ability to delete/undo matches.
///
/// **Parameters:** None
///
/// **Returns:** HTML page with match history table and delete options
async fn match_history_handler(State(state): State<AppState>) -> Html<String> {
    // Get recent matches with participants
    let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score, m.timestamp 
           FROM matches m ORDER BY m.timestamp DESC LIMIT 50"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut match_rows = String::new();
    
    for (match_id, match_type, t1_score, t2_score, timestamp) in &matches {
        // Get participants for this match
        let participants: Vec<(String, i32, f64)> = sqlx::query_as(
            r#"SELECT p.name, mp.team, mp.rating_change 
               FROM match_participants mp 
               JOIN players p ON mp.player_id = p.id 
               WHERE mp.match_id = ?"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect();
        
        // Format player names with individual rating changes
        let format_player = |name: &str, change: f64| {
            let change_class = if change >= 0.0 { "rating-up" } else { "rating-down" };
            let change_sign = if change >= 0.0 { "+" } else { "" };
            format!("{} <span class='{}' style='font-size:0.85em;'>({}{})</span>", 
                name, change_class, change_sign, change as i32)
        };
        
        let team1_display: String = team1.iter()
            .map(|(n, _, c)| format_player(n, *c))
            .collect::<Vec<_>>()
            .join(" &amp; ");
        let team2_display: String = team2.iter()
            .map(|(n, _, c)| format_player(n, *c))
            .collect::<Vec<_>>()
            .join(" &amp; ");
        
        let winner_badge = if t1_score > t2_score { 
            ("<span class='badge badge-win'>W</span>", "<span class='badge badge-loss'>L</span>")
        } else { 
            ("<span class='badge badge-loss'>L</span>", "<span class='badge badge-win'>W</span>")
        };
        
        let type_emoji = if match_type == "doubles" { "👥" } else { "🎾" };
        
        match_rows.push_str(&format!(r#"
            <tr>
                <td>{} {}</td>
                <td>{} {}</td>
                <td style="text-align:center; font-weight:bold;">{} - {}</td>
                <td>{} {}</td>
                <td>{}</td>
                <td>
                    <form method="POST" action="/matches/{}/delete" style="display:inline;" 
                          onsubmit="return confirm('Delete this match and revert ratings?')">
                        <button type="submit" class="btn btn-danger btn-sm">🗑️</button>
                    </form>
                </td>
            </tr>
        "#, type_emoji, match_type, winner_badge.0, team1_display,
            t1_score, t2_score, 
            winner_badge.1, team2_display,
            &timestamp[..16], match_id));
    }
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Match History - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container">
            <h1>📜 Match History</h1>
            {}
            
            <table>
                <thead>
                    <tr>
                        <th>Type</th>
                        <th>Team 1</th>
                        <th>Score</th>
                        <th>Team 2</th>
                        <th>Date</th>
                        <th>Undo</th>
                    </tr>
                </thead>
                <tbody>
                    {}
                </tbody>
            </table>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), match_rows);
    
    Html(html)
}

/// Displays a player's detailed profile with ratings, stats, achievements, and match history
///
/// **Endpoint:** `GET /players/:id`
///
/// **Description:** Shows complete player profile including singles/doubles ratings, win/loss record, achievements, rating chart, head-to-head stats, partner synergy, and recent matches.
///
/// **Parameters:** `id` (path): Player ID
///
/// **Returns:** HTML page with player profile data, or 404 if player not found
async fn player_profile_handler(
    State(state): State<AppState>,
    Path(player_id): Path<i64>,
) -> Result<Html<String>, (StatusCode, String)> {
    // Get player info
    let player: Option<(i64, String, Option<String>, f64, f64, f64, f64)> = sqlx::query_as(
        "SELECT id, name, email, singles_rating, singles_rd, doubles_rating, doubles_rd FROM players WHERE id = ?"
    )
    .bind(player_id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    let (_id, name, email, singles_rating, _singles_rd, _doubles_rating, _doubles_rd) = player
        .ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?;
    
    // Get match stats
    let total_matches: i64 = sqlx::query_scalar(
        "SELECT COUNT(DISTINCT match_id) FROM match_participants WHERE player_id = ?"
    )
    .bind(player_id)
    .fetch_one(&state.pool)
    .await
    .unwrap_or(0);
    
    // Get wins (where their team scored more)
    let wins: i64 = sqlx::query_scalar(
        r#"SELECT COUNT(*) FROM match_participants mp
           JOIN matches m ON mp.match_id = m.id
           WHERE mp.player_id = ? 
           AND ((mp.team = 1 AND m.team1_score > m.team2_score) 
                OR (mp.team = 2 AND m.team2_score > m.team1_score))"#
    )
    .bind(player_id)
    .fetch_one(&state.pool)
    .await
    .unwrap_or(0);
    
    let losses = total_matches - wins;
    let win_rate = if total_matches > 0 { (wins as f64 / total_matches as f64 * 100.0) as i32 } else { 0 };
    
    // Get rating history (last 20 matches)
    let history: Vec<(f64, f64, String)> = sqlx::query_as(
        r#"SELECT mp.rating_after, mp.rating_change, m.timestamp
           FROM match_participants mp
           JOIN matches m ON mp.match_id = m.id
           WHERE mp.player_id = ?
           ORDER BY m.timestamp DESC
           LIMIT 20"#
    )
    .bind(player_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build rating chart data
    let chart_data: String = history.iter().rev()
        .map(|(rating, _, _)| format!("{:.0}", rating))
        .collect::<Vec<_>>()
        .join(",");
    
    // Get head-to-head stats
    let h2h: Vec<(i64, String, i64, i64)> = sqlx::query_as(
        r#"SELECT 
            opp.id,
            opp.name,
            SUM(CASE WHEN 
                (mp1.team = 1 AND m.team1_score > m.team2_score) OR 
                (mp1.team = 2 AND m.team2_score > m.team1_score) 
            THEN 1 ELSE 0 END) as wins,
            SUM(CASE WHEN 
                (mp1.team = 1 AND m.team1_score < m.team2_score) OR 
                (mp1.team = 2 AND m.team2_score < m.team1_score) 
            THEN 1 ELSE 0 END) as losses
        FROM match_participants mp1
        JOIN matches m ON mp1.match_id = m.id
        JOIN match_participants mp2 ON m.id = mp2.match_id AND mp2.player_id != mp1.player_id AND mp2.team != mp1.team
        JOIN players opp ON mp2.player_id = opp.id
        WHERE mp1.player_id = ?
        GROUP BY opp.id, opp.name
        ORDER BY (wins + losses) DESC
        LIMIT 10"#
    )
    .bind(player_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let h2h_rows: String = h2h.iter()
        .map(|(opp_id, opp_name, w, l)| {
            let total = w + l;
            let pct = if total > 0 { (*w as f64 / total as f64 * 100.0) as i32 } else { 0 };
            format!(r#"<tr><td><a href="/players/{}">{}</a></td><td>{}</td><td>{}</td><td>{}%</td></tr>"#,
                opp_id, opp_name, w, l, pct)
        })
        .collect();
    
    // Achievements
    let mut achievements = Vec::new();
    if total_matches >= 1 { achievements.push("🎾 First Match"); }
    if total_matches >= 10 { achievements.push("🔟 10 Matches"); }
    if total_matches >= 50 { achievements.push("🏆 50 Matches"); }
    if wins >= 1 { achievements.push("✨ First Win"); }
    if wins >= 10 { achievements.push("🔥 10 Wins"); }
    if win_rate >= 60 && total_matches >= 5 { achievements.push("💪 60% Win Rate"); }
    if singles_rating >= 1700.0 { achievements.push("⭐ Rising Star (1700+)"); }
    if singles_rating >= 1900.0 { achievements.push("👑 Elite (1900+)"); }
    
    let achievements_html: String = achievements.iter()
        .map(|a| format!(r#"<span class="achievement">{}</span>"#, a))
        .collect();
    
    // Recent matches
    let recent: Vec<(String, i32, i32, i32, f64, String)> = sqlx::query_as(
        r#"SELECT m.match_type, m.team1_score, m.team2_score, mp.team, mp.rating_change, m.timestamp
           FROM match_participants mp
           JOIN matches m ON mp.match_id = m.id
           WHERE mp.player_id = ?
           ORDER BY m.timestamp DESC
           LIMIT 10"#
    )
    .bind(player_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let recent_rows: String = recent.iter()
        .map(|(mtype, t1, t2, team, change, ts)| {
            let won = (*team == 1 && t1 > t2) || (*team == 2 && t2 > t1);
            let result = if won { "<span class='badge badge-win'>W</span>" } else { "<span class='badge badge-loss'>L</span>" };
            let change_class = if *change >= 0.0 { "rating-up" } else { "rating-down" };
            let change_sign = if *change >= 0.0 { "+" } else { "" };
            format!(r#"<tr><td>{}</td><td>{} - {}</td><td>{}</td><td class="{}">{}{}</td><td>{}</td></tr>"#,
                mtype, t1, t2, result, change_class, change_sign, *change as i32, &ts[..16])
        })
        .collect();
    
    // Partner synergy - how well do you perform with different partners in doubles?
    let partners: Vec<(i64, String, i64, i64, f64)> = sqlx::query_as(
        r#"SELECT 
            partner.id,
            partner.name,
            SUM(CASE WHEN 
                (mp1.team = 1 AND m.team1_score > m.team2_score) OR 
                (mp1.team = 2 AND m.team2_score > m.team1_score) 
            THEN 1 ELSE 0 END) as wins,
            SUM(CASE WHEN 
                (mp1.team = 1 AND m.team1_score < m.team2_score) OR 
                (mp1.team = 2 AND m.team2_score < m.team1_score) 
            THEN 1 ELSE 0 END) as losses,
            AVG(mp1.rating_change) as avg_change
        FROM match_participants mp1
        JOIN matches m ON mp1.match_id = m.id
        JOIN match_participants mp2 ON m.id = mp2.match_id 
            AND mp2.player_id != mp1.player_id 
            AND mp2.team = mp1.team
        JOIN players partner ON mp2.player_id = partner.id
        WHERE mp1.player_id = ? AND m.match_type = 'doubles'
        GROUP BY partner.id, partner.name
        HAVING (wins + losses) >= 1
        ORDER BY (wins + losses) DESC
        LIMIT 8"#
    )
    .bind(player_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let partners_rows: String = if partners.is_empty() {
        "<tr><td colspan='5' style='text-align:center; color:#999;'>No doubles matches yet</td></tr>".to_string()
    } else {
        partners.iter()
            .map(|(pid, pname, w, l, avg)| {
                let total = w + l;
                let pct = if total > 0 { (*w as f64 / total as f64 * 100.0) as i32 } else { 0 };
                let avg_class = if *avg >= 0.0 { "rating-up" } else { "rating-down" };
                let avg_sign = if *avg >= 0.0 { "+" } else { "" };
                let synergy = if pct >= 60 { "🔥" } else if pct >= 50 { "✓" } else { "⚠️" };
                format!(r#"<tr><td><a href="/players/{}">{}</a></td><td>{}</td><td>{}</td><td>{}% {}</td><td class="{}">{}{:.0}</td></tr>"#,
                    pid, pname, w, l, pct, synergy, avg_class, avg_sign, avg)
            })
            .collect()
    };
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{} - Pickleball ELO</title>
        <style>
            {}
            .profile-header {{ text-align: center; margin-bottom: 30px; }}
            .rating-display {{ font-size: 48px; font-weight: bold; color: #003594; }}
            .sections {{ display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }}
            @media (max-width: 768px) {{ .sections {{ grid-template-columns: 1fr; }} }}
            .chart-container {{ height: 100px; margin: 20px 0; }}
        </style>
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    </head>
    <body>
        <div class="container">
            {}
            
            <div class="profile-header">
                <h1>👤 {}</h1>
                <p style="color: #666;">{}</p>
                <a href="/players/{}/edit" class="btn btn-sm" style="margin-top: 10px;">✏️ Edit Profile</a>
            </div>
            
            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-value">{:.0}</div>
                    <div class="stat-label">ELO Rating</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Matches</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}-{}</div>
                    <div class="stat-label">W-L ({}%)</div>
                </div>
            </div>
            
            <h2>🏅 Achievements</h2>
            <div style="margin: 20px 0;">
                {}
            </div>
            
            <div class="chart-container">
                <canvas id="ratingChart"></canvas>
            </div>
            
            <div class="sections">
                <div>
                    <h2>📊 Head-to-Head</h2>
                    <table>
                        <thead><tr><th>Opponent</th><th>W</th><th>L</th><th>%</th></tr></thead>
                        <tbody>{}</tbody>
                    </table>
                </div>
                
                <div>
                    <h2>🤝 Partner Synergy</h2>
                    <p style="font-size: 12px; color: #666; margin-bottom: 10px;">How well you perform with each doubles partner</p>
                    <table>
                        <thead><tr><th>Partner</th><th>W</th><th>L</th><th>Win%</th><th>Avg Δ</th></tr></thead>
                        <tbody>{}</tbody>
                    </table>
                </div>
            </div>
            
            <h2>📜 Recent Matches</h2>
            <table>
                <thead><tr><th>Type</th><th>Score</th><th>Result</th><th>Δ</th><th>Date</th></tr></thead>
                <tbody>{}</tbody>
            </table>
        </div>
        
        <script>
            const ctx = document.getElementById('ratingChart').getContext('2d');
            new Chart(ctx, {{
                type: 'line',
                data: {{
                    labels: [{}].map((_, i) => ''),
                    datasets: [{{
                        label: 'Rating',
                        data: [{}],
                        borderColor: '#003594',
                        backgroundColor: 'rgba(102, 126, 234, 0.1)',
                        fill: true,
                        tension: 0.3
                    }}]
                }},
                options: {{
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: {{ legend: {{ display: false }} }},
                    scales: {{ x: {{ display: false }}, y: {{ beginAtZero: false }} }}
                }}
            }});
        </script>
    </body>
    </html>
    "#, name, COMMON_CSS, nav_html(), name, 
        email.as_deref().unwrap_or("No email"), player_id,
        singles_rating, total_matches, wins, losses, win_rate,
        achievements_html, h2h_rows, partners_rows, recent_rows, chart_data, chart_data);
    
    Ok(Html(html))
}

/// Calculates the most balanced team combination for 4 players based on ratings
///
/// **Endpoint:** `GET /balance`
///
/// **Description:** Allows selection of 4 players and computes the team pairing with the smallest rating difference to ensure competitive balance.
///
/// **Query Parameters:** `p1`, `p2`, `p3`, `p4` (optional player IDs)
///
/// **Returns:** HTML form with player selector dropdowns and balanced team result if 4 players selected
async fn team_balancer_handler(
    State(state): State<AppState>,
    Query(params): Query<BalanceQuery>,
) -> Html<String> {
    let players: Vec<(i64, String, f64)> = sqlx::query_as(
        "SELECT id, name, singles_rating FROM players ORDER BY name"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let player_options: String = players.iter()
        .map(|(id, name, rating)| {
            format!(r#"<option value="{}">{} ({:.0})</option>"#, id, name, rating)
        })
        .collect();
    
    // Calculate balance if 4 players selected
    let mut balance_result = String::new();
    if let (Some(p1), Some(p2), Some(p3), Some(p4)) = (params.p1, params.p2, params.p3, params.p4) {
        let selected: HashMap<i64, (String, f64)> = players.iter()
            .filter(|(id, _, _)| [p1, p2, p3, p4].contains(id))
            .map(|(id, name, rating)| (*id, (name.clone(), *rating)))
            .collect();
        
        if selected.len() == 4 {
            let ids = [p1, p2, p3, p4];
            let mut best_diff = f64::MAX;
            let mut best_teams = ((0, 0), (0, 0));
            
            // Try all possible team combinations
            let combos = [
                ((0, 1), (2, 3)),
                ((0, 2), (1, 3)),
                ((0, 3), (1, 2)),
            ];
            
            for ((a, b), (c, d)) in combos {
                let team1_rating = selected.get(&ids[a]).map(|(_, r)| r).unwrap_or(&1500.0) 
                    + selected.get(&ids[b]).map(|(_, r)| r).unwrap_or(&1500.0);
                let team2_rating = selected.get(&ids[c]).map(|(_, r)| r).unwrap_or(&1500.0) 
                    + selected.get(&ids[d]).map(|(_, r)| r).unwrap_or(&1500.0);
                let diff = (team1_rating - team2_rating).abs();
                
                if diff < best_diff {
                    best_diff = diff;
                    best_teams = ((a, b), (c, d));
                }
            }
            
            let ((a, b), (c, d)) = best_teams;
            let t1_names = format!("{} & {}", 
                selected.get(&ids[a]).map(|(n, _)| n.as_str()).unwrap_or("?"),
                selected.get(&ids[b]).map(|(n, _)| n.as_str()).unwrap_or("?"));
            let t2_names = format!("{} & {}", 
                selected.get(&ids[c]).map(|(n, _)| n.as_str()).unwrap_or("?"),
                selected.get(&ids[d]).map(|(n, _)| n.as_str()).unwrap_or("?"));
            
            let t1_rating = selected.get(&ids[a]).map(|(_, r)| r).unwrap_or(&1500.0) 
                + selected.get(&ids[b]).map(|(_, r)| r).unwrap_or(&1500.0);
            let t2_rating = selected.get(&ids[c]).map(|(_, r)| r).unwrap_or(&1500.0) 
                + selected.get(&ids[d]).map(|(_, r)| r).unwrap_or(&1500.0);
            
            balance_result = format!(r#"
                <div style="background: #d4edda; padding: 20px; border-radius: 8px; margin: 20px 0;">
                    <h3 style="color: #155724; margin: 0 0 15px 0;">⚖️ Most Balanced Teams</h3>
                    <div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px; align-items: center;">
                        <div style="text-align: center;">
                            <div style="font-weight: bold; font-size: 18px;">{}</div>
                            <div style="color: #666;">Combined: {:.0}</div>
                        </div>
                        <div style="font-size: 24px; font-weight: bold;">VS</div>
                        <div style="text-align: center;">
                            <div style="font-weight: bold; font-size: 18px;">{}</div>
                            <div style="color: #666;">Combined: {:.0}</div>
                        </div>
                    </div>
                    <div style="text-align: center; margin-top: 15px; color: #155724;">
                        Rating difference: <strong>{:.0}</strong> points
                    </div>
                </div>
            "#, t1_names, t1_rating, t2_names, t2_rating, best_diff);
        }
    }
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Team Balancer - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container" style="max-width: 600px;">
            <h1>⚖️ Team Balancer</h1>
            {}
            
            <p style="text-align: center; color: #666;">Select 4 players to find the most balanced doubles teams</p>
            
            <form method="GET" action="/balance">
                <div class="stats-grid">
                    <div class="stat-card">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 1</label>
                        <select name="p1" style="width: 100%; padding: 8px; border-radius: 6px; border: 2px solid #ddd;">
                            <option value="">Select...</option>
                            {}
                        </select>
                    </div>
                    <div class="stat-card">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 2</label>
                        <select name="p2" style="width: 100%; padding: 8px; border-radius: 6px; border: 2px solid #ddd;">
                            <option value="">Select...</option>
                            {}
                        </select>
                    </div>
                    <div class="stat-card">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 3</label>
                        <select name="p3" style="width: 100%; padding: 8px; border-radius: 6px; border: 2px solid #ddd;">
                            <option value="">Select...</option>
                            {}
                        </select>
                    </div>
                    <div class="stat-card">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 4</label>
                        <select name="p4" style="width: 100%; padding: 8px; border-radius: 6px; border: 2px solid #ddd;">
                            <option value="">Select...</option>
                            {}
                        </select>
                    </div>
                </div>
                
                <div style="text-align: center; margin-top: 20px;">
                    <button type="submit" class="btn btn-success" style="font-size: 18px; padding: 15px 40px;">
                        🎯 Find Balanced Teams
                    </button>
                </div>
            </form>
            
            {}
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), player_options, player_options, player_options, player_options, balance_result);
    
    Html(html)
}

/// Deletes a match and reverts all player ratings to their pre-match values
///
/// **Endpoint:** `POST /matches/:id/delete`
///
/// **Description:** Removes a match record from the database and restores all participants' ratings to before the match was played.
///
/// **Parameters:** `id` (path): Match ID to delete
///
/// **Returns:** Redirect to `/matches` on success, or error response on failure
async fn delete_match(
    State(state): State<AppState>,
    Path(match_id): Path<i64>,
) -> Result<Redirect, (StatusCode, String)> {
    // Get match info and participants to revert ratings
    let participants: Vec<(i64, String, f64, f64)> = sqlx::query_as(
        r#"SELECT mp.player_id, m.match_type, mp.rating_before, mp.rating_after
           FROM match_participants mp
           JOIN matches m ON mp.match_id = m.id
           WHERE mp.match_id = ?"#
    )
    .bind(match_id)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    // Revert each player's rating (UNIFIED: always use singles_rating)
    for (player_id, _match_type, rating_before, _rating_after) in &participants {
        sqlx::query("UPDATE players SET singles_rating = ? WHERE id = ?")
            .bind(rating_before)
            .bind(player_id)
            .execute(&state.pool)
            .await
            .ok();
    }
    
    // Delete the match (cascade deletes participants)
    sqlx::query("DELETE FROM matches WHERE id = ?")
        .bind(match_id)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    Ok(Redirect::to("/matches"))
}

/// Displays the form to create a new player
///
/// **Endpoint:** `GET /players/new`
///
/// **Description:** Shows an HTML form for adding a new player to the system with name and optional email.
///
/// **Parameters:** None
///
/// **Returns:** HTML form page
async fn new_player_form() -> Html<String> {
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Add Player - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container" style="max-width: 500px;">
            <h1>➕ Add New Player</h1>
            {}
            
            <form method="POST" action="/players/new">
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px;">Player Name *</label>
                    <input type="text" name="name" required placeholder="Enter player name"
                           style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px;">Email (optional)</label>
                    <input type="email" name="email" placeholder="player@example.com"
                           style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
                </div>
                
                <button type="submit" class="btn btn-success" style="width: 100%; padding: 14px;">Create Player</button>
            </form>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html());
    
    Html(html)
}

/// Creates a new player in the database with initial ratings
///
/// **Endpoint:** `POST /players/new`
///
/// **Description:** Processes form submission to add a new player. Initializes with default ELO ratings (1500) and RD (350).
///
/// **Form Fields:**
/// - `name` (required): Player name
/// - `email` (optional): Player email address
///
/// **Returns:** Redirect to `/players` on success, or error response if player name already exists
async fn create_player(
    State(state): State<AppState>,
    Form(player): Form<NewPlayer>,
) -> Result<Redirect, (StatusCode, String)> {
    let email = player.email.filter(|e| !e.is_empty());
    
    sqlx::query("INSERT INTO players (name, email) VALUES (?, ?)")
        .bind(&player.name)
        .bind(&email)
        .execute(&state.pool)
        .await
        .map_err(|e| {
            if e.to_string().contains("UNIQUE constraint") {
                (StatusCode::BAD_REQUEST, format!("Player '{}' already exists", player.name))
            } else {
                (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e))
            }
        })?;
    
    Ok(Redirect::to("/players"))
}

/// Displays the form to edit an existing player's details
///
/// **Endpoint:** `GET /players/:id/edit`
///
/// **Description:** Shows an HTML form pre-populated with a player's current information (name, email, ratings) for editing.
///
/// **Parameters:** `id` (path): Player ID
///
/// **Returns:** HTML form page with current player data, or 404 if player not found
async fn edit_player_form(
    State(state): State<AppState>,
    Path(player_id): Path<i64>,
) -> Result<Html<String>, (StatusCode, String)> {
    let player: Option<(i64, String, Option<String>, f64, f64)> = sqlx::query_as(
        "SELECT id, name, email, singles_rating, doubles_rating FROM players WHERE id = ?"
    )
    .bind(player_id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    let (id, name, email, singles_rating, doubles_rating) = player
        .ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?;
    
    let email_value = email.unwrap_or_default();
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Edit {} - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container" style="max-width: 500px;">
            <h1>✏️ Edit Player</h1>
            {}
            
            <form method="POST" action="/players/{}/edit">
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px;">Player Name *</label>
                    <input type="text" name="name" required value="{}"
                           style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px;">Email</label>
                    <input type="email" name="email" value="{}"
                           style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label style="display: block; font-weight: bold; margin-bottom: 8px;">ELO Rating</label>
                    <input type="number" name="singles_rating" step="0.1" value="{:.1}"
                           style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
                    <input type="hidden" name="doubles_rating" value="{:.1}">
                    <p style="color: #666; font-size: 12px; margin-top: 5px;">Unified rating for all match types</p>
                </div>
                
                <button type="submit" class="btn btn-success" style="width: 100%; padding: 14px;">Save Changes</button>
            </form>
            
            <a href="/players/{}" class="back-link">← Back to Profile</a>
        </div>
    </body>
    </html>
    "#, name, COMMON_CSS, nav_html(), id, name, email_value, singles_rating, doubles_rating, id);
    
    Ok(Html(html))
}

/// Updates an existing player's profile information
///
/// **Endpoint:** `POST /players/:id/edit`
///
/// **Description:** Processes form submission to update player name, email, and both rating values (for manual corrections).
///
/// **Parameters:** `id` (path): Player ID
///
/// **Form Fields:**
/// - `name` (required): Updated player name
/// - `email` (optional): Updated email address
/// - `singles_rating` (required): Updated singles rating value
/// - `doubles_rating` (required): Updated doubles rating value
///
/// **Returns:** Redirect to player profile on success, or error response on failure
async fn update_player(
    State(state): State<AppState>,
    Path(player_id): Path<i64>,
    Form(player): Form<EditPlayer>,
) -> Result<Redirect, (StatusCode, String)> {
    let email = player.email.filter(|e| !e.is_empty());
    
    sqlx::query(
        "UPDATE players SET name = ?, email = ?, singles_rating = ?, doubles_rating = ? WHERE id = ?"
    )
    .bind(&player.name)
    .bind(&email)
    .bind(player.singles_rating)
    .bind(player.doubles_rating)
    .execute(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?;
    
    Ok(Redirect::to(&format!("/players/{}", player_id)))
}

/// Displays the form to record a new match result
///
/// **Endpoint:** `GET /matches/new`
///
/// **Description:** Shows an interactive HTML form for entering match details with toggleable singles/doubles mode, player selection, and score entry.
///
/// **Parameters:** None
///
/// **Returns:** HTML form page with player dropdown lists
async fn new_match_form(State(state): State<AppState>) -> Html<String> {
    let players: Vec<(i64, String, f64, f64)> = sqlx::query_as(
        "SELECT id, name, singles_rating, doubles_rating FROM players ORDER BY name"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let player_options: String = players.iter()
        .map(|(id, name, sr, dr)| {
            format!(r#"<option value="{}">{} (S:{:.0} / D:{:.0})</option>"#, id, name, sr, dr)
        })
        .collect();
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Record Match - Pickleball ELO</title>
        <style>
            {}
            .team-box {{ background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
            .team-box h3 {{ margin: 0 0 15px 0; color: #003594; }}
            .score-row {{ display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px; align-items: center; margin-bottom: 20px; }}
            .vs {{ font-weight: bold; color: #999; font-size: 20px; }}
            .match-type-toggle {{ display: flex; gap: 10px; margin-bottom: 20px; }}
            .match-type-toggle label {{ flex: 1; padding: 15px; text-align: center; border: 2px solid #ddd; border-radius: 8px; cursor: pointer; }}
            .match-type-toggle input {{ display: none; }}
            .match-type-toggle input:checked + label {{ border-color: #003594; background: #f0f5ff; color: #003594; }}
            .doubles-player {{ margin-top: 15px; }}
            .hidden {{ display: none !important; }}
            select, input[type="number"] {{ width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box; }}
        </style>
    </head>
    <body>
        <div class="container" style="max-width: 600px;">
            <h1>🎾 Record Match Result</h1>
            {}
            
            <form method="POST" action="/matches/new">
                <div class="match-type-toggle">
                    <input type="radio" id="singles" name="match_type" value="singles" checked>
                    <label for="singles">🎾 Singles</label>
                    <input type="radio" id="doubles" name="match_type" value="doubles">
                    <label for="doubles">👥 Doubles</label>
                </div>
                
                <div class="team-box">
                    <h3>Team 1</h3>
                    <div>
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 1</label>
                        <select name="team1_player1" required>
                            <option value="">Select player...</option>
                            {}
                        </select>
                    </div>
                    <div class="doubles-player hidden">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 2</label>
                        <select name="team1_player2">
                            <option value="">Select player...</option>
                            {}
                        </select>
                    </div>
                </div>
                
                <div class="team-box">
                    <h3>Team 2</h3>
                    <div>
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 1</label>
                        <select name="team2_player1" required>
                            <option value="">Select player...</option>
                            {}
                        </select>
                    </div>
                    <div class="doubles-player hidden">
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Player 2</label>
                        <select name="team2_player2">
                            <option value="">Select player...</option>
                            {}
                        </select>
                    </div>
                </div>
                
                <div class="score-row">
                    <div>
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Team 1 Score</label>
                        <input type="number" name="team1_score" min="0" max="15" required value="11">
                    </div>
                    <div class="vs">VS</div>
                    <div>
                        <label style="display: block; margin-bottom: 8px; font-weight: bold;">Team 2 Score</label>
                        <input type="number" name="team2_score" min="0" max="15" required value="0">
                    </div>
                </div>
                
                <button type="submit" class="btn btn-success" style="width: 100%; padding: 14px; font-size: 16px;">
                    Record Match
                </button>
            </form>
        </div>
        
        <script>
            document.querySelectorAll('input[name="match_type"]').forEach(radio => {{
                radio.addEventListener('change', function() {{
                    document.querySelectorAll('.doubles-player').forEach(field => {{
                        field.classList.toggle('hidden', this.value === 'singles');
                    }});
                }});
            }});
        </script>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), player_options, player_options, player_options, player_options);
    
    Html(html)
}

/// Records a new match result and updates all participants' ratings using ELO
///
/// **Endpoint:** `POST /matches/new`
///
/// **Description:** Processes match submission, creates match record, records participants, and calculates/applies rating changes based on match outcome and margin of victory.
///
/// **Form Fields:**
/// - `match_type` (required): 'singles' or 'doubles'
/// - `team1_player1` (required): First team's primary player ID
/// - `team1_player2` (optional): First team's second player ID (for doubles)
/// - `team2_player1` (required): Second team's primary player ID
/// - `team2_player2` (optional): Second team's second player ID (for doubles)
/// - `team1_score` (required): Team 1 final score
/// - `team2_score` (required): Team 2 final score
///
/// **Returns:** Redirect to `/matches` on success, or error response on failure
async fn create_match(
    State(state): State<AppState>,
    Form(match_data): Form<NewMatch>,
) -> Result<Redirect, (StatusCode, String)> {
    let session_id: i64 = sqlx::query_scalar(
        "INSERT INTO sessions (notes) VALUES ('Web UI Session') RETURNING id"
    )
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create session: {}", e)))?;
    
    let match_id: i64 = sqlx::query_scalar(
        "INSERT INTO matches (session_id, match_type, team1_score, team2_score) VALUES (?, ?, ?, ?) RETURNING id"
    )
    .bind(session_id)
    .bind(&match_data.match_type)
    .bind(match_data.team1_score)
    .bind(match_data.team2_score)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create match: {}", e)))?;
    
    let is_doubles = match_data.match_type == "doubles";
    // UNIFIED RATING: Always use singles_rating as the unified rating column
    let rating_col = "singles";
    
    let mut team1_players = vec![match_data.team1_player1];
    if is_doubles { if let Some(p2) = match_data.team1_player2 { team1_players.push(p2); } }
    
    let mut team2_players = vec![match_data.team2_player1];
    if is_doubles { if let Some(p2) = match_data.team2_player2 { team2_players.push(p2); } }
    
    let _team1_wins = match_data.team1_score > match_data.team2_score;
    let calc = EloCalculator::new();
    
    // Calculate per-point performance for team 1
    let team1_performance = calculate_weighted_score(
        1500.0,  // placeholder, will use effective opponent per player
        1500.0,
        match_data.team1_score,
        match_data.team2_score,
    );
    
    // Update and record for team 1
    for player_id in &team1_players {
        let current: (f64, f64, f64) = sqlx::query_as(&format!(
            "SELECT {}_rating, {}_rd, {}_volatility FROM players WHERE id = ?",
            rating_col, rating_col, rating_col
        ))
        .bind(player_id)
        .fetch_one(&state.pool)
        .await
        .unwrap_or((1500.0, 350.0, 0.06));
        
        let current_rating = EloRating::new_with_rating(current.0);
        
        // Calculate opponent rating(s)
        let opponent_rating = if is_doubles {
            // Get opponent ratings
            let mut team2_ratings = vec![];
            for pid in &team2_players {
                let rating: f64 = sqlx::query_scalar(&format!(
                    "SELECT {}_rating FROM players WHERE id = ?",
                    rating_col
                ))
                .bind(pid)
                .fetch_one(&state.pool)
                .await
                .unwrap_or(1500.0);
                team2_ratings.push(rating);
            }
            
            // Get teammate rating
            let teammate_rating = if team1_players.len() > 1 {
                let teammate_id = team1_players.iter().find(|pid| **pid != *player_id).copied().unwrap_or(*player_id);
                sqlx::query_scalar::<_, f64>(&format!(
                    "SELECT {}_rating FROM players WHERE id = ?",
                    rating_col
                ))
                .bind(teammate_id)
                .fetch_one(&state.pool)
                .await
                .unwrap_or(1500.0)
            } else {
                1500.0
            };
            
            // Effective opponent = Opp1 + Opp2 - Teammate
            calculate_effective_opponent_rating(
                team2_ratings.get(0).copied().unwrap_or(1500.0),
                team2_ratings.get(1).copied().unwrap_or(1500.0),
                teammate_rating,
            )
        } else {
            // Singles: opponent is team2_player1
            sqlx::query_scalar::<_, f64>(&format!(
                "SELECT {}_rating FROM players WHERE id = ?",
                rating_col
            ))
            .bind(match_data.team2_player1)
            .fetch_one(&state.pool)
            .await
            .unwrap_or(1500.0)
        };
        
        let opponent = EloRating::new_with_rating(opponent_rating);
        let new_rating = calc.update_rating(&current_rating, &opponent, team1_performance);
        let change = new_rating.rating - current.0;
        
        sqlx::query(&format!("UPDATE players SET {}_rating = ? WHERE id = ?", rating_col))
            .bind(new_rating.rating)
            .bind(player_id)
            .execute(&state.pool)
            .await
            .ok();
        
        sqlx::query(
            "INSERT INTO match_participants (match_id, player_id, team, rating_before, rd_before, volatility_before, rating_after, rd_after, volatility_after, rating_change) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?)"
        )
        .bind(match_id)
        .bind(player_id)
        .bind(current.0)
        .bind(current.1)
        .bind(current.2)
        .bind(new_rating.rating)
        .bind(current.1)
        .bind(current.2)
        .bind(change)
        .execute(&state.pool)
        .await
        .ok();
    }
    
    // Calculate per-point performance for team 2
    let team2_performance = calculate_weighted_score(
        1500.0,
        1500.0,
        match_data.team2_score,
        match_data.team1_score,
    );
    
    // Update and record for team 2
    for player_id in &team2_players {
        let current: (f64, f64, f64) = sqlx::query_as(&format!(
            "SELECT {}_rating, {}_rd, {}_volatility FROM players WHERE id = ?",
            rating_col, rating_col, rating_col
        ))
        .bind(player_id)
        .fetch_one(&state.pool)
        .await
        .unwrap_or((1500.0, 350.0, 0.06));
        
        let current_rating = EloRating::new_with_rating(current.0);
        
        // Calculate opponent rating(s)
        let opponent_rating = if is_doubles {
            // Get opponent ratings
            let mut team1_ratings = vec![];
            for pid in &team1_players {
                let rating: f64 = sqlx::query_scalar(&format!(
                    "SELECT {}_rating FROM players WHERE id = ?",
                    rating_col
                ))
                .bind(pid)
                .fetch_one(&state.pool)
                .await
                .unwrap_or(1500.0);
                team1_ratings.push(rating);
            }
            
            // Get teammate rating
            let teammate_rating = if team2_players.len() > 1 {
                let teammate_id = team2_players.iter().find(|pid| **pid != *player_id).copied().unwrap_or(*player_id);
                sqlx::query_scalar::<_, f64>(&format!(
                    "SELECT {}_rating FROM players WHERE id = ?",
                    rating_col
                ))
                .bind(teammate_id)
                .fetch_one(&state.pool)
                .await
                .unwrap_or(1500.0)
            } else {
                1500.0
            };
            
            // Effective opponent
            calculate_effective_opponent_rating(
                team1_ratings.get(0).copied().unwrap_or(1500.0),
                team1_ratings.get(1).copied().unwrap_or(1500.0),
                teammate_rating,
            )
        } else {
            // Singles: opponent is team1_player1
            sqlx::query_scalar::<_, f64>(&format!(
                "SELECT {}_rating FROM players WHERE id = ?",
                rating_col
            ))
            .bind(match_data.team1_player1)
            .fetch_one(&state.pool)
            .await
            .unwrap_or(1500.0)
        };
        
        let opponent = EloRating::new_with_rating(opponent_rating);
        let new_rating = calc.update_rating(&current_rating, &opponent, team2_performance);
        let change = new_rating.rating - current.0;
        
        sqlx::query(&format!("UPDATE players SET {}_rating = ? WHERE id = ?", rating_col))
            .bind(new_rating.rating)
            .bind(player_id)
            .execute(&state.pool)
            .await
            .ok();
        
        sqlx::query(
            "INSERT INTO match_participants (match_id, player_id, team, rating_before, rd_before, volatility_before, rating_after, rd_after, volatility_after, rating_change) VALUES (?, ?, 2, ?, ?, ?, ?, ?, ?, ?)"
        )
        .bind(match_id)
        .bind(player_id)
        .bind(current.0)
        .bind(current.1)
        .bind(current.2)
        .bind(new_rating.rating)
        .bind(current.1)
        .bind(current.2)
        .bind(change)
        .execute(&state.pool)
        .await
        .ok();
    }
    
    Ok(Redirect::to("/matches"))
}

/// Displays a list of all players with their current ratings
///
/// **Endpoint:** `GET /players`
///
/// **Description:** Shows a sortable table of all registered players with their current singles and doubles ratings, linked to individual profiles.
///
/// **Parameters:** None
///
/// **Returns:** HTML page with players table
async fn players_list_handler(State(state): State<AppState>) -> Html<String> {
    // UNIFIED RATING: just get singles_rating as the unified rating
    let players: Vec<(i64, String, Option<String>, f64)> = sqlx::query_as(
        "SELECT id, name, email, singles_rating FROM players ORDER BY singles_rating DESC"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let player_rows: String = players.iter()
        .map(|(id, name, _email, rating)| {
            format!(r#"<tr>
                <td><a href="/players/{}">{}</a></td>
                <td>{:.0}</td>
            </tr>"#, id, name, rating)
        })
        .collect();
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>All Players - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container">
            <h1>👥 All Players ({})</h1>
            {}
            
            <div style="text-align: center; margin-bottom: 20px;">
                <a href="/players/new" class="btn btn-success">➕ Add New Player</a>
            </div>
            
            <table>
                <thead>
                    <tr><th>Name</th><th>Rating</th></tr>
                </thead>
                <tbody>{}</tbody>
            </table>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, players.len(), nav_html(), player_rows);
    
    Html(html)
}

/// Displays the top 10 ranked players in singles and doubles
///
/// **Endpoint:** `GET /leaderboard`
///
/// **Description:** Shows side-by-side leaderboards for the top 10 singles and top 10 doubles players with medal badges (🥇🥈🥉).
///
/// **Parameters:** None
///
/// **Returns:** HTML page with unified leaderboard
async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
    // UNIFIED RATING: Single leaderboard using singles_rating as unified rating
    let players: Vec<(i64, String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.id, p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 10"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let player_rows: String = players.iter().enumerate()
        .map(|(i, (id, name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!(r#"<div style="padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">
                <div><span style="font-size: 18px; margin-right: 8px;">{}</span><strong>{}.</strong> <a href="/players/{}">{}</a></div>
                <strong>{:.0}</strong>
            </div>"#, medal, i + 1, id, name, rating)
        })
        .collect();
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Leaderboard - Pickleball ELO</title>
        <style>
            {}
            .leaderboard {{ background: #f9f9f9; border-radius: 8px; overflow: hidden; max-width: 500px; margin: 0 auto; }}
        </style>
    </head>
    <body>
        <div class="container">
            <h1>🏓 Leaderboard</h1>
            {}
            
            <h2 style="text-align: center;">📊 Top Players (Unified ELO)</h2>
            <div class="leaderboard">{}</div>
            
            <p style="text-align: center; color: #666; margin-top: 20px; font-size: 14px;">
                Unified rating: singles and doubles matches both contribute to one rating.<br>
                <a href="/about" style="color: #003594;">Learn how ratings work →</a>
            </p>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), player_rows);
    
    Html(html)
}

/// Returns JSON API endpoint for leaderboard data
///
/// **Endpoint:** `GET /api/leaderboard`
///
/// **Description:** Provides unified leaderboard data as JSON for external applications.
///
/// **Parameters:** None
///
/// **Returns:** JSON object with `players` array containing rank, name, and rating
async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
    // UNIFIED RATING: Single leaderboard
    let players: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 10"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    axum::Json(serde_json::json!({
        "players": players.iter().enumerate().map(|(i, (n, r))| {
            serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32})
        }).collect::<Vec<_>>(),
    }))
}

/// Returns JSON API endpoint for all players
///
/// **Endpoint:** `GET /api/players`
///
/// **Description:** Provides a complete list of all players with their ID, name, and current ratings (singles and doubles) as JSON.
///
/// **Parameters:** None
///
/// **Returns:** JSON array of player objects with id, name, singles_rating, and doubles_rating
async fn api_players_handler(State(state): State<AppState>) -> axum::Json<Vec<PlayerJson>> {
    let players: Vec<(i64, String, f64, f64)> = sqlx::query_as(
        "SELECT id, name, singles_rating, doubles_rating FROM players ORDER BY name"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    axum::Json(players.into_iter().map(|(id, name, sr, dr)| PlayerJson {
        id, name, singles_rating: sr, doubles_rating: dr,
    }).collect())
}

// ============== SESSION MANAGEMENT ==============

/// Displays a list of recent sessions with match counts and email status
///
/// **Endpoint:** `GET /sessions`
///
/// **Description:** Shows the 20 most recent sessions with start/end times, match counts, notes, and email status (pending/sent), with links to preview and send session summaries.
///
/// **Parameters:** None
///
/// **Returns:** HTML page with sessions table
async fn sessions_list_handler(State(state): State<AppState>) -> Html<String> {
    let sessions: Vec<(i64, String, Option<String>, i32, Option<String>)> = sqlx::query_as(
        r#"SELECT s.id, s.start_time, s.end_time, s.summary_sent, s.notes
           FROM sessions s
           ORDER BY s.start_time DESC
           LIMIT 20"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut session_rows = String::new();
    for (id, start, end, sent, notes) in &sessions {
        // Count matches in this session
        let match_count: i64 = sqlx::query_scalar(
            "SELECT COUNT(*) FROM matches WHERE session_id = ?"
        )
        .bind(id)
        .fetch_one(&state.pool)
        .await
        .unwrap_or(0);
        
        let status = if *sent == 1 { 
            "<span class='badge badge-win'>Sent</span>" 
        } else { 
            "<span class='badge' style='background:#ffc107;color:#000;'>Pending</span>" 
        };
        
        let end_str = end.as_deref().unwrap_or("In Progress");
        let notes_str = notes.as_deref().unwrap_or("-");
        
        session_rows.push_str(&format!(r#"
            <tr>
                <td>#{}</td>
                <td>{}</td>
                <td>{}</td>
                <td>{}</td>
                <td>{}</td>
                <td>{}</td>
                <td>
                    <a href="/sessions/{}/preview" class="btn btn-sm">📧 Preview</a>
                </td>
            </tr>
        "#, id, &start[..16], end_str, match_count, notes_str, status, id));
    }
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Sessions - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container">
            <h1>📧 Session Management</h1>
            {}
            
            <p style="text-align: center; color: #666; margin-bottom: 20px;">
                Preview and send session summary emails to players
            </p>
            
            <table>
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Started</th>
                        <th>Ended</th>
                        <th>Matches</th>
                        <th>Notes</th>
                        <th>Email</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {}
                </tbody>
            </table>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), session_rows);
    
    Html(html)
}

/// Displays a preview of the session summary email before sending
///
/// **Endpoint:** `GET /sessions/:id/preview`
///
/// **Description:** Shows what the session summary email will look like, including recipient list, match count, and current leaderboard data. Allows sending the email if not already sent.
///
/// **Parameters:** `id` (path): Session ID
///
/// **Returns:** HTML page with email preview and send button, or 404 if session not found
async fn session_preview_handler(
    State(state): State<AppState>,
    Path(session_id): Path<i64>,
) -> Result<Html<String>, (StatusCode, String)> {
    // Get session info
    let session: Option<(i64, String, Option<String>, i32)> = sqlx::query_as(
        "SELECT id, start_time, end_time, summary_sent FROM sessions WHERE id = ?"
    )
    .bind(session_id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    let (id, start_time, end_time, sent) = session
        .ok_or((StatusCode::NOT_FOUND, "Session not found".to_string()))?;
    
    // Get matches in this session
    let match_count: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM matches WHERE session_id = ?"
    )
    .bind(session_id)
    .fetch_one(&state.pool)
    .await
    .unwrap_or(0);
    
    // Get players with email who participated in this session
    let recipients: Vec<(String, String)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.email
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE m.session_id = ? AND p.email IS NOT NULL AND p.email != ''
           ORDER BY p.name"#
    )
    .bind(session_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Get matches in this session with details (grouped by day)
    let matches: Vec<(i64, String, String, i32, i32)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, 
           strftime('%Y-%m-%d', m.timestamp) as match_date,
           m.team1_score, m.team2_score
           FROM matches m
           WHERE m.session_id = ?
           ORDER BY m.timestamp"#
    )
    .bind(session_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build matches grouped by day
    let mut matches_by_day: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
    
    for (match_id, match_type, match_date, team1_score, team2_score) in &matches {
        // Get participants for this match
        let participants: Vec<(String, i32, f64)> = sqlx::query_as(
            r#"SELECT p.name, mp.team, mp.rating_change
               FROM match_participants mp
               JOIN players p ON mp.player_id = p.id
               WHERE mp.match_id = ?
               ORDER BY mp.team, p.name"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect();
        
        let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        
        let winner = if team1_score > team2_score { &team1_names } else { &team2_names };
        let match_str = format!(
            "<div style='padding:8px;background:#f9f9f9;margin:4px 0;border-radius:4px;'>\
            <strong>{}</strong>: {} vs {} ({}-{}) — <span style='color:#28a745;'>{} wins</span></div>",
            match_type.to_uppercase(), team1_names, team2_names, team1_score, team2_score, winner
        );
        
        matches_by_day.entry(match_date.clone()).or_default().push(match_str);
    }
    
    let matches_html: String = matches_by_day.iter()
        .map(|(date, match_list)| {
            format!("<h4 style='margin-top:15px;color:#003594;'>📅 {}</h4>{}", 
                date, match_list.join(""))
        })
        .collect();
    
    // Get top players for this session
    let top_singles: Vec<(String, f64)> = sqlx::query_as(
        "SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let top_doubles: Vec<(String, f64)> = sqlx::query_as(
        "SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let recipients_list: String = if recipients.is_empty() {
        "<p style='color: #dc3545;'>⚠️ No players with email addresses participated in this session</p>".to_string()
    } else {
        recipients.iter()
            .map(|(name, email)| format!(r#"<div style="padding: 8px; background: #f9f9f9; margin: 4px 0; border-radius: 4px;">
                <strong>{}</strong> &lt;{}&gt;
            </div>"#, name, email))
            .collect()
    };
    
    let singles_list: String = top_singles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    let _doubles_list: String = top_doubles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    let already_sent = if sent == 1 {
        "<div style='background: #d4edda; padding: 15px; border-radius: 8px; margin-bottom: 20px;'>
            ✅ This session summary has already been sent.
        </div>"
    } else { "" };
    
    let send_button = if sent == 0 && !recipients.is_empty() {
        format!(r#"
            <form method="POST" action="/sessions/{}/send" style="text-align: center; margin-top: 30px;"
                  onsubmit="return confirm('Send session summary to {} recipient(s)?')">
                <button type="submit" class="btn btn-success" style="font-size: 18px; padding: 15px 40px;">
                    📧 Send Session Summary
                </button>
            </form>
        "#, session_id, recipients.len())
    } else {
        String::new()
    };
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Session #{} Preview - Pickleball ELO</title>
        <style>{}</style>
    </head>
    <body>
        <div class="container">
            <h1>📧 Session #{} Email Preview</h1>
            {}
            
            {}
            
            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Matches</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Recipients</div>
                </div>
            </div>
            
            <h2>📬 Recipients</h2>
            {}
            
            <h2>🎾 Match Results (by Day)</h2>
            {}
            
            <h2>📄 Email Preview</h2>
            <div style="border: 2px solid #003594; border-radius: 8px; padding: 20px; background: #fafafa;">
                <h3 style="color: #003594;">🏓 Pickleball Session Summary</h3>
                <p>Session: {} to {}</p>
                
                <h4>🎾 Match Results</h4>
                {}
                
                <div style="margin-top: 20px;">
                    <h4>📊 Top Players (Unified ELO)</h4>
                    {}
                </div>
            </div>
            
            {}
            
            <a href="/sessions" class="back-link">← Back to Sessions</a>
        </div>
    </body>
    </html>
    "#, id, COMMON_CSS, id, nav_html(), already_sent, match_count, recipients.len(), 
        recipients_list, matches_html, &start_time[..16], end_time.as_deref().unwrap_or("In Progress"),
        matches_html, singles_list, send_button);
    
    Ok(Html(html))
}

/// Sends the session summary email to all participating players with email addresses
///
/// **Endpoint:** `POST /sessions/:id/send`
///
/// **Description:** Sends an HTML email containing the session summary with leaderboard snapshots to all players who participated in the session and have email addresses on file. Marks session as sent.
///
/// **Parameters:** `id` (path): Session ID
///
/// **Returns:** Redirect to `/sessions` on success, or error response if no recipients found or email send fails
async fn send_session_email(
    State(state): State<AppState>,
    Path(session_id): Path<i64>,
) -> Result<Redirect, (StatusCode, String)> {
    // Get recipients
    let recipients: Vec<(String, String)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.email
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE m.session_id = ? AND p.email IS NOT NULL AND p.email != ''"#
    )
    .bind(session_id)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    if recipients.is_empty() {
        return Err((StatusCode::BAD_REQUEST, "No recipients with email addresses".to_string()));
    }
    
    // Get matches in this session with details (grouped by day)
    let matches: Vec<(i64, String, String, i32, i32)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, 
           strftime('%Y-%m-%d', m.timestamp) as match_date,
           m.team1_score, m.team2_score
           FROM matches m
           WHERE m.session_id = ?
           ORDER BY m.timestamp"#
    )
    .bind(session_id)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build matches grouped by day for email
    let mut matches_by_day: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
    
    for (match_id, match_type, match_date, team1_score, team2_score) in &matches {
        let participants: Vec<(String, i32, f64)> = sqlx::query_as(
            r#"SELECT p.name, mp.team, mp.rating_change
               FROM match_participants mp
               JOIN players p ON mp.player_id = p.id
               WHERE mp.match_id = ?
               ORDER BY mp.team, p.name"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect();
        
        let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        
        let winner = if team1_score > team2_score { &team1_names } else { &team2_names };
        let match_str = format!(
            "<tr><td><strong>{}</strong></td><td>{} vs {}</td><td>{}-{}</td><td style='color:#28a745;'>{} wins</td></tr>",
            match_type.to_uppercase(), team1_names, team2_names, team1_score, team2_score, winner
        );
        
        matches_by_day.entry(match_date.clone()).or_default().push(match_str);
    }
    
    let matches_email_html: String = matches_by_day.iter()
        .map(|(date, match_list)| {
            format!("<h3 style='color:#003594;margin-top:20px;'>📅 {}</h3><table style='width:100%;'>{}</table>", 
                date, match_list.join(""))
        })
        .collect();
    
    // Get leaderboard data for email
    let top_singles: Vec<(String, f64)> = sqlx::query_as(
        "SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let top_doubles: Vec<(String, f64)> = sqlx::query_as(
        "SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build email HTML
    let singles_html: String = top_singles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<tr><td>{} {}. {}</td><td style='text-align:right;'>{:.0}</td></tr>", medal, i+1, name, rating)
        })
        .collect();
    
    let _doubles_html: String = top_doubles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<tr><td>{} {}. {}</td><td style='text-align:right;'>{:.0}</td></tr>", medal, i+1, name, rating)
        })
        .collect();
    
    let email_body = format!(r#"
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; background: #f5f5f5; padding: 20px;">
        <div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 12px;">
            <h1 style="color: #003594; text-align: center;">🏓 Pickleball Session Summary</h1>
            
            <h2 style="color: #333; border-bottom: 2px solid #003594; padding-bottom: 10px;">🎾 Match Results</h2>
            {}
            
            <h2 style="color: #333; border-bottom: 2px solid #003594; padding-bottom: 10px; margin-top: 30px;">📊 Top Players (Unified ELO)</h2>
            <table style="width: 100%;">{}</table>
            
            <p style="text-align: center; margin-top: 30px; color: #666; font-size: 12px;">
                Pickleball ELO Tracker - Pure ELO Rating System
            </p>
        </div>
    </body>
    </html>
    "#, matches_email_html, singles_html);
    
    // Send emails using lettre (already in dependencies)
    use lettre::{Message, SmtpTransport, Transport};
    use lettre::transport::smtp::authentication::Credentials;
    
    let smtp_user = "split@danesabo.com";
    let smtp_pass = "Keep an eye 0ut 4 Split!";
    
    let creds = Credentials::new(smtp_user.to_string(), smtp_pass.to_string());
    
    let mailer = SmtpTransport::starttls_relay("smtppro.zoho.com")
        .unwrap()
        .port(587)
        .credentials(creds)
        .build();
    
    let mut sent_count = 0;
    for (name, email) in &recipients {
        let email_msg = Message::builder()
            .from("Pickleball ELO <split@danesabo.com>".parse().unwrap())
            .to(format!("{} <{}>", name, email).parse().unwrap())
            .subject("🏓 Pickleball Session Summary")
            .header(lettre::message::header::ContentType::TEXT_HTML)
            .body(email_body.clone())
            .unwrap();
        
        match mailer.send(&email_msg) {
            Ok(_) => { sent_count += 1; }
            Err(e) => { eprintln!("Failed to send to {}: {}", email, e); }
        }
    }
    
    println!("📧 Sent session summary to {}/{} recipients", sent_count, recipients.len());
    
    // Mark session as sent
    sqlx::query("UPDATE sessions SET summary_sent = 1 WHERE id = ?")
        .bind(session_id)
        .execute(&state.pool)
        .await
        .ok();
    
    Ok(Redirect::to("/sessions"))
}

/// Daily summary page - shows today's matches and allows sending summary email
///
/// **Endpoint:** `GET /daily`
///
/// **Description:** Shows all matches from today with a preview of the daily summary email.
/// Same email goes to all participants.
#[derive(Deserialize)]
struct DailyQuery {
    date: Option<String>,
}

async fn daily_summary_handler(
    State(state): State<AppState>,
    Query(query): Query<DailyQuery>,
) -> Html<String> {
    // Default to today, allow override via ?date=YYYY-MM-DD
    let target_date = query.date.unwrap_or_else(|| {
        chrono::Local::now().format("%Y-%m-%d").to_string()
    });
    
    // Get all matches from the target date
    let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score, 
           strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as match_time
           FROM matches m
           WHERE date(m.timestamp) = ?
           ORDER BY m.timestamp"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build match list HTML with clear team display
    let mut matches_html = String::new();
    for (match_id, match_type, team1_score, team2_score, match_time) in &matches {
        let participants: Vec<(String, i32, f64)> = sqlx::query_as(
            r#"SELECT p.name, mp.team, mp.rating_change
               FROM match_participants mp
               JOIN players p ON mp.player_id = p.id
               WHERE mp.match_id = ?
               ORDER BY mp.team, p.name"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect();
        
        let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        
        let team1_won = team1_score > team2_score;
        let winner_style = "color:#28a745;font-weight:bold;";
        let loser_style = "color:#666;";
        
        matches_html.push_str(&format!(
            r#"<tr>
                <td>{}</td>
                <td><strong>{}</strong></td>
                <td style="{}">{}</td>
                <td style="text-align:center;">{}</td>
                <td style="{}">{}</td>
            </tr>"#,
            match_time, 
            match_type.to_uppercase(),
            if team1_won { winner_style } else { loser_style }, team1_names,
            format!("{} - {}", team1_score, team2_score),
            if !team1_won { winner_style } else { loser_style }, team2_names
        ));
    }
    
    // Get all players who played today with their daily ELO change
    let player_rating_changes: Vec<(String, String, f64, f64)> = sqlx::query_as(
        r#"SELECT p.name, COALESCE(p.email, '') as email,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'singles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as singles_change,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'doubles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as doubles_change
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE date(m.timestamp) = ?
           GROUP BY p.id, p.name
           ORDER BY singles_change + doubles_change DESC"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut players_html = String::new();
    for (name, _email, singles_change, doubles_change) in &player_rating_changes {
        let total = singles_change + doubles_change;
        let total_str = if total > 0.0 { format!("<span style='color:#28a745;font-weight:bold;'>+{:.0}</span>", total) }
            else if total < 0.0 { format!("<span style='color:#dc3545;font-weight:bold;'>{:.0}</span>", total) }
            else { "—".to_string() };
        
        players_html.push_str(&format!(
            "<tr><td>{}</td><td style='text-align:center;'>{}</td></tr>",
            name, total_str
        ));
    }
    
    // Get recipients (players with email)
    let recipients: Vec<&(String, String, f64, f64)> = player_rating_changes.iter()
        .filter(|(_, email, _, _)| !email.is_empty())
        .collect();
    
    let recipients_html: String = recipients.iter()
        .map(|(name, email, _, _)| format!(
            "<span style='display:inline-block;background:#f0f5ff;padding:4px 10px;border-radius:12px;margin:3px;'>{} &lt;{}&gt;</span>",
            name, email
        ))
        .collect();
    
    // Get top 5 for leaderboard preview
    let top_singles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let top_doubles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let singles_list: String = top_singles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    let _doubles_list: String = top_doubles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    // === ELO HISTORY CHART DATA (UNIFIED) ===
    let elo_history: Vec<(i64, String, String, f64)> = sqlx::query_as(
        r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str,
           p.name, mp.rating_after
           FROM matches m
           JOIN match_participants mp ON m.id = mp.match_id
           JOIN players p ON mp.player_id = p.id
           WHERE date(m.timestamp) = ?
           ORDER BY m.timestamp, p.name"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let colors = ["#003594", "#FFB81C", "#dc3545", "#28a745", "#6f42c1", "#fd7e14", "#20c997", "#e83e8c"];
    
    // Helper to build chart data from history
    fn build_chart_data(history: &[(i64, String, String, f64)], colors: &[&str]) -> (String, String) {
        let mut match_order: Vec<(i64, String)> = Vec::new();
        let mut seen: std::collections::HashSet<i64> = std::collections::HashSet::new();
        let mut player_ratings: std::collections::HashMap<(String, i64), f64> = std::collections::HashMap::new();
        let mut all_players: std::collections::HashSet<String> = std::collections::HashSet::new();
        
        for (match_id, time_str, name, rating) in history {
            if !seen.contains(match_id) {
                match_order.push((*match_id, time_str.clone()));
                seen.insert(*match_id);
            }
            player_ratings.insert((name.clone(), *match_id), *rating);
            all_players.insert(name.clone());
        }
        
        let mut sorted_players: Vec<String> = all_players.into_iter().collect();
        sorted_players.sort();
        
        let mut datasets = String::new();
        for (i, name) in sorted_players.iter().enumerate() {
            let color = colors[i % colors.len()];
            let mut last = 1500.0_f64;
            let points: String = match_order.iter()
                .map(|(mid, _)| {
                    if let Some(r) = player_ratings.get(&(name.clone(), *mid)) { last = *r; }
                    format!("{:.1}", last)
                })
                .collect::<Vec<_>>()
                .join(",");
            datasets.push_str(&format!(
                r#"{{"label":"{}","data":[{}],"borderColor":"{}","backgroundColor":"{}","tension":0.3,"fill":false}},"#,
                name, points, color, color
            ));
        }
        
        let labels: String = match_order.iter()
            .map(|(_, t)| format!("\"{}\"", t))
            .collect::<Vec<_>>()
            .join(",");
        
        (labels, datasets)
    }
    
    let (elo_labels, elo_datasets) = build_chart_data(&elo_history, &colors);
    
    // === PARTNER SYNERGY HEATMAP DATA ===
    // Calculate win rate for each partner pair (all time, for doubles)
    let synergy_data: Vec<(String, String, i64, i64)> = sqlx::query_as(
        r#"SELECT p1.name as player1, p2.name as player2,
           COUNT(*) as games_together,
           SUM(CASE WHEN 
               (mp1.team = 1 AND m.team1_score > m.team2_score) OR 
               (mp1.team = 2 AND m.team2_score > m.team1_score)
           THEN 1 ELSE 0 END) as wins_together
           FROM match_participants mp1
           JOIN match_participants mp2 ON mp1.match_id = mp2.match_id 
               AND mp1.player_id < mp2.player_id AND mp1.team = mp2.team
           JOIN players p1 ON mp1.player_id = p1.id
           JOIN players p2 ON mp2.player_id = p2.id
           JOIN matches m ON mp1.match_id = m.id
           WHERE m.match_type = 'doubles'
           GROUP BY p1.name, p2.name"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build symmetric matrix for heatmap
    let mut all_players: Vec<String> = synergy_data.iter()
        .flat_map(|(p1, p2, _, _)| vec![p1.clone(), p2.clone()])
        .collect::<std::collections::HashSet<_>>()
        .into_iter()
        .collect();
    all_players.sort();
    
    let mut synergy_matrix: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
    for (p1, p2, games, wins) in &synergy_data {
        let win_rate = if *games > 0 { *wins as f64 / *games as f64 } else { 0.0 };
        synergy_matrix.insert((p1.clone(), p2.clone()), win_rate);
        synergy_matrix.insert((p2.clone(), p1.clone()), win_rate); // Mirror
    }
    
    // Build heatmap HTML (CSS grid)
    let mut heatmap_html = format!(
        "<div style='display:grid;grid-template-columns:120px repeat({}, 1fr);gap:2px;font-size:12px;'>",
        all_players.len()
    );
    // Header row
    heatmap_html.push_str("<div></div>"); // Empty corner
    for p in &all_players {
        heatmap_html.push_str(&format!(
            "<div style='text-align:center;font-weight:bold;padding:5px;writing-mode:vertical-rl;transform:rotate(180deg);'>{}</div>",
            p.split_whitespace().next().unwrap_or(p) // First name only
        ));
    }
    // Data rows
    for p1 in &all_players {
        heatmap_html.push_str(&format!(
            "<div style='font-weight:bold;padding:5px;text-align:right;'>{}</div>",
            p1.split_whitespace().next().unwrap_or(p1)
        ));
        for p2 in &all_players {
            if p1 == p2 {
                heatmap_html.push_str("<div style='background:#333;'></div>"); // Diagonal
            } else if let Some(win_rate) = synergy_matrix.get(&(p1.clone(), p2.clone())) {
                // Color: red (0%) -> white (50%) -> green (100%)
                let (r, g, b) = if *win_rate >= 0.5 {
                    let t = (*win_rate - 0.5) * 2.0;
                    (((1.0 - t) * 255.0) as u8, 200, ((1.0 - t) * 255.0) as u8)
                } else {
                    let t = *win_rate * 2.0;
                    (255, ((t) * 200.0) as u8, ((t) * 255.0) as u8)
                };
                heatmap_html.push_str(&format!(
                    "<div style='background:rgb({},{},{});text-align:center;padding:8px;' title='{} + {}: {:.0}%'>{:.0}%</div>",
                    r, g, b, p1, p2, win_rate * 100.0, win_rate * 100.0
                ));
            } else {
                heatmap_html.push_str("<div style='background:#eee;text-align:center;padding:8px;'>—</div>");
            }
        }
    }
    heatmap_html.push_str("</div>");
    
    let send_button = if !matches.is_empty() && !recipients.is_empty() {
        format!(r#"
            <form method="POST" action="/daily/send?date={}" style="text-align: center; margin-top: 30px;"
                  onsubmit="return confirm('Send daily summary to {} player(s)?')">
                <button type="submit" class="btn btn-success" style="font-size: 18px; padding: 15px 40px;">
                    📧 Send Daily Summary
                </button>
            </form>
        "#, target_date, recipients.len())
    } else if matches.is_empty() {
        "<p style='text-align:center;color:#666;margin-top:20px;'>No matches recorded for this date.</p>".to_string()
    } else {
        "<p style='text-align:center;color:#dc3545;margin-top:20px;'>⚠️ No players with email addresses played today.</p>".to_string()
    };
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Daily Summary - Pickleball ELO</title>
        <style>{}</style>
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    </head>
    <body>
        <div class="container">
            <h1>🏓 Daily Summary</h1>
            {}
            
            <div style="text-align: center; margin-bottom: 20px;">
                <input type="date" id="datePicker" value="{}" 
                       style="padding: 10px; font-size: 16px; border-radius: 6px; border: 2px solid #003594;"
                       onchange="window.location.href='/daily?date=' + this.value">
            </div>
            
            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Matches</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Recipients</div>
                </div>
            </div>
            
            <h2>🎾 Today's Matches</h2>
            <table>
                <thead>
                    <tr>
                        <th>Time</th>
                        <th>Type</th>
                        <th>Team 1</th>
                        <th>Score</th>
                        <th>Team 2</th>
                    </tr>
                </thead>
                <tbody>
                    {}
                </tbody>
            </table>
            
            <h2>📈 Player Performance</h2>
            <table>
                <thead>
                    <tr>
                        <th>Player</th>
                        <th>ELO Δ</th>
                    </tr>
                </thead>
                <tbody>
                    {}
                </tbody>
            </table>
            
            <h2>📈 ELO Journey</h2>
            <div style="background:#fafafa;padding:15px;border-radius:8px;margin-bottom:20px;">
                <canvas id="eloChart" height="180"></canvas>
            </div>
            <script>
                new Chart(document.getElementById('eloChart'), {{
                    type: 'line',
                    data: {{ labels: [{}], datasets: [{}] }},
                    options: {{
                        responsive: true,
                        plugins: {{
                            title: {{ display: true, text: 'Unified ELO Rating', font: {{ size: 14 }} }},
                            legend: {{ position: 'bottom', labels: {{ boxWidth: 12, font: {{ size: 10 }} }} }}
                        }},
                        scales: {{ y: {{ title: {{ display: true, text: 'Rating' }} }} }}
                    }}
                }});
            </script>
            
            <h2>🤝 Partner Synergy (All-Time Doubles)</h2>
            <p style="color:#666;font-size:14px;margin-bottom:10px;">Win rate when partnered together. Green = good chemistry, Red = rough times.</p>
            {}
            
            <h2>📬 Recipients</h2>
            <div style="margin-bottom: 20px;">{}</div>
            
            <h2>📄 Email Preview</h2>
            <div style="border: 2px solid #003594; border-radius: 8px; padding: 20px; background: #fafafa;">
                <h3 style="color: #003594;">🏓 Pickleball Daily Summary - {}</h3>
                
                <h4 style="border-bottom: 2px solid #FFB81C; padding-bottom: 5px;">🎾 Match Results</h4>
                <table style="width:100%;">{}</table>
                
                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
                    <div>
                        <h4 style="border-bottom: 2px solid #FFB81C; padding-bottom: 5px;">📊 Top Players (Unified ELO)</h4>
                        {}
                    </div>
                </div>
            </div>
            
            {}
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html(), target_date, matches.len(), recipients.len(),
        matches_html, players_html, elo_labels, elo_datasets, heatmap_html, recipients_html, target_date, matches_html, singles_list, send_button);
    
    Html(html)
}

/// Send daily summary email to all participants
///
/// **Endpoint:** `POST /daily/send`
async fn send_daily_summary(
    State(state): State<AppState>,
    Query(query): Query<DailyQuery>,
) -> Result<Redirect, (StatusCode, String)> {
    let target_date = query.date.unwrap_or_else(|| {
        chrono::Local::now().format("%Y-%m-%d").to_string()
    });
    
    // Get all participants with email for the day
    let recipients: Vec<(String, String)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.email
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE date(m.timestamp) = ? AND p.email IS NOT NULL AND p.email != ''"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    
    if recipients.is_empty() {
        return Err((StatusCode::BAD_REQUEST, "No recipients with email addresses".to_string()));
    }
    
    // Get all matches from the target date
    let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score,
           strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as match_time
           FROM matches m
           WHERE date(m.timestamp) = ?
           ORDER BY m.timestamp"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build matches HTML for email
    let mut matches_email_html = String::new();
    for (match_id, match_type, team1_score, team2_score, match_time) in &matches {
        let participants: Vec<(String, i32)> = sqlx::query_as(
            r#"SELECT p.name, mp.team
               FROM match_participants mp
               JOIN players p ON mp.player_id = p.id
               WHERE mp.match_id = ?
               ORDER BY mp.team, p.name"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t)| *t == 2).collect();
        
        let team1_names: String = team1.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        let team2_names: String = team2.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        
        let (winner, loser, winner_score, loser_score) = if team1_score > team2_score {
            (&team1_names, &team2_names, team1_score, team2_score)
        } else {
            (&team2_names, &team1_names, team2_score, team1_score)
        };
        
        matches_email_html.push_str(&format!(
            "<tr><td>{}</td><td><strong>{}</strong></td><td>{} def. {}</td><td>{}-{}</td></tr>",
            match_time, match_type.to_uppercase(), winner, loser, winner_score, loser_score
        ));
    }
    
    // Get player ELO changes for the day
    let player_rating_changes: Vec<(String, f64, f64)> = sqlx::query_as(
        r#"SELECT p.name,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'singles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as singles_change,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'doubles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as doubles_change
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE date(m.timestamp) = ?
           GROUP BY p.id, p.name
           ORDER BY singles_change + doubles_change DESC"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut players_email_html = String::new();
    for (name, singles_change, doubles_change) in &player_rating_changes {
        let total = singles_change + doubles_change;
        let total_str = if total > 0.0 { format!("<span style='color:#28a745;font-weight:bold;'>+{:.0}</span>", total) }
            else if total < 0.0 { format!("<span style='color:#dc3545;font-weight:bold;'>{:.0}</span>", total) }
            else { "—".to_string() };
        
        players_email_html.push_str(&format!(
            "<tr><td>{}</td><td style='text-align:center;'>{}</td></tr>",
            name, total_str
        ));
    }
    
    // Build QuickChart URLs for email charts
    let colors_email = ["rgb(0,53,148)", "rgb(255,184,28)", "rgb(220,53,69)", "rgb(40,167,69)", "rgb(111,66,193)"];
    
    // Helper to build chart data for email
    fn build_email_chart(history: &[(i64, String, String, f64)], colors: &[&str], title: &str) -> String {
        let mut match_order: Vec<(i64, String)> = Vec::new();
        let mut seen: std::collections::HashSet<i64> = std::collections::HashSet::new();
        let mut player_ratings: std::collections::HashMap<(String, i64), f64> = std::collections::HashMap::new();
        let mut all_players: std::collections::HashSet<String> = std::collections::HashSet::new();
        
        for (match_id, time_str, name, rating) in history {
            if !seen.contains(match_id) {
                match_order.push((*match_id, time_str.clone()));
                seen.insert(*match_id);
            }
            player_ratings.insert((name.clone(), *match_id), *rating);
            all_players.insert(name.clone());
        }
        
        if match_order.is_empty() { return String::new(); }
        
        let mut sorted_players: Vec<String> = all_players.into_iter().collect();
        sorted_players.sort();
        
        let labels: String = match_order.iter()
            .map(|(_, t)| format!("\"{}\"", t))
            .collect::<Vec<_>>()
            .join(",");
        
        let mut datasets = String::new();
        for (i, name) in sorted_players.iter().enumerate() {
            let color = colors[i % colors.len()];
            let mut last = 1500.0_f64;
            let points: String = match_order.iter()
                .map(|(mid, _)| {
                    if let Some(r) = player_ratings.get(&(name.clone(), *mid)) { last = *r; }
                    format!("{:.0}", last)
                })
                .collect::<Vec<_>>()
                .join(",");
            let short_name = name.split_whitespace().next().unwrap_or(name);
            datasets.push_str(&format!(
                r#"{{"label":"{}","data":[{}],"borderColor":"{}","fill":false,"tension":0.3}},"#,
                short_name, points, color
            ));
        }
        
        let chart_config = format!(
            r#"{{"type":"line","data":{{"labels":[{}],"datasets":[{}]}},"options":{{"plugins":{{"title":{{"display":true,"text":"{}"}},"legend":{{"position":"bottom"}}}},"scales":{{"y":{{"title":{{"display":true,"text":"Rating"}}}}}}}}}}"#,
            labels, datasets.trim_end_matches(','), title
        );
        
        // URL encode the config
        let encoded = chart_config.replace(' ', "%20").replace('"', "%22").replace('#', "%23");
        format!("https://quickchart.io/chart?c={}&w=500&h=250&bkg=white", encoded)
    }
    
    // Get singles/doubles history for email charts
    let singles_email_history: Vec<(i64, String, String, f64)> = sqlx::query_as(
        r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str,
           p.name, mp.rating_after
           FROM matches m
           JOIN match_participants mp ON m.id = mp.match_id
           JOIN players p ON mp.player_id = p.id
           WHERE date(m.timestamp) = ? AND m.match_type = 'singles'
           ORDER BY m.timestamp, p.name"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let doubles_email_history: Vec<(i64, String, String, f64)> = sqlx::query_as(
        r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str,
           p.name, mp.rating_after
           FROM matches m
           JOIN match_participants mp ON m.id = mp.match_id
           JOIN players p ON mp.player_id = p.id
           WHERE date(m.timestamp) = ? AND m.match_type = 'doubles'
           ORDER BY m.timestamp, p.name"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let singles_chart_url = build_email_chart(&singles_email_history, &colors_email, "Singles Rating");
    let doubles_chart_url = build_email_chart(&doubles_email_history, &colors_email, "Doubles Rating");
    
    let charts_email_html = format!(
        r#"{}{}
        "#,
        if !singles_chart_url.is_empty() { format!(r#"<img src="{}" alt="Singles Rating Chart" style="width:100%;max-width:500px;margin-bottom:15px;">"#, singles_chart_url) } else { String::new() },
        if !doubles_chart_url.is_empty() { format!(r#"<img src="{}" alt="Doubles Rating Chart" style="width:100%;max-width:500px;">"#, doubles_chart_url) } else { String::new() }
    );
    
    // Get leaderboard data
    let top_singles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let top_doubles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let singles_html: String = top_singles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<tr><td>{} {}. {}</td><td style='text-align:right;'>{:.0}</td></tr>", medal, i+1, name, rating)
        })
        .collect();
    
    let doubles_html: String = top_doubles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<tr><td>{} {}. {}</td><td style='text-align:right;'>{:.0}</td></tr>", medal, i+1, name, rating)
        })
        .collect();
    
    // Partner synergy heatmap for email (as HTML table)
    let synergy_data: Vec<(String, String, i64, i64)> = sqlx::query_as(
        r#"SELECT p1.name as player1, p2.name as player2,
           COUNT(*) as games_together,
           SUM(CASE WHEN 
               (mp1.team = 1 AND m.team1_score > m.team2_score) OR 
               (mp1.team = 2 AND m.team2_score > m.team1_score)
           THEN 1 ELSE 0 END) as wins_together
           FROM match_participants mp1
           JOIN match_participants mp2 ON mp1.match_id = mp2.match_id 
               AND mp1.player_id < mp2.player_id AND mp1.team = mp2.team
           JOIN players p1 ON mp1.player_id = p1.id
           JOIN players p2 ON mp2.player_id = p2.id
           JOIN matches m ON mp1.match_id = m.id
           WHERE m.match_type = 'doubles'
           GROUP BY p1.name, p2.name"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    // Build email-safe heatmap table
    let mut all_players: Vec<String> = synergy_data.iter()
        .flat_map(|(p1, p2, _, _)| vec![p1.clone(), p2.clone()])
        .collect::<std::collections::HashSet<_>>()
        .into_iter()
        .collect();
    all_players.sort();
    
    let mut synergy_matrix: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
    for (p1, p2, games, wins) in &synergy_data {
        let win_rate = if *games > 0 { *wins as f64 / *games as f64 } else { 0.0 };
        synergy_matrix.insert((p1.clone(), p2.clone()), win_rate);
        synergy_matrix.insert((p2.clone(), p1.clone()), win_rate);
    }
    
    let mut heatmap_email = String::from("<table style='width:100%;border-collapse:collapse;font-size:12px;'><tr><td></td>");
    for p in &all_players {
        let short_name = p.split_whitespace().next().unwrap_or(p);
        heatmap_email.push_str(&format!("<td style='text-align:center;padding:5px;font-weight:bold;'>{}</td>", short_name));
    }
    heatmap_email.push_str("</tr>");
    for p1 in &all_players {
        let short_name = p1.split_whitespace().next().unwrap_or(p1);
        heatmap_email.push_str(&format!("<tr><td style='font-weight:bold;padding:5px;'>{}</td>", short_name));
        for p2 in &all_players {
            if p1 == p2 {
                heatmap_email.push_str("<td style='background:#333;'></td>");
            } else if let Some(win_rate) = synergy_matrix.get(&(p1.clone(), p2.clone())) {
                let (r, g, b) = if *win_rate >= 0.5 {
                    let t = (*win_rate - 0.5) * 2.0;
                    (((1.0 - t) * 255.0) as u8, 200, ((1.0 - t) * 255.0) as u8)
                } else {
                    let t = *win_rate * 2.0;
                    (255, ((t) * 200.0) as u8, ((t) * 255.0) as u8)
                };
                heatmap_email.push_str(&format!(
                    "<td style='background:rgb({},{},{});text-align:center;padding:8px;'>{:.0}%</td>",
                    r, g, b, win_rate * 100.0
                ));
            } else {
                heatmap_email.push_str("<td style='background:#eee;text-align:center;padding:8px;'>—</td>");
            }
        }
        heatmap_email.push_str("</tr>");
    }
    heatmap_email.push_str("</table>");
    
    let email_body = format!(r#"
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; background: #f5f5f5; padding: 20px;">
        <div style="max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 12px; border-top: 4px solid #FFB81C;">
            <h1 style="color: #003594; text-align: center;">🏓 Pickleball Daily Summary</h1>
            <p style="text-align: center; color: #666;">{}</p>
            
            <h2 style="color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 10px;">🎾 Match Results</h2>
            <table style="width: 100%; border-collapse: collapse;">
                <tr style="background: #003594; color: white;">
                    <th style="padding: 8px; text-align: left;">Time</th>
                    <th style="padding: 8px; text-align: left;">Type</th>
                    <th style="padding: 8px; text-align: left;">Result</th>
                    <th style="padding: 8px; text-align: left;">Score</th>
                </tr>
                {}
            </table>
            
            <h2 style="color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 10px; margin-top: 30px;">📈 ELO Journey</h2>
            <div style="text-align:center;">
                {}
            </div>
            
            <h2 style="color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 10px; margin-top: 30px;">📈 Today's Performance</h2>
            <table style="width: 100%; border-collapse: collapse;">
                <tr style="background: #003594; color: white;">
                    <th style="padding: 8px; text-align: left;">Player</th>
                    <th style="padding: 8px; text-align: center;">ELO Δ</th>
                </tr>
                {}
            </table>
            
            <h2 style="color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 10px; margin-top: 30px;">📊 Leaderboard</h2>
            <div style="display: flex; gap: 20px;">
                <div style="flex: 1;">
                    <h3 style="color: #003594; font-size: 14px;">Singles</h3>
                    <table style="width: 100%;">{}</table>
                </div>
                <div style="flex: 1;">
                    <h3 style="color: #003594; font-size: 14px;">Doubles</h3>
                    <table style="width: 100%;">{}</table>
                </div>
            </div>
            
            <h2 style="color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 10px; margin-top: 30px;">🤝 Partner Synergy</h2>
            <p style="color: #666; font-size: 12px; margin-bottom: 10px;">Win rate when partnered together (all-time doubles)</p>
            {}
            
            <p style="text-align: center; margin-top: 30px; color: #666; font-size: 12px;">
                Pickleball ELO Tracker · Hail to Pitt! 🐆
            </p>
        </div>
    </body>
    </html>
    "#, target_date, matches_email_html, charts_email_html, players_email_html, singles_html, doubles_html, heatmap_email);
    
    // Send emails
    use lettre::{Message, SmtpTransport, Transport};
    use lettre::transport::smtp::authentication::Credentials;
    
    let smtp_user = "split@danesabo.com";
    let smtp_pass = "Keep an eye 0ut 4 Split!";
    
    let creds = Credentials::new(smtp_user.to_string(), smtp_pass.to_string());
    
    let mailer = SmtpTransport::starttls_relay("smtppro.zoho.com")
        .unwrap()
        .port(587)
        .credentials(creds)
        .build();
    
    let mut sent_count = 0;
    for (name, email) in &recipients {
        let email_msg = Message::builder()
            .from("Pickleball ELO <split@danesabo.com>".parse().unwrap())
            .to(format!("{} <{}>", name, email).parse().unwrap())
            .subject(format!("🏓 Pickleball Daily Summary - {}", target_date))
            .header(lettre::message::header::ContentType::TEXT_HTML)
            .body(email_body.clone())
            .unwrap();
        
        match mailer.send(&email_msg) {
            Ok(_) => { sent_count += 1; }
            Err(e) => { eprintln!("Failed to send to {}: {}", email, e); }
        }
    }
    
    println!("📧 Sent daily summary ({}) to {}/{} players", target_date, sent_count, recipients.len());
    
    Ok(Redirect::to("/daily"))
}

/// Public read-only daily summary page (no forms, no admin features)
///
/// **Endpoint:** `GET /daily/public`
///
/// **Description:** Public-facing daily summary with Chart.js graphs, match results,
/// leaderboards, and partner synergy heatmap. No forms, no email features, no admin links.
/// Safe to embed on external websites.
async fn daily_public_handler(
    State(state): State<AppState>,
    Query(query): Query<DailyQuery>,
) -> Html<String> {
    let target_date = query.date.unwrap_or_else(|| {
        chrono::Local::now().format("%Y-%m-%d").to_string()
    });
    
    // Get all matches from the target date
    let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as(
        r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score, 
           strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as match_time
           FROM matches m
           WHERE date(m.timestamp) = ?
           ORDER BY m.timestamp"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut matches_html = String::new();
    for (match_id, match_type, team1_score, team2_score, match_time) in &matches {
        let participants: Vec<(String, i32, f64)> = sqlx::query_as(
            r#"SELECT p.name, mp.team, mp.rating_change
               FROM match_participants mp
               JOIN players p ON mp.player_id = p.id
               WHERE mp.match_id = ?
               ORDER BY mp.team, p.name"#
        )
        .bind(match_id)
        .fetch_all(&state.pool)
        .await
        .unwrap_or_default();
        
        let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect();
        let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect();
        
        let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::<Vec<_>>().join(" & ");
        
        let team1_won = team1_score > team2_score;
        let winner_style = "color:#28a745;font-weight:bold;";
        let loser_style = "color:#666;";
        
        matches_html.push_str(&format!(
            r#"<tr>
                <td>{}</td>
                <td><strong>{}</strong></td>
                <td style="{}">{}</td>
                <td style="text-align:center;">{}</td>
                <td style="{}">{}</td>
            </tr>"#,
            match_time, 
            match_type.to_uppercase(),
            if team1_won { winner_style } else { loser_style }, team1_names,
            format!("{} - {}", team1_score, team2_score),
            if !team1_won { winner_style } else { loser_style }, team2_names
        ));
    }
    
    // Player performance (without email column)
    let player_rating_changes: Vec<(String, f64, f64)> = sqlx::query_as(
        r#"SELECT p.name,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'singles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as singles_change,
           CAST(COALESCE(SUM(CASE WHEN m.match_type = 'doubles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as doubles_change
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           JOIN matches m ON mp.match_id = m.id
           WHERE date(m.timestamp) = ?
           GROUP BY p.id, p.name
           ORDER BY singles_change + doubles_change DESC"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut players_html = String::new();
    for (name, singles_change, doubles_change) in &player_rating_changes {
        let total = singles_change + doubles_change;
        let total_str = if total > 0.0 { format!("<span style='color:#28a745;font-weight:bold;'>+{:.0}</span>", total) }
            else if total < 0.0 { format!("<span style='color:#dc3545;font-weight:bold;'>{:.0}</span>", total) }
            else { "—".to_string() };
        
        players_html.push_str(&format!(
            "<tr><td>{}</td><td style='text-align:center;'>{}</td></tr>",
            name, total_str
        ));
    }
    
    // Leaderboards
    let top_singles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let top_doubles: Vec<(String, f64)> = sqlx::query_as(
        r#"SELECT DISTINCT p.name, p.singles_rating 
           FROM players p
           JOIN match_participants mp ON p.id = mp.player_id
           ORDER BY p.singles_rating DESC LIMIT 5"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let singles_list: String = top_singles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    let _doubles_list: String = top_doubles.iter().enumerate()
        .map(|(i, (name, rating))| {
            let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
            format!("<div>{}{}. {} - {:.0}</div>", medal, i+1, name, rating)
        })
        .collect();
    
    // Chart data (unified ELO - all match types)
    let elo_history: Vec<(i64, String, String, f64)> = sqlx::query_as(
        r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str,
           p.name, mp.rating_after
           FROM matches m
           JOIN match_participants mp ON m.id = mp.match_id
           JOIN players p ON mp.player_id = p.id
           WHERE date(m.timestamp) = ?
           ORDER BY m.timestamp, p.name"#
    )
    .bind(&target_date)
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let colors = ["#003594", "#FFB81C", "#dc3545", "#28a745", "#6f42c1", "#fd7e14", "#20c997", "#e83e8c"];
    
    fn build_chart_data_public(history: &[(i64, String, String, f64)], colors: &[&str]) -> (String, String) {
        let mut match_order: Vec<(i64, String)> = Vec::new();
        let mut seen: std::collections::HashSet<i64> = std::collections::HashSet::new();
        let mut player_ratings: std::collections::HashMap<(String, i64), f64> = std::collections::HashMap::new();
        let mut all_players: std::collections::HashSet<String> = std::collections::HashSet::new();
        
        for (match_id, time_str, name, rating) in history {
            if !seen.contains(match_id) {
                match_order.push((*match_id, time_str.clone()));
                seen.insert(*match_id);
            }
            player_ratings.insert((name.clone(), *match_id), *rating);
            all_players.insert(name.clone());
        }
        
        let mut sorted_players: Vec<String> = all_players.into_iter().collect();
        sorted_players.sort();
        
        let mut datasets = String::new();
        for (i, name) in sorted_players.iter().enumerate() {
            let color = colors[i % colors.len()];
            let mut last = 1500.0_f64;
            let points: String = match_order.iter()
                .map(|(mid, _)| {
                    if let Some(r) = player_ratings.get(&(name.clone(), *mid)) { last = *r; }
                    format!("{:.1}", last)
                })
                .collect::<Vec<_>>()
                .join(",");
            datasets.push_str(&format!(
                r#"{{"label":"{}","data":[{}],"borderColor":"{}","backgroundColor":"{}","tension":0.3,"fill":false}},"#,
                name, points, color, color
            ));
        }
        
        let labels: String = match_order.iter()
            .map(|(_, t)| format!("\"{}\"", t))
            .collect::<Vec<_>>()
            .join(",");
        
        (labels, datasets)
    }
    
    let (elo_labels, elo_datasets) = build_chart_data_public(&elo_history, &colors);
    
    // Partner synergy heatmap
    let synergy_data: Vec<(String, String, i64, i64)> = sqlx::query_as(
        r#"SELECT p1.name as player1, p2.name as player2,
           COUNT(*) as games_together,
           SUM(CASE WHEN 
               (mp1.team = 1 AND m.team1_score > m.team2_score) OR 
               (mp1.team = 2 AND m.team2_score > m.team1_score)
           THEN 1 ELSE 0 END) as wins_together
           FROM match_participants mp1
           JOIN match_participants mp2 ON mp1.match_id = mp2.match_id 
               AND mp1.player_id < mp2.player_id AND mp1.team = mp2.team
           JOIN players p1 ON mp1.player_id = p1.id
           JOIN players p2 ON mp2.player_id = p2.id
           JOIN matches m ON mp1.match_id = m.id
           WHERE m.match_type = 'doubles'
           GROUP BY p1.name, p2.name"#
    )
    .fetch_all(&state.pool)
    .await
    .unwrap_or_default();
    
    let mut all_players: Vec<String> = synergy_data.iter()
        .flat_map(|(p1, p2, _, _)| vec![p1.clone(), p2.clone()])
        .collect::<std::collections::HashSet<_>>()
        .into_iter()
        .collect();
    all_players.sort();
    
    let mut synergy_matrix: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new();
    for (p1, p2, games, wins) in &synergy_data {
        let win_rate = if *games > 0 { *wins as f64 / *games as f64 } else { 0.0 };
        synergy_matrix.insert((p1.clone(), p2.clone()), win_rate);
        synergy_matrix.insert((p2.clone(), p1.clone()), win_rate);
    }
    
    let mut heatmap_html = format!(
        "<div style='display:grid;grid-template-columns:120px repeat({}, 1fr);gap:2px;font-size:12px;'>",
        all_players.len()
    );
    heatmap_html.push_str("<div></div>");
    for p in &all_players {
        heatmap_html.push_str(&format!(
            "<div style='text-align:center;font-weight:bold;padding:5px;writing-mode:vertical-rl;transform:rotate(180deg);'>{}</div>",
            p.split_whitespace().next().unwrap_or(p)
        ));
    }
    for p1 in &all_players {
        heatmap_html.push_str(&format!(
            "<div style='font-weight:bold;padding:5px;text-align:right;'>{}</div>",
            p1.split_whitespace().next().unwrap_or(p1)
        ));
        for p2 in &all_players {
            if p1 == p2 {
                heatmap_html.push_str("<div style='background:#333;'></div>");
            } else if let Some(win_rate) = synergy_matrix.get(&(p1.clone(), p2.clone())) {
                let (r, g, b) = if *win_rate >= 0.5 {
                    let t = (*win_rate - 0.5) * 2.0;
                    (((1.0 - t) * 255.0) as u8, 200, ((1.0 - t) * 255.0) as u8)
                } else {
                    let t = *win_rate * 2.0;
                    (255, ((t) * 200.0) as u8, ((t) * 255.0) as u8)
                };
                heatmap_html.push_str(&format!(
                    "<div style='background:rgb({},{},{});text-align:center;padding:8px;' title='{} + {}: {:.0}%'>{:.0}%</div>",
                    r, g, b, p1, p2, win_rate * 100.0, win_rate * 100.0
                ));
            } else {
                heatmap_html.push_str("<div style='background:#eee;text-align:center;padding:8px;'>—</div>");
            }
        }
    }
    heatmap_html.push_str("</div>");
    
    let no_matches_msg = if matches.is_empty() {
        "<p style='text-align:center;color:#666;margin:40px 0;'>No matches recorded for this date.</p>"
    } else { "" };
    
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Pickleball Daily Summary - {}</title>
        <style>
            body {{ 
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; 
                background: linear-gradient(135deg, #003594 0%, #001a4d 100%);
                padding: 20px;
                margin: 0;
                min-height: 100vh;
            }}
            .container {{ 
                max-width: 900px; 
                margin: 0 auto; 
                background: white; 
                padding: 30px; 
                border-radius: 12px; 
                box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            }}
            h1 {{ color: #003594; text-align: center; }}
            h2 {{ color: #003594; border-bottom: 3px solid #FFB81C; padding-bottom: 10px; margin-top: 30px; }}
            table {{ width: 100%; border-collapse: collapse; margin: 15px 0; }}
            th, td {{ padding: 10px; text-align: left; border-bottom: 1px solid #eee; }}
            th {{ background: #003594; color: white; }}
            tr:hover {{ background: #f9f9f9; }}
            .stats-grid {{
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
                gap: 15px;
                margin: 20px 0;
            }}
            .stat-card {{
                background: #f0f5ff;
                padding: 20px;
                border-radius: 8px;
                text-align: center;
            }}
            .stat-value {{ font-size: 28px; font-weight: bold; color: #003594; }}
            .stat-label {{ font-size: 12px; color: #666; margin-top: 5px; }}
            .leaderboards {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 15px; }}
            .leaderboard {{ background: #f9f9f9; padding: 15px; border-radius: 8px; }}
            .leaderboard h3 {{ margin: 0 0 10px 0; color: #003594; font-size: 14px; }}
            .date-nav {{ text-align: center; margin-bottom: 20px; }}
            .date-nav a {{ 
                display: inline-block; padding: 8px 16px; margin: 0 5px;
                background: #003594; color: white; text-decoration: none; 
                border-radius: 6px; font-size: 14px;
            }}
            .date-nav a:hover {{ background: #FFB81C; color: #003594; }}
            .date-nav span {{ font-size: 18px; font-weight: bold; color: #003594; margin: 0 15px; }}
            .footer {{ text-align: center; margin-top: 30px; color: #666; font-size: 12px; }}
            @media (max-width: 600px) {{
                .leaderboards {{ grid-template-columns: 1fr; }}
            }}
        </style>
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    </head>
    <body>
        <div class="container">
            <h1>🏓 Pickleball Daily Summary</h1>
            
            <div class="date-nav">
                <a href="/daily/public?date={}">&larr; Prev</a>
                <span>{}</span>
                <a href="/daily/public?date={}">Next &rarr;</a>
            </div>
            
            {}
            
            <div class="stats-grid">
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Matches</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value">{}</div>
                    <div class="stat-label">Players</div>
                </div>
            </div>
            
            <h2>🎾 Match Results</h2>
            <table>
                <thead>
                    <tr><th>Time</th><th>Type</th><th>Team 1</th><th>Score</th><th>Team 2</th></tr>
                </thead>
                <tbody>{}</tbody>
            </table>
            
            <h2>📈 Player Performance</h2>
            <table>
                <thead>
                    <tr><th>Player</th><th>ELO Δ</th></tr>
                </thead>
                <tbody>{}</tbody>
            </table>
            
            <h2>📈 ELO Journey</h2>
            <div style="background:#fafafa;padding:15px;border-radius:8px;margin-bottom:20px;">
                <canvas id="eloChart" height="180"></canvas>
            </div>
            <script>
                if ([{}].length > 0) {{
                    new Chart(document.getElementById('eloChart'), {{
                        type: 'line',
                        data: {{ labels: [{}], datasets: [{}] }},
                        options: {{
                            responsive: true,
                            plugins: {{
                                title: {{ display: true, text: 'Unified ELO Rating', font: {{ size: 14 }} }},
                                legend: {{ position: 'bottom', labels: {{ boxWidth: 12, font: {{ size: 10 }} }} }}
                            }},
                            scales: {{ y: {{ title: {{ display: true, text: 'Rating' }} }} }}
                        }}
                    }});
                }}
            </script>
            
            <h2>📊 Current Leaderboard</h2>
            <div class="leaderboard" style="max-width: 400px; margin: 0 auto;">
                <h3>🏓 Top Players (Unified ELO)</h3>
                {}
            </div>
            
            <h2>🤝 Partner Synergy</h2>
            <p style="color:#666;font-size:13px;margin-bottom:10px;">Win rate when partnered together (all-time doubles)</p>
            {}
            
            <div class="footer">
                Pickleball ELO Tracker · ELO Rating System · Hail to Pitt! 🐆
            </div>
        </div>
    </body>
    </html>
    "#, 
        target_date,
        // Prev date
        (chrono::NaiveDate::parse_from_str(&target_date, "%Y-%m-%d")
            .unwrap_or_else(|_| chrono::Local::now().date_naive()) - chrono::Duration::days(1))
            .format("%Y-%m-%d"),
        target_date,
        // Next date
        (chrono::NaiveDate::parse_from_str(&target_date, "%Y-%m-%d")
            .unwrap_or_else(|_| chrono::Local::now().date_naive()) + chrono::Duration::days(1))
            .format("%Y-%m-%d"),
        no_matches_msg,
        matches.len(),
        player_rating_changes.len(),
        matches_html,
        players_html,
        elo_labels, elo_labels, elo_datasets,
        singles_list,
        heatmap_html
    );
    
    Html(html)
}

/// About page explaining the ELO rating system
///
/// **Endpoint:** `GET /about`
async fn about_handler() -> Html<String> {
    let html = format!(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>About - Pickleball ELO</title>
        <style>
            {}
            .content {{ max-width: 800px; margin: 0 auto; line-height: 1.8; }}
            .formula {{ background: #f5f5f5; padding: 15px 20px; border-radius: 8px; margin: 20px 0; font-family: monospace; overflow-x: auto; }}
            .example {{ background: #e8f4f8; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #003594; }}
            .section {{ margin-bottom: 40px; }}
            h2 {{ color: #003594; border-bottom: 2px solid #FFB81C; padding-bottom: 8px; }}
            h3 {{ color: #333; margin-top: 25px; }}
            ul {{ margin-left: 20px; }}
            li {{ margin-bottom: 8px; }}
            code {{ background: #e9e9e9; padding: 2px 6px; border-radius: 4px; font-family: monospace; }}
        </style>
    </head>
    <body>
        <div class="container">
            {}
            <div class="content">
                <h1>📊 How the Rating System Works</h1>
                <p style="color: #666; font-size: 1.1em;">A unified ELO system designed for recreational pickleball</p>
                
                <div class="section">
                    <h2>🎯 The Basics</h2>
                    <p><strong>One rating for everything.</strong> Whether you play singles or doubles, all matches contribute to a single ELO rating. Everyone starts at <strong>1500</strong>.</p>
                    <p>The system rewards skill and adapts to your performance. Beat higher-rated players? Big gains. Lose to lower-rated players? Bigger losses. It's that simple.</p>
                </div>
                
                <div class="section">
                    <h2>📈 Per-Point Scoring</h2>
                    <p>Unlike traditional ELO that only cares about winning or losing, our system considers <em>how</em> you played:</p>
                    <div class="formula">
                        Performance = Points Won ÷ Total Points
                    </div>
                    <p>This means:</p>
                    <ul>
                        <li>Winning <strong>11-2</strong> earns more than winning <strong>11-9</strong></li>
                        <li>Losing <strong>9-11</strong> costs less than losing <strong>2-11</strong></li>
                        <li>Close games = smaller rating swings for everyone</li>
                    </ul>
                    <div class="example">
                        <strong>Example:</strong> You win 11-7 against someone equally rated.<br>
                        Performance = 11 ÷ 18 = <strong>0.611</strong><br>
                        Expected (equal ratings) = <strong>0.500</strong><br>
                        You outperformed expectations → rating goes up!
                    </div>
                </div>
                
                <div class="section">
                    <h2>👥 The Doubles Problem</h2>
                    <p>In doubles, you have a partner. If your partner is much stronger than you, you'll probably win more — but how much credit should <em>you</em> get?</p>
                    <p>We solve this with the <strong>Effective Opponent</strong> formula:</p>
                    <div class="formula">
                        Effective Opponent = Opponent 1 + Opponent 2 − Teammate
                    </div>
                    <p>This creates a personalized opponent rating for each player. Here's how it works:</p>
                    <ul>
                        <li><strong>Strong teammate</strong> → Lower effective opponent → Less credit for winning, less blame for losing</li>
                        <li><strong>Weak teammate</strong> → Higher effective opponent → More credit for winning, more blame for losing</li>
                    </ul>
                    <div class="example">
                        <strong>Example:</strong> You're 1500 playing with a 1600 partner against two 1550 opponents.<br>
                        Your effective opponent = 1550 + 1550 − 1600 = <strong>1500</strong><br>
                        Your partner's effective opponent = 1550 + 1550 − 1500 = <strong>1600</strong><br><br>
                        If you win, you gain less than your partner because their effective opponent was harder!
                    </div>
                </div>
                
                <div class="section">
                    <h2>🔢 The Math</h2>
                    <p>For those who want the details:</p>
                    
                    <h3>Expected Performance</h3>
                    <div class="formula">
                        Expected = 1 / (1 + 10^((Opponent − You) / 400))
                    </div>
                    <p>This is the standard ELO expectation formula. If you're 200 points above your opponent, you're expected to score about 76% of points.</p>
                    
                    <h3>Rating Change</h3>
                    <div class="formula">
                        Δ Rating = K × (Actual − Expected)
                    </div>
                    <p>We use <strong>K = 32</strong>, which is standard for casual/club play. This means:</p>
                    <ul>
                        <li>Maximum gain/loss per match: ±32 points</li>
                        <li>Typical swing for competitive matches: ±10-15 points</li>
                        <li>Close match against equal opponent: ±2-5 points</li>
                    </ul>
                </div>
                
                <div class="section">
                    <h2>❓ Why This System?</h2>
                    <p>We tried Glicko-2 first (with rating deviation and volatility), but it was:</p>
                    <ul>
                        <li><strong>Confusing</strong> — nobody understood what "RD 150" meant</li>
                        <li><strong>Opaque</strong> — the math was hidden behind complexity</li>
                        <li><strong>Overkill</strong> — designed for chess with thousands of games, not rec pickleball</li>
                    </ul>
                    <p>Pure ELO with per-point scoring is:</p>
                    <ul>
                        <li><strong>Transparent</strong> — you can calculate changes by hand</li>
                        <li><strong>Fair</strong> — accounts for margin of victory and partner strength</li>
                        <li><strong>Simple</strong> — one number that goes up when you play well</li>
                    </ul>
                </div>
                
                <div class="section">
                    <h2>🏆 Rating Tiers</h2>
                    <table style="width: 100%; max-width: 400px;">
                        <tr><td><strong>Below 1400</strong></td><td>Developing</td></tr>
                        <tr><td><strong>1400-1500</strong></td><td>Intermediate</td></tr>
                        <tr><td><strong>1500-1600</strong></td><td>Solid</td></tr>
                        <tr><td><strong>1600-1700</strong></td><td>Strong</td></tr>
                        <tr><td><strong>1700+</strong></td><td>⭐ Rising Star</td></tr>
                        <tr><td><strong>1900+</strong></td><td>👑 Elite</td></tr>
                    </table>
                </div>
                
                <p style="text-align: center; margin-top: 40px; color: #666;">
                    <a href="/" class="btn">← Back to Home</a>
                </p>
            </div>
        </div>
    </body>
    </html>
    "#, COMMON_CSS, nav_html());
    
    Html(html)
}
