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

use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;

use rshogi::board::{self, Move32List, Position};
use rshogi::movegen::generate_legal_all_move32;
use rshogi::types::{Bitboard, Color, Move32, PieceType, RepetitionState, Square};

const ROOT_MOVE: &str = "__root__";

const CHECK_SQUARE_TYPES: [(&str, PieceType); 14] = [
    ("pawn", PieceType::PAWN),
    ("lance", PieceType::LANCE),
    ("knight", PieceType::KNIGHT),
    ("silver", PieceType::SILVER),
    ("bishop", PieceType::BISHOP),
    ("rook", PieceType::ROOK),
    ("gold", PieceType::GOLD),
    ("king", PieceType::KING),
    ("pro_pawn", PieceType::PRO_PAWN),
    ("pro_lance", PieceType::PRO_LANCE),
    ("pro_knight", PieceType::PRO_KNIGHT),
    ("pro_silver", PieceType::PRO_SILVER),
    ("horse", PieceType::HORSE),
    ("dragon", PieceType::DRAGON),
];

#[derive(Debug, Clone)]
struct PositionInput {
    label: String,
    sfen: String,
}

fn load_positions(path: &PathBuf) -> Result<Vec<PositionInput>, Box<dyn std::error::Error>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut positions = Vec::new();

    for line in reader.lines() {
        let line = line?;
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let Some((label, sfen)) = trimmed.split_once(',') else {
            continue;
        };
        let label = label.trim();
        let sfen = sfen.trim();
        if !label.is_empty() && !sfen.is_empty() {
            positions.push(PositionInput { label: label.to_string(), sfen: sfen.to_string() });
        }
    }

    Ok(positions)
}

fn parse_args() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let mut positions_path: Option<PathBuf> = None;

    let mut args = env::args().skip(1);
    while let Some(arg) = args.next() {
        if arg.as_str() == "--positions" {
            let value = args.next().ok_or("--positions requires a value")?;
            positions_path = Some(PathBuf::from(value));
        }
    }

    Ok(positions_path.unwrap_or_else(|| PathBuf::from("data/bench/diverse_positions.csv")))
}

fn push_bitboard_columns(columns: &mut Vec<String>, bb: Bitboard) {
    let [p0, p1] = bb.parts();
    columns.push(p0.to_string());
    columns.push(p1.to_string());
}

fn attacked_by_color_with_king_mask(pos: &Position, by_color: Color) -> Bitboard {
    let king_sq = pos.king_square(by_color.flip());
    if king_sq.is_none() {
        return Bitboard::EMPTY;
    }

    let mut out = Bitboard::EMPTY;
    for sq in Square::iter() {
        if pos.is_attacked_by_color_with_king(by_color, sq, king_sq) {
            out.set(sq);
        }
    }
    out
}

fn pawn_drop_probe_masks(pos: &Position) -> (Bitboard, Bitboard) {
    let candidates = pos.check_squares(PieceType::PAWN);
    let mut legal = Bitboard::EMPTY;

    for to in &candidates {
        let drop = Move32::drop(PieceType::PAWN, to, pos.turn());
        if pos.is_legal_move32(drop) {
            legal.set(to);
        }
    }

    (candidates, legal)
}

fn legal_moves(pos: &Position) -> Vec<Move32> {
    let mut legal = Move32List::new();
    generate_legal_all_move32(pos, &mut legal);
    legal.iter().copied().collect()
}

fn gives_check_summary(pos: &Position, legal_moves: &[Move32]) -> (usize, String) {
    let mut moves = Vec::new();
    for &mv in legal_moves {
        if pos.gives_check(mv) {
            moves.push(mv.to_usi());
        }
    }
    let count = moves.len();
    (count, moves.join(" "))
}

const fn repetition_state_code(state: RepetitionState) -> i32 {
    match state {
        RepetitionState::None => 0,
        RepetitionState::Win => 1,
        RepetitionState::Lose => 2,
        RepetitionState::Draw => 3,
        RepetitionState::Superior => 4,
        RepetitionState::Inferior => 5,
    }
}

fn build_header() -> Vec<String> {
    let mut header = vec![
        "label".to_string(),
        "move".to_string(),
        "sfen".to_string(),
        "in_check".to_string(),
        "attack_b_wo_king_p0".to_string(),
        "attack_b_wo_king_p1".to_string(),
        "attack_w_wo_king_p0".to_string(),
        "attack_w_wo_king_p1".to_string(),
        "pawn_drop_cand_p0".to_string(),
        "pawn_drop_cand_p1".to_string(),
        "pawn_drop_legal_p0".to_string(),
        "pawn_drop_legal_p1".to_string(),
        "gives_check_count".to_string(),
        "gives_check_moves".to_string(),
        "key_low".to_string(),
        "key_high".to_string(),
        "board_key_low".to_string(),
        "board_key_high".to_string(),
        "hand_key_low".to_string(),
        "hand_key_high".to_string(),
        "turn".to_string(),
        "hand_black_bits".to_string(),
        "hand_white_bits".to_string(),
        "state_hand_bits".to_string(),
        "plies_from_null".to_string(),
        "continuous_check_black".to_string(),
        "continuous_check_white".to_string(),
        "repetition_counter".to_string(),
        "repetition_distance".to_string(),
        "repetition_times".to_string(),
        "repetition_type".to_string(),
        "checkers_p0".to_string(),
        "checkers_p1".to_string(),
        "blockers_black_p0".to_string(),
        "blockers_black_p1".to_string(),
        "blockers_white_p0".to_string(),
        "blockers_white_p1".to_string(),
        "pinners_black_p0".to_string(),
        "pinners_black_p1".to_string(),
        "pinners_white_p0".to_string(),
        "pinners_white_p1".to_string(),
    ];

    for (name, _) in CHECK_SQUARE_TYPES {
        header.push(format!("cs_{name}_p0"));
        header.push(format!("cs_{name}_p1"));
    }

    header
}

#[allow(clippy::too_many_lines)]
fn build_row(label: &str, move_label: &str, pos: &Position) -> Vec<String> {
    let attack_black = attacked_by_color_with_king_mask(pos, Color::BLACK);
    let attack_white = attacked_by_color_with_king_mask(pos, Color::WHITE);
    let (pawn_drop_candidates, pawn_drop_legal) = pawn_drop_probe_masks(pos);
    let legal = legal_moves(pos);
    let (gives_check_count, gives_check_moves) = gives_check_summary(pos, &legal);

    let state = pos.state_stack().current();
    let key = pos.key();
    let board_key = pos.board_key();
    let hand_key = pos.hand_key();

    let mut row = Vec::with_capacity(80);
    row.push(label.to_string());
    row.push(move_label.to_string());
    row.push(pos.to_sfen(None));
    row.push((!pos.checkers().is_empty() as u8).to_string());
    push_bitboard_columns(&mut row, attack_black);
    push_bitboard_columns(&mut row, attack_white);
    push_bitboard_columns(&mut row, pawn_drop_candidates);
    push_bitboard_columns(&mut row, pawn_drop_legal);
    row.push(gives_check_count.to_string());
    row.push(gives_check_moves);
    row.push(key.low_u64().to_string());
    row.push(key.high_u64().to_string());
    row.push(board_key.low_u64().to_string());
    row.push(board_key.high_u64().to_string());
    row.push(hand_key.low_u64().to_string());
    row.push(hand_key.high_u64().to_string());
    row.push(pos.turn().to_index().to_string());
    row.push(pos.hand(Color::BLACK).bits().to_string());
    row.push(pos.hand(Color::WHITE).bits().to_string());
    row.push(state.hand.bits().to_string());
    row.push(state.plies_from_null.to_string());
    row.push(state.continuous_check[Color::BLACK.to_index()].to_string());
    row.push(state.continuous_check[Color::WHITE.to_index()].to_string());
    row.push(state.repetition_counter.to_string());
    row.push(state.repetition_distance.to_string());
    row.push(state.repetition_times.to_string());
    row.push(repetition_state_code(state.repetition_type).to_string());
    push_bitboard_columns(&mut row, pos.checkers());
    push_bitboard_columns(&mut row, pos.blockers_for_king(Color::BLACK));
    push_bitboard_columns(&mut row, pos.blockers_for_king(Color::WHITE));
    push_bitboard_columns(&mut row, pos.pinners(Color::BLACK));
    push_bitboard_columns(&mut row, pos.pinners(Color::WHITE));

    for (_, piece_type) in CHECK_SQUARE_TYPES {
        push_bitboard_columns(&mut row, pos.check_squares(piece_type));
    }

    row
}

fn print_row(row: Vec<String>) {
    println!("{}", row.join(","));
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let positions_path = parse_args()?;
    let positions = load_positions(&positions_path)?;
    if positions.is_empty() {
        return Err("no positions loaded from CSV".into());
    }

    println!("engine,rshogi");
    println!("{}", build_header().join(","));

    for input in positions {
        let position =
            board::position_from_sfen(&input.sfen).map_err(|e| format!("invalid sfen: {e}"))?;
        print_row(build_row(&input.label, ROOT_MOVE, &position));

        for mv in legal_moves(&position) {
            let mut child = position.clone();
            child.apply_move32(mv);
            print_row(build_row(&input.label, &mv.to_usi(), &child));
        }
    }

    Ok(())
}
