#![allow(clippy::pedantic, clippy::nursery, clippy::cargo)]

use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};

use rshogi::board::movegen::{LegalAll, MoveListGen};
use rshogi::board::Position;
use rshogi::records::formats::{move_order_key, parse_csa_str, parse_ki2_bytes, parse_kif_bytes};
use rshogi::records::GameRecord;
use rshogi::types::square::flip;
use rshogi::types::{Color, Move32, Piece, PieceType, Square};

#[derive(Default)]
struct Stats {
    files: usize,
    moves: usize,
    missing_moves: usize,
    legal_moves_sum: usize,
    legal_moves_max: usize,
    index_counts: Vec<u64>,
    feature: FeatureStats,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum RecordKind {
    Csa,
    Kif,
    Ki2,
}

#[derive(Default)]
struct FeatureStats {
    drop_moves: u64,
    quiet_moves: u64,
    capture_moves: u64,
    promote_moves: u64,
    piece_counts: [u64; 15],
    drop_piece_counts: [u64; 15],
    to_rank_counts: [u64; 9],
    to_file_counts: [u64; 9],
    from_rank_counts: [u64; 9],
    from_file_counts: [u64; 9],
}

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
        print_usage();
        return;
    }

    let (paths, limit) = parse_args(&args);
    if paths.is_empty() {
        print_usage();
        return;
    }

    let mut files = Vec::new();
    let mut collected = 0usize;
    eprintln!("collecting files...");
    for path in paths {
        if collect_paths(&path, &mut files, limit, &mut collected) {
            break;
        }
    }
    files.sort();

    let mut stats = Stats::default();
    let total = files.len();
    let mut last_reported = 0u8;
    for (idx, path) in files.into_iter().enumerate() {
        if let Some(kind) = detect_kind(&path) {
            stats.files += 1;
            if let Err(err) = analyze_file(&path, kind, &mut stats) {
                eprintln!("skip {}: {err}", path.display());
            }
        }
        if total > 0 {
            let percent = (((idx + 1) * 100) / total) as u8;
            if percent >= last_reported.saturating_add(1) || idx + 1 == total {
                eprintln!("progress: {percent}% ({}/{})", idx + 1, total);
                last_reported = percent;
            }
        }
    }

    print_summary(&stats);
}

fn print_usage() {
    println!(
        "usage: cargo run --bin move_order_stats -- <files/dirs...>\n\
         options:\n\
           --n <num>   対象ファイル数の上限（先頭から）\n\
         \n\
         対応形式: .kif .ki2 .csa\n\
         ディレクトリは再帰的に探索します。"
    );
}

fn parse_args(args: &[String]) -> (Vec<PathBuf>, Option<usize>) {
    let mut paths = Vec::new();
    let mut limit = None;
    let mut idx = 0usize;
    while idx < args.len() {
        match args[idx].as_str() {
            "--n" => {
                if let Some(value) = args.get(idx + 1) {
                    if let Ok(parsed) = value.parse::<usize>() {
                        limit = Some(parsed);
                    }
                    idx += 2;
                } else {
                    idx += 1;
                }
            }
            _ => {
                paths.push(PathBuf::from(&args[idx]));
                idx += 1;
            }
        }
    }
    (paths, limit)
}

fn collect_paths(
    path: &Path,
    out: &mut Vec<PathBuf>,
    limit: Option<usize>,
    collected: &mut usize,
) -> bool {
    if path.is_file() {
        if is_supported_path(path) {
            out.push(path.to_path_buf());
            *collected += 1;
            if *collected % 1000 == 0 {
                eprintln!("collecting: {collected}");
            }
            if limit.is_some_and(|limit| *collected >= limit) {
                return true;
            }
        }
        return false;
    }
    if let Ok(entries) = fs::read_dir(path) {
        for entry in entries.flatten() {
            let entry_path = entry.path();
            if entry_path.is_dir() {
                if collect_paths(&entry_path, out, limit, collected) {
                    return true;
                }
            } else if is_supported_path(&entry_path) {
                out.push(entry_path);
                *collected += 1;
                if *collected % 1000 == 0 {
                    eprintln!("collecting: {collected}");
                }
                if limit.is_some_and(|limit| *collected >= limit) {
                    return true;
                }
            }
        }
    }
    false
}

fn detect_kind(path: &Path) -> Option<RecordKind> {
    let ext = path.extension().and_then(OsStr::to_str)?;
    match ext.to_ascii_lowercase().as_str() {
        "csa" => Some(RecordKind::Csa),
        "kif" => Some(RecordKind::Kif),
        "ki2" => Some(RecordKind::Ki2),
        _ => None,
    }
}

fn is_supported_path(path: &Path) -> bool {
    detect_kind(path).is_some()
}

fn analyze_file(path: &Path, kind: RecordKind, stats: &mut Stats) -> Result<(), String> {
    let data = fs::read(path).map_err(|err| err.to_string())?;
    let record = parse_record(kind, &data)?;
    analyze_record(&record, stats).map_err(|err| err.to_string())
}

fn parse_record(kind: RecordKind, data: &[u8]) -> Result<GameRecord, String> {
    match kind {
        RecordKind::Csa => {
            let text = std::str::from_utf8(data).map_err(|err| err.to_string())?;
            parse_csa_str(text).map_err(|err| err.to_string())
        }
        RecordKind::Kif => parse_kif_bytes(data).map_err(|err| err.to_string()),
        RecordKind::Ki2 => parse_ki2_bytes(data).map_err(|err| err.to_string()),
    }
}

fn analyze_record(record: &GameRecord, stats: &mut Stats) -> Result<(), &'static str> {
    let mut pos = Position::empty();
    pos.set_sfen(record.init_position_sfen()).map_err(|_| "invalid sfen")?;

    for mv_record in record.moves() {
        let mv16 = mv_record.mv();
        let mv = pos.move32_from_move(mv16);
        let legal = MoveListGen::<LegalAll>::new(&pos);
        let mut moves: Vec<Move32> = legal
            .iter()
            .copied()
            .filter_map(|mv| {
                let full = pos.move32_from_move(mv);
                full.is_normal().then_some(full)
            })
            .collect();
        moves.sort_by_key(|candidate| move_order_key(&pos, *candidate));
        stats.legal_moves_sum += moves.len();
        stats.legal_moves_max = stats.legal_moves_max.max(moves.len());
        stats.moves += 1;

        if let Some(index) = moves.iter().position(|candidate| *candidate == mv) {
            if stats.index_counts.len() <= index {
                stats.index_counts.resize(index + 1, 0);
            }
            stats.index_counts[index] += 1;
        } else {
            stats.missing_moves += 1;
        }

        update_feature_stats(&mut stats.feature, &pos, mv);
        pos.apply_move(mv16);
    }

    Ok(())
}

fn perspective_square(sq: Square, side: Color) -> Square {
    if side == Color::BLACK {
        sq
    } else {
        flip(sq)
    }
}

fn print_summary(stats: &Stats) {
    println!("files: {}", stats.files);
    println!("moves: {}", stats.moves);
    println!("missing moves: {}", stats.missing_moves);
    if stats.moves > 0 {
        let avg_legal = stats.legal_moves_sum as f64 / stats.moves as f64;
        println!("avg legal moves: {:.2}", avg_legal);
        println!("max legal moves: {}", stats.legal_moves_max);
    }

    if stats.index_counts.is_empty() {
        println!("no index data");
        return;
    }

    let total_hits: u64 = stats.index_counts.iter().sum();
    println!("indexed moves: {}", total_hits);

    let buckets = [
        (0usize, 0usize),
        (1, 7),
        (8, 15),
        (16, 31),
        (32, 63),
        (64, 127),
        (128, 255),
        (256, 511),
        (512, usize::MAX),
    ];

    println!("index buckets:");
    for (start, end) in buckets {
        let count = count_in_range(&stats.index_counts, start, end);
        let ratio =
            if total_hits == 0 { 0.0 } else { (count as f64) * 100.0 / (total_hits as f64) };
        if end == usize::MAX {
            println!("  [{start}..]: {count} ({ratio:.2}%)");
        } else {
            println!("  [{start}..{end}]: {count} ({ratio:.2}%)");
        }
    }

    let under_128 = count_in_range(&stats.index_counts, 0, 127);
    let under_256 = count_in_range(&stats.index_counts, 0, 255);
    println!(
        "varint hints: <128={} ({:.2}%), <256={} ({:.2}%)",
        under_128,
        pct(under_128, total_hits),
        under_256,
        pct(under_256, total_hits),
    );
    print_feature_summary(&stats.feature, stats.moves as u64);
}

fn count_in_range(counts: &[u64], start: usize, end: usize) -> u64 {
    let mut total = 0u64;
    if start >= counts.len() {
        return 0;
    }
    let last = if end == usize::MAX { counts.len() - 1 } else { end.min(counts.len() - 1) };
    for value in counts.iter().take(last + 1).skip(start) {
        total += *value;
    }
    total
}

fn pct(count: u64, total: u64) -> f64 {
    if total == 0 {
        0.0
    } else {
        (count as f64) * 100.0 / (total as f64)
    }
}

fn update_feature_stats(stats: &mut FeatureStats, pos: &Position, mv: Move32) {
    let is_drop = mv.is_drop();
    let is_capture = if is_drop {
        false
    } else {
        let to_piece = pos.piece_on(mv.to_sq());
        to_piece != Piece::NONE && to_piece.color() != pos.turn()
    };
    let is_promotion = mv.is_promotion();

    if is_drop {
        stats.drop_moves += 1;
    }
    if is_capture {
        stats.capture_moves += 1;
    } else {
        stats.quiet_moves += 1;
    }
    if is_promotion {
        stats.promote_moves += 1;
    }

    let piece_type = if is_drop {
        mv.dropped_piece().unwrap_or(PieceType::NONE)
    } else {
        pos.moved_piece_before(mv).piece_type()
    };
    if let Some(idx) = piece_index(piece_type) {
        stats.piece_counts[idx] += 1;
        if is_drop {
            stats.drop_piece_counts[idx] += 1;
        }
    }

    let to_sq = perspective_square(mv.to_sq(), pos.turn());
    update_square_counts(to_sq, &mut stats.to_rank_counts, &mut stats.to_file_counts);
    if !is_drop {
        let from_sq = perspective_square(mv.from_sq(), pos.turn());
        update_square_counts(from_sq, &mut stats.from_rank_counts, &mut stats.from_file_counts);
    }
}

fn update_square_counts(sq: Square, ranks: &mut [u64; 9], files: &mut [u64; 9]) {
    let file = sq.file().raw();
    let rank = sq.rank().raw();
    if (0..=8).contains(&rank) {
        ranks[rank as usize] += 1;
    }
    if (0..=8).contains(&file) {
        files[file as usize] += 1;
    }
}

fn piece_index(piece_type: PieceType) -> Option<usize> {
    match piece_type {
        PieceType::PAWN => Some(0),
        PieceType::LANCE => Some(1),
        PieceType::KNIGHT => Some(2),
        PieceType::SILVER => Some(3),
        PieceType::BISHOP => Some(4),
        PieceType::ROOK => Some(5),
        PieceType::GOLD => Some(6),
        PieceType::PRO_PAWN => Some(7),
        PieceType::PRO_LANCE => Some(8),
        PieceType::PRO_KNIGHT => Some(9),
        PieceType::PRO_SILVER => Some(10),
        PieceType::HORSE => Some(11),
        PieceType::DRAGON => Some(12),
        PieceType::KING => Some(13),
        _ => None,
    }
}

fn print_feature_summary(stats: &FeatureStats, total_moves: u64) {
    println!("feature summary:");
    println!(
        "  drop: {} ({:.2}%), quiet: {} ({:.2}%), capture: {} ({:.2}%), promote: {} ({:.2}%)",
        stats.drop_moves,
        pct(stats.drop_moves, total_moves),
        stats.quiet_moves,
        pct(stats.quiet_moves, total_moves),
        stats.capture_moves,
        pct(stats.capture_moves, total_moves),
        stats.promote_moves,
        pct(stats.promote_moves, total_moves),
    );

    println!("  piece counts:");
    print_piece_row("P", stats.piece_counts[0], total_moves);
    print_piece_row("L", stats.piece_counts[1], total_moves);
    print_piece_row("N", stats.piece_counts[2], total_moves);
    print_piece_row("S", stats.piece_counts[3], total_moves);
    print_piece_row("B", stats.piece_counts[4], total_moves);
    print_piece_row("R", stats.piece_counts[5], total_moves);
    print_piece_row("G", stats.piece_counts[6], total_moves);
    print_piece_row("+P", stats.piece_counts[7], total_moves);
    print_piece_row("+L", stats.piece_counts[8], total_moves);
    print_piece_row("+N", stats.piece_counts[9], total_moves);
    print_piece_row("+S", stats.piece_counts[10], total_moves);
    print_piece_row("+B", stats.piece_counts[11], total_moves);
    print_piece_row("+R", stats.piece_counts[12], total_moves);
    print_piece_row("K", stats.piece_counts[13], total_moves);

    println!("  drop piece counts:");
    print_piece_row("P", stats.drop_piece_counts[0], total_moves);
    print_piece_row("L", stats.drop_piece_counts[1], total_moves);
    print_piece_row("N", stats.drop_piece_counts[2], total_moves);
    print_piece_row("S", stats.drop_piece_counts[3], total_moves);
    print_piece_row("B", stats.drop_piece_counts[4], total_moves);
    print_piece_row("R", stats.drop_piece_counts[5], total_moves);
    print_piece_row("G", stats.drop_piece_counts[6], total_moves);

    print_square_summary("to_rank", &stats.to_rank_counts, total_moves);
    print_square_summary("to_file", &stats.to_file_counts, total_moves);
    print_square_summary("from_rank", &stats.from_rank_counts, total_moves);
    print_square_summary("from_file", &stats.from_file_counts, total_moves);
}

fn print_piece_row(label: &str, count: u64, total: u64) {
    println!("    {label}: {count} ({:.2}%)", pct(count, total));
}

fn print_square_summary(label: &str, counts: &[u64; 9], total: u64) {
    println!("  {label}:");
    for (idx, count) in counts.iter().enumerate() {
        let rank_or_file = idx + 1;
        println!("    {rank_or_file}: {count} ({:.2}%)", pct(*count, total));
    }
}
