use chess_engine::engine::{Engine, SearchResult};
use chess_engine::position::Position;
use chess_engine::types::{Board, Difficulty, GameState, MoveData, Side};
use rand::Rng;
use std::io::{self, Write};

const OPENING_BOOK_PATH: &str = "opening_books/lpb-allbook.bin";

fn format_with_commas(n: u64) -> String {
    let s = n.to_string();
    let mut result = String::new();
    let len = s.len();

    for (i, ch) in s.chars().enumerate() {
        if i > 0 && (len - i).is_multiple_of(3) {
            result.push(',');
        }
        result.push(ch);
    }

    result
}

struct CommandLineInterface {
    engine: Engine,
    display_enabled: bool,
    flip: bool,
}

impl CommandLineInterface {
    fn new() -> Self {
        let engine = Engine::new(
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(OPENING_BOOK_PATH),
            None,
        );

        Self {
            engine,
            display_enabled: true,
            flip: false,
        }
    }

    fn show_help(&self) {
        println!("\n======================= INFORMATION ======================");
        println!("h or help - Displays help on the commands");
        println!("d and D   - Display board and toggle display setting");
        println!("moves     - Displays of list of possible moves");
        println!("fen       - Displays a FEN string for the current position");
        println!("hash      - Displays hash key for the current position");
        println!("q or quit - Quits the program");
        println!("================= CONTROLLING THE ENGINE =================");
        println!("go        - Starts the engine from the current position");
        println!("new       - Starts a new game");
        println!("p or play - The computer plays a move");
        println!("off       - Turns the computer player off");
        println!("switch    - Switches sides");
        println!("undo      - Takes back the last move");
        println!("===================== CONFIGURATION ======================");
        println!("fen <FEN>    - Loads a FEN string");
        println!("f            - Flips the board");
        println!("b or book    - Toggles opening book usage");
        println!("difficulty   - Sets the difficulty level");
        println!("sd <depth>   - Sets the maximum search depth");
        println!("st <seconds> - Sets the time limit per move in seconds");
        println!("sn <nodes>   - Sets the maximum search nodes");
    }

    fn display_board(&self) {
        if self.display_enabled {
            self.engine.position.display_board(self.flip);
        }
    }

    fn print_result(&mut self, result: GameState) {
        match result {
            GameState::InProgress => {}
            GameState::Checkmate(winner) => {
                self.display_board();
                println!("\nGAME OVER");

                if winner == Side::White {
                    println!("{{White mates}}");
                } else {
                    println!("{{Black mates}}");
                }

                self.engine.new_game();
            }
            GameState::Stalemate => {
                println!("{{Stalemate}}");
                self.engine.new_game();
            }
            GameState::DrawByRepetition => {
                println!("{{Draw by repetition}}");
                self.engine.new_game();
            }
            GameState::DrawByFiftyMoveRule => {
                println!("{{Draw by fifty move rule}}");
                self.engine.new_game();
            }
            GameState::DrawByInsufficientMaterial => {
                println!("{{Stalemate}}");
                self.engine.new_game();
            }
        }
    }

    fn run_main_loop(&mut self) {
        self.display_board();

        loop {
            println!("\n-----------------------------------------------");
            println!(
                "*   Ply: {} | Level: {} | To move: {:?}   *",
                self.engine.position.ply_from_start_of_game,
                self.engine
                    .difficulty
                    .map(|d| d.name())
                    .unwrap_or("Not set"),
                self.engine.position.side
            );
            println!("-----------------------------------------------");

            // Computer's turn
            if self.engine.computer_side == Some(self.engine.position.side) {
                println!("\nComputer is thinking...");
                let has_legal_moves = self.make_computer_move();
                let game_result = self.engine.position.get_game_state();

                if has_legal_moves {
                    self.print_result(game_result);
                } else {
                    println!("(No legal moves)");
                    self.engine.computer_side = None;
                }

                self.display_board();

                continue;
            }

            print!("\nFrom square OR command > ");
            io::stdout().flush().unwrap();

            let mut input = String::new();
            match io::stdin().read_line(&mut input) {
                Ok(0) => return, // EOF
                Ok(_) => {}
                Err(_) => return,
            }

            let command = input.trim();

            // COMMANDS WITHOUT PARAMETERS
            match command {
                "d" => {
                    self.engine.position.display_board(self.flip);
                    continue;
                }
                "D" => {
                    self.display_enabled = !self.display_enabled;

                    if self.display_enabled {
                        println!("\nBoard display enabled");
                        self.display_board();
                    } else {
                        println!("\nBoard display disabled");
                    }
                    continue;
                }
                "f" => {
                    self.flip = !self.flip;
                    self.display_board();
                    continue;
                }
                "b" | "book" => {
                    match self.engine.book {
                        Some(_) => {
                            self.engine.book = None;
                            println!("\nOpening book disabled");
                        }
                        None => match self.engine.load_opening_book(OPENING_BOOK_PATH) {
                            Ok(_) => println!("\nOpening book enabled"),
                            Err(e) => println!("\nFailed to enable opening book: {}", e),
                        },
                    };
                    continue;
                }
                "difficulty" => {
                    self.handle_difficulty_command();
                    continue;
                }
                "go" => {
                    self.handle_go_command();
                    continue;
                }
                "h" | "help" => {
                    self.show_help();
                    continue;
                }
                "fen" => {
                    println!("\n{}", self.engine.position.to_fen());
                    continue;
                }
                "hash" => {
                    println!(
                        "\nHash key: {:016X}",
                        self.engine.position.board.hash.current_key
                    );
                    continue;
                }
                "moves" => {
                    println!();
                    for (index, move_str) in
                        self.engine.position.get_legal_moves().iter().enumerate()
                    {
                        print!("{} ", move_str);
                        if (index + 1) % 8 == 0 {
                            println!();
                        }
                    }
                    println!();
                    continue;
                }
                "new" => {
                    self.engine.new_game();
                    self.display_board();
                    continue;
                }
                "p" | "play" => {
                    self.engine.computer_side = Some(self.engine.position.side);
                    continue;
                }
                "off" => {
                    self.engine.computer_side = None;
                    continue;
                }
                "q" | "quit" => {
                    println!("\nGoodbye!\n");
                    break;
                }
                "switch" => {
                    self.engine.position.side = self.engine.position.side.opponent();
                    continue;
                }
                "undo" => {
                    if self.engine.position.ply_from_start_of_game == 0 {
                        println!("\nNo moves to undo");
                        continue;
                    }
                    self.engine.computer_side = None;
                    self.engine.position.take_back_move();
                    self.display_board();
                    continue;
                }
                _ => {}
            }

            // COMMANDS WITH PARAMETERS
            if let Some(fen_str) = command.strip_prefix("fen ") {
                println!();
                match Position::from_fen(fen_str) {
                    Ok(position) => {
                        self.engine.position = position;
                        println!("FEN loaded successfully");
                        self.display_board();
                    }
                    Err(e) => println!("Error loading FEN: {}", e),
                }
                continue;
            }

            if command.starts_with("sd ") {
                if let Ok(depth) = command[3..].parse::<u16>() {
                    self.engine.search_settings.max_depth = depth;
                    println!("\nSearch maximum search depth set to {}", depth);
                }
                continue;
            }

            if command.starts_with("st ") {
                if let Ok(time) = command[3..].parse::<u64>() {
                    let time_in_ms = time * 1000;
                    self.engine.search_settings.movetime = Some(time_in_ms);
                    println!("\nSearch time set to {} seconds", time);
                }
                continue;
            }

            if command.starts_with("sn ") {
                if let Ok(nodes) = command[3..].parse::<usize>() {
                    self.engine.search_settings.max_nodes = Some(nodes);
                    println!("\nMaximum search nodes set to {}", nodes);
                }
                continue;
            }

            // PARSE "FROM" AND THEN "TO" SQUARE
            if command.len() < 2 {
                println!("\nINVALID COMMAND!");
                continue;
            }

            let from_square = Self::parse_square(command[..2].trim());
            if from_square.is_none() {
                println!("\nINVALID FROM SQUARE!");
                continue;
            }
            let from_square = from_square.unwrap();

            let mut to_input = String::new();

            let cleaned_command = command.replace(" ", "");

            match cleaned_command.len() {
                // Need to prompt for "to" square
                2 => {
                    // Get to square
                    print!("             To square > ");
                    io::stdout().flush().unwrap();

                    match io::stdin().read_line(&mut to_input) {
                        Ok(0) => return,
                        Ok(_) => {}
                        Err(_) => return,
                    }

                    println!();
                }
                // "to" square is included in command
                4 => {
                    to_input = cleaned_command[2..].to_string();
                }
                _ => {
                    println!("\nINVALID COMMAND!");
                    continue;
                }
            }

            let to_square = Self::parse_square(to_input.trim());
            if to_square.is_none() {
                println!("\nINVALID TO SQUARE!");
                continue;
            }
            let to_square = to_square.unwrap();

            // Construct move string and try to make the move
            let move_str = format!(
                "{}{}{}{}",
                ((from_square % 8) as u8 + b'a') as char,
                ((from_square / 8) as u8 + b'1') as char,
                ((to_square % 8) as u8 + b'a') as char,
                ((to_square / 8) as u8 + b'1') as char
            );

            if let Some(move_idx) = self.engine.position.parse_move_string(&move_str) {
                if let Some(mv) = self.engine.position.move_list[move_idx] {
                    if !self.engine.position.make_move(mv.from, mv.to, mv.promote) {
                        println!("\nILLEGAL MOVE!");
                        continue;
                    }

                    let game_result = self.engine.position.get_game_state();
                    self.print_result(game_result);
                    self.display_board();
                } else {
                    panic!("Move found in move list, but is `None`");
                }
            } else {
                println!("\nILLEGAL MOVE!");
            }
        }
    }

    fn handle_go_command(&mut self) {
        println!("\nChoose your side:");
        println!("1. White");
        println!("2. Black");
        println!("3. Random");
        print!("\nEnter choice (1-3) > ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(_) => {}
            Err(_) => return,
        }

        println!();

        let choice = input.trim();
        let player_side = match choice {
            "1" => Side::White,
            "2" => Side::Black,
            "3" => {
                let side = match rand::thread_rng().gen_bool(0.5) {
                    true => Side::White,
                    false => Side::Black,
                };
                println!("You are playing as {:?}", side);
                side
            }
            _ => {
                println!("Invalid choice. Defaulting to White.");
                Side::White
            }
        };

        self.engine.computer_side = Some(player_side.opponent());
    }

    fn handle_difficulty_command(&mut self) {
        println!("\nChoose difficulty level:");
        Difficulty::iter().enumerate().for_each(|(i, d)| {
            println!("{}. {:<8} (depth {})", i + 1, d.name(), d.max_depth());
        });
        print!("\nEnter choice (1-6) > ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(_) => {}
            Err(_) => return,
        }

        println!();

        let choice = input.trim();
        let difficulty = match choice {
            "1" => Difficulty::Beginner,
            "2" => Difficulty::Easy,
            "3" => Difficulty::Medium,
            "4" => Difficulty::Hard,
            "5" => Difficulty::Expert,
            "6" => Difficulty::Master,
            _ => {
                println!("Invalid choice. Difficulty not changed.");
                return;
            }
        };

        self.engine.difficulty = Some(difficulty);
        self.engine.search_settings.max_depth = difficulty.max_depth() as u16;

        println!(
            "Difficulty set to {} (max depth: {})",
            difficulty.name(),
            difficulty.max_depth()
        );
    }

    fn display_search_statistics(&self, result: &SearchResult) {
        let nodes_per_second = match result.time_ms {
            0 => 0, // Avoid division by zero
            ms => ((result.nodes as f64 / ms as f64) * 1000.0) as u64,
        };

        let q_nodes = result.qnodes;
        let total_nodes = result.nodes;
        let main_nodes = total_nodes.saturating_sub(q_nodes);
        let q_percent = if total_nodes > 0 {
            (q_nodes as f64 / total_nodes as f64 * 100.0) as u64
        } else {
            0
        };

        let beta_cutoffs = self.engine.position.beta_cutoffs;
        let cutoff_rate = if main_nodes > 0 {
            (beta_cutoffs as f64 / main_nodes as f64 * 100.0) as u64
        } else {
            0
        };

        if !result.from_book && !result.principal_variation.is_empty() {
            print!("  PV:  ");
            for (i, MoveData { from, to, promote }) in result.principal_variation.iter().enumerate()
            {
                if i > 0 {
                    print!(" ");
                }
                print!("{}", Board::move_to_uci_string(*from, *to, *promote, false));

                if i >= 7 {
                    if result.principal_variation.len() > 8 {
                        print!(" ...");
                    }
                    break;
                }
            }

            let padding = 51usize.saturating_sub(result.principal_variation.len().min(8) * 6);
            println!("{}", " ".repeat(padding));
        }

        if !result.from_book {
            // Display comprehensive statistics
            println!("\n┌─────────────────────── SEARCH STATISTICS ───────────────────────┐");
            println!(
                "│ Time:        {:>9} ms  │  Depth:  {:>4}     Quiescence: {:>3}  │",
                format_with_commas(result.time_ms),
                result.depth,
                self.engine.position.max_depth_reached - result.depth as usize
            );
            println!(
                "│ Nodes:       {:>12}  │  Qui-Nodes:    {:>12} ({}%)  │",
                format_with_commas(total_nodes as u64),
                format_with_commas(q_nodes as u64),
                q_percent
            );
            println!(
                "│ NPS:         {:>12}  │  β-Cutoffs:    {:>12} ({}%)  │",
                format_with_commas(nodes_per_second),
                format_with_commas(beta_cutoffs as u64),
                cutoff_rate
            );

            println!("└─────────────────────────────────────────────────────────────────┘");
        }
    }

    fn make_computer_move(&mut self) -> bool {
        println!("\n┌───────┬──────────────┬──────────┬────────────────────┐");
        println!("│ DEPTH │    NODES     │  SCORE   │     BEST MOVE      │");
        println!("├───────┼──────────────┼──────────┼────────────────────┤");

        let result = self
            .engine
            .think(Some(|depth, score, position: &mut Position| {
                print!(
                    "│ {:>5} │ {:>12} │ {:>8} │ ",
                    depth,
                    format_with_commas(position.nodes as u64),
                    score
                );

                if let Some(move_) = position
                    .pv_table
                    .get(0)
                    .and_then(|ply| ply.get(0))
                    .and_then(|&m| m)
                {
                    print!(
                        "{:^18} ",
                        Board::move_to_uci_string(move_.from, move_.to, move_.promote, true)
                    );
                } else {
                    print!("{:^18} ", "");
                }

                println!("│");
                std::io::Write::flush(&mut std::io::stdout()).unwrap();
            }));

        println!("└───────┴──────────────┴──────────┴────────────────────┘");

        let (from, to, promote) =
            if let (Some(from), Some(to)) = (result.best_move_from, result.best_move_to) {
                (from, to, result.best_move_promote)
            } else {
                return false;
            };

        self.engine.position.make_move(from, to, promote);
        self.display_search_statistics(&result);

        println!(
            "\nComputer plays{}: \x1b[32m{}\x1b[0m",
            if result.from_book { " (book move)" } else { "" },
            Board::move_to_uci_string(from, to, promote, true)
        );

        true
    }

    fn parse_square(input: &str) -> Option<usize> {
        if input.len() != 2 {
            return None;
        }

        let chars: Vec<char> = input.chars().collect();

        if chars[0] < 'a' || chars[0] > 'h' || chars[1] < '1' || chars[1] > '8' {
            return None;
        }

        let file = (chars[0] as u8 - b'a') as usize;
        let rank = (chars[1] as u8 - b'1') as usize;

        Some(rank * 8 + file)
    }
}

fn main() {
    println!("\n==============================");
    println!("|   Brendan's Chess Engine   |");
    println!("==============================");
    println!("\n\"h or help\" displays a list of commands\n");

    let mut cli = CommandLineInterface::new();
    // cli.show_help();
    cli.run_main_loop();
}
