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

use rshogi::records::formats::kif::{parse_ki2_bytes, parse_kif_bytes};
use rshogi::records::formats::sbinpack::{
    chain_from_record, deserialize_file, serialize_file, SbinpackChain,
};
use rshogi::records::record::{GameRecord, MoveRecord};
use rshogi::types::Eval;
use std::env;
use std::fs;
use std::io;
use std::path::Path;

const DEFAULT_MAX_GAMES: usize = 1000;

fn is_kif_file(path: &Path) -> bool {
    path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| ext.eq_ignore_ascii_case("kif"))
}

fn walk_kif_files(
    dir: &Path,
    limit: usize,
    seen: &mut usize,
    handler: &mut impl FnMut(&Path) -> bool,
) -> io::Result<bool> {
    let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<Vec<_>, io::Error>>()?;
    entries.sort_by_key(|entry| entry.file_name());

    for entry in entries {
        if *seen >= limit {
            return Ok(false);
        }
        let path = entry.path();
        if path.is_dir() {
            if !walk_kif_files(&path, limit, seen, handler)? {
                return Ok(false);
            }
        } else if is_kif_file(&path) {
            *seen += 1;
            if !handler(&path) {
                return Ok(false);
            }
        }
    }
    Ok(true)
}

fn parse_record(bytes: &[u8]) -> Result<GameRecord, String> {
    match parse_kif_bytes(bytes) {
        Ok(record) => Ok(record),
        Err(err) => parse_ki2_bytes(bytes).map_err(|err2| format!("kif={err}; ki2={err2}")),
    }
}

fn with_filled_eval(record: &GameRecord) -> (GameRecord, usize) {
    let mut missing = 0usize;
    let mut moves: Vec<MoveRecord> = Vec::with_capacity(record.move_count());
    for mv in record.main_moves() {
        let mut cloned = mv.clone();
        if cloned.eval().is_none() {
            cloned.set_eval(Some(Eval::from_raw(0)));
            missing += 1;
        }
        moves.push(cloned);
    }
    let mut rebuilt = GameRecord::from_main_line_with_result(
        record.init_position_sfen().to_string(),
        moves,
        record.result(),
    )
    .expect("record rebuild should succeed");
    rebuilt.set_metadata(record.metadata().clone());
    (rebuilt, missing)
}

fn stem_score_from_record(record: &GameRecord) -> i16 {
    record.main_moves().next().and_then(|mv| mv.eval()).map(|eval| eval.raw()).unwrap_or(0)
}

fn chain_matches(left: &SbinpackChain, right: &SbinpackChain) -> bool {
    if left.stem.packed_sfen.data != right.stem.packed_sfen.data {
        return false;
    }
    if left.stem.score != right.stem.score
        || left.stem.best_move != right.stem.best_move
        || left.stem.ply_result != right.stem.ply_result
    {
        return false;
    }
    if left.moves.len() != right.moves.len() {
        return false;
    }
    for (l, r) in left.moves.iter().zip(right.moves.iter()) {
        if l.mv != r.mv || l.eval != r.eval {
            return false;
        }
    }
    true
}

fn main() -> io::Result<()> {
    let mut args = env::args().skip(1);
    let dir = args.next().unwrap_or_else(|| "_refs/data/kif/Suisho2mkif-1100k/2mkif".to_string());
    let max_games =
        args.next().and_then(|value| value.parse::<usize>().ok()).unwrap_or(DEFAULT_MAX_GAMES);

    let mut attempted = 0usize;
    let mut parsed_ok = 0usize;
    let mut filled_eval = 0usize;
    let mut encoded_ok = 0usize;
    let mut roundtrip_ok = 0usize;
    let mut parse_errors = 0usize;
    let mut encode_errors = 0usize;
    let mut roundtrip_errors = 0usize;

    let mut seen = 0usize;
    let mut handler = |path: &Path| -> bool {
        attempted += 1;
        let bytes = match fs::read(path) {
            Ok(bytes) => bytes,
            Err(_) => {
                parse_errors += 1;
                return true;
            }
        };
        let record = match parse_record(&bytes) {
            Ok(record) => record,
            Err(_) => {
                parse_errors += 1;
                return true;
            }
        };
        parsed_ok += 1;

        let (record, missing) = with_filled_eval(&record);
        filled_eval += missing;

        let stem_score = stem_score_from_record(&record);
        let chain = match chain_from_record(&record, stem_score) {
            Ok(chain) => chain,
            Err(_) => {
                encode_errors += 1;
                return true;
            }
        };
        encoded_ok += 1;

        let bytes = match serialize_file(std::slice::from_ref(&chain)) {
            Ok(bytes) => bytes,
            Err(_) => {
                encode_errors += 1;
                return true;
            }
        };
        let decoded = match deserialize_file(&bytes) {
            Ok(decoded) => decoded,
            Err(_) => {
                roundtrip_errors += 1;
                return true;
            }
        };
        if decoded.chains.len() != 1 || !chain_matches(&chain, &decoded.chains[0]) {
            roundtrip_errors += 1;
            return true;
        }
        roundtrip_ok += 1;
        true
    };

    let _ = walk_kif_files(Path::new(&dir), max_games, &mut seen, &mut handler)?;

    println!("sbinpack roundtrip summary");
    println!("  dir: {dir}");
    println!("  files scanned: {seen}");
    println!("  attempted: {attempted}");
    println!("  parsed: {parsed_ok} (errors: {parse_errors})");
    println!("  filled eval: {filled_eval}");
    println!("  encoded: {encoded_ok} (errors: {encode_errors})");
    println!("  roundtrip ok: {roundtrip_ok} (errors: {roundtrip_errors})");

    Ok(())
}
