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

use pyo3::basic::CompareOp;
use pyo3::exceptions::{PyAttributeError, PyImportError, PyIndexError, PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyBytes, PyDict, PyType};
use std::ffi::CString;
use std::path::Path;

mod python_types;
mod svg;
use python_types::{
    PyBitboard, PyColor, PyHand, PyMoveType, PyPiece, PyPieceType, PyRepetitionState, PySquare,
};
use svg::position_to_svg;

use ::rshogi::board::movegen::{generate_moves, Evasions, LegalAll, NonEvasions};
use ::rshogi::board::{self, MoveList, PackedSfen, Position};
use ::rshogi::book::{
    book_key_after as book_key_after_inner, book_key_from_position as book_key_from_position_inner,
    Book, BookBuilder, BookEntry, BookKey, BookMove, MemoryBook, StaticBook,
};
use ::rshogi::records::formats::sbinpack::{
    chains_from_record as chains_from_record_sbinpack,
    deserialize_file as deserialize_sbinpack_file, serialize_file as serialize_sbinpack_file,
    unpack_ply_result,
};
use ::rshogi::records::formats::{
    board_to_bod, board_to_csa, export_csa as export_csa_record, export_jkf as export_jkf_record,
    export_ki2 as export_ki2_record, export_kif as export_kif_record, move_to_bod,
    parse_csa_str as parse_csa_str_record, parse_ki2_str as parse_ki2_str_record,
    parse_kif_str as parse_kif_str_record,
};
use ::rshogi::records::record::{
    GameRecord, GameRecordMetadata, GameRecordResult, MoveRecord, NodeId,
};
use ::rshogi::records::time_control::TimeControl;
use ::rshogi::types::{Eval, GameResult, MOVE_END, MOVE_NONE, MOVE_NULL, MOVE_RESIGN, MOVE_WIN};
use ::rshogi::types::{Move, Move16};

#[pyclass(name = "Move16")]
#[derive(Clone, Copy)]
struct PyMove16 {
    inner: Move16,
}

#[pymethods]
#[allow(dead_code)]
impl PyMove16 {
    #[new]
    fn new(value: u16) -> Self {
        Self { inner: Move16::from_raw(value) }
    }

    #[classmethod]
    fn from_usi(_cls: &Bound<'_, PyType>, s: &str) -> PyResult<Self> {
        Move16::from_usi(s)
            .map(|mv| Self { inner: mv })
            .ok_or_else(|| PyValueError::new_err("invalid USI move"))
    }

    #[classattr]
    #[pyo3(name = "MOVE_NONE")]
    fn move_none(py: Python<'_>) -> PyResult<Py<PyMove16>> {
        Py::new(py, Self { inner: Move16::MOVE_NONE })
    }

    #[classattr]
    #[pyo3(name = "MOVE_NULL")]
    fn move_null(py: Python<'_>) -> PyResult<Py<PyMove16>> {
        Py::new(py, Self { inner: Move16::MOVE_NULL })
    }

    #[classattr]
    #[pyo3(name = "MOVE_RESIGN")]
    fn move_resign(py: Python<'_>) -> PyResult<Py<PyMove16>> {
        Py::new(py, Self { inner: Move16::MOVE_RESIGN })
    }

    #[classattr]
    #[pyo3(name = "MOVE_WIN")]
    fn move_win(py: Python<'_>) -> PyResult<Py<PyMove16>> {
        Py::new(py, Self { inner: Move16::MOVE_WIN })
    }

    #[classattr]
    #[pyo3(name = "MOVE_END")]
    fn move_end(py: Python<'_>) -> PyResult<Py<PyMove16>> {
        Py::new(py, Self { inner: Move16::MOVE_END })
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_usi(&self) -> String {
        self.inner.to_usi()
    }

    #[pyo3(signature = (board=None))]
    #[allow(clippy::wrong_self_convention)]
    fn to_csa(&self, board: Option<&PyBoard>) -> Option<String> {
        if self.inner.is_drop() {
            return Move::from_move16(self.inner).to_csa();
        }

        let board = board?;
        let mv = board.position.move_from_move16(self.inner);
        if !mv.is_ok() {
            return None;
        }
        mv.to_csa()
    }

    fn is_drop(&self) -> bool {
        self.inner.is_drop()
    }

    fn is_promote(&self) -> bool {
        self.inner.is_promote()
    }

    fn is_ok(&self) -> bool {
        self.inner.is_ok()
    }

    #[allow(clippy::wrong_self_convention)]
    fn from_sq(&self) -> PySquare {
        PySquare::from_square(self.inner.from_sq())
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_sq(&self) -> PySquare {
        PySquare::from_square(self.inner.to_sq())
    }

    fn dropped_piece_type(&self) -> Option<PyPieceType> {
        self.inner.dropped_piece().map(PyPieceType::from_piece_type)
    }

    fn type_of(&self) -> PyMoveType {
        PyMoveType::from_move_type(self.inner.move_type())
    }

    #[getter]
    fn value(&self) -> u16 {
        self.inner.raw()
    }

    fn __int__(&self) -> u16 {
        self.inner.raw()
    }

    fn __repr__(&self) -> String {
        format!("Move16({})", self.inner.to_usi())
    }

    fn __str__(&self) -> String {
        self.inner.to_usi()
    }
}

impl PyMove16 {
    pub(crate) fn from_move16(inner: Move16) -> Self {
        Self { inner }
    }

    #[allow(dead_code)]
    pub(crate) fn inner(&self) -> Move16 {
        self.inner
    }
}

#[pyclass(name = "BookKey")]
#[derive(Clone, Copy)]
struct PyBookKey {
    inner: BookKey,
}

#[pymethods]
impl PyBookKey {
    #[getter]
    fn low(&self) -> u64 {
        self.inner.low_u64()
    }

    #[getter]
    fn high(&self) -> u64 {
        self.inner.high_u64()
    }

    fn __int__(&self) -> u128 {
        let low = self.inner.low_u64() as u128;
        let high = self.inner.high_u64() as u128;
        (high << 64) | low
    }

    fn __repr__(&self) -> String {
        format!("BookKey(low={:#x}, high={:#x})", self.inner.low_u64(), self.inner.high_u64())
    }

    fn __str__(&self) -> String {
        let low = self.inner.low_u64();
        let high = self.inner.high_u64();
        if high == 0 {
            format!("{:#x}", low)
        } else {
            format!("0x{high:016x}{low:016x}")
        }
    }

    fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
        let other_key = if let Ok(other) = other.extract::<PyRef<'_, PyBookKey>>() {
            other.inner
        } else {
            return Err(PyTypeError::new_err("unsupported comparison for BookKey"));
        };
        match op {
            CompareOp::Eq => Ok(self.inner == other_key),
            CompareOp::Ne => Ok(self.inner != other_key),
            _ => Err(PyTypeError::new_err("BookKey only supports == and !=")),
        }
    }

    fn __hash__(&self) -> u64 {
        let low = self.inner.low_u64();
        let high = self.inner.high_u64();
        low ^ high.rotate_left(1)
    }
}

impl PyBookKey {
    fn from_book_key(inner: BookKey) -> Self {
        Self { inner }
    }
}

#[pyclass(name = "BookMove")]
#[derive(Clone, Copy)]
struct PyBookMove {
    inner: BookMove,
}

#[pymethods]
impl PyBookMove {
    #[new]
    fn new(move16: &PyMove16, score: i16, depth: u16) -> Self {
        Self { inner: BookMove::new(move16.inner, score, depth) }
    }

    #[getter]
    fn move16(&self) -> PyMove16 {
        PyMove16::from_move16(self.inner.move16())
    }

    #[getter]
    fn score(&self) -> i16 {
        self.inner.score()
    }

    #[getter]
    fn depth(&self) -> u16 {
        self.inner.depth()
    }

    fn __repr__(&self) -> String {
        format!(
            "BookMove(move={}, score={}, depth={})",
            self.inner.move16().to_usi(),
            self.inner.score(),
            self.inner.depth()
        )
    }
}

impl PyBookMove {
    fn from_book_move(inner: BookMove) -> Self {
        Self { inner }
    }
}

#[pyclass(name = "BookEntry")]
struct PyBookEntry {
    key: BookKey,
    moves: Vec<BookMove>,
}

#[pymethods]
impl PyBookEntry {
    #[getter]
    fn key(&self) -> PyBookKey {
        PyBookKey::from_book_key(self.key)
    }

    #[getter]
    fn moves(&self) -> Vec<PyBookMove> {
        self.moves.iter().copied().map(PyBookMove::from_book_move).collect()
    }

    fn __len__(&self) -> usize {
        self.moves.len()
    }

    fn __repr__(&self) -> String {
        format!("BookEntry(key={:#x}, moves={})", self.key.low_u64(), self.moves.len())
    }
}

impl PyBookEntry {
    fn from_book_entry(entry: BookEntry<'_>) -> Self {
        Self { key: entry.key(), moves: entry.moves().to_vec() }
    }
}

#[pyclass(name = "MemoryBook")]
struct PyMemoryBook {
    inner: MemoryBook,
}

#[pymethods]
impl PyMemoryBook {
    #[new]
    fn new() -> Self {
        Self { inner: MemoryBook::new() }
    }

    fn insert_move(&mut self, key: &PyBookKey, book_move: &PyBookMove) {
        self.inner.insert_move(key.inner, book_move.inner);
    }

    fn get(&self, key: &PyBookKey) -> Option<PyBookEntry> {
        self.inner.get(key.inner).map(PyBookEntry::from_book_entry)
    }

    fn contains(&self, key: &PyBookKey) -> bool {
        self.inner.contains(key.inner)
    }

    fn __len__(&self) -> usize {
        self.inner.len()
    }

    fn __repr__(&self) -> String {
        format!("MemoryBook(len={})", self.inner.len())
    }
}

#[pyclass(name = "StaticBook")]
#[derive(Clone)]
struct PyStaticBook {
    inner: StaticBook,
}

#[pymethods]
impl PyStaticBook {
    #[classmethod]
    fn from_bytes(_cls: &Bound<'_, PyType>, data: &Bound<'_, PyAny>) -> PyResult<Self> {
        let bytes: Vec<u8> = data.extract()?;
        let inner = StaticBook::from_bytes(&bytes)
            .map_err(|err| PyValueError::new_err(format!("book error: {err}")))?;
        Ok(Self { inner })
    }

    #[classmethod]
    fn from_memory_book(_cls: &Bound<'_, PyType>, book: &PyMemoryBook) -> Self {
        Self { inner: StaticBook::from_memory_book(&book.inner) }
    }

    #[classmethod]
    fn read_file(_cls: &Bound<'_, PyType>, path: &str) -> PyResult<Self> {
        let inner = StaticBook::read_file(path)
            .map_err(|err| PyValueError::new_err(format!("book error: {err}")))?;
        Ok(Self { inner })
    }

    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.inner.to_bytes())
    }

    fn write_file(&self, path: &str) -> PyResult<()> {
        self.inner
            .write_file(path)
            .map_err(|err| PyValueError::new_err(format!("book error: {err}")))
    }

    fn get(&self, key: &PyBookKey) -> Option<PyBookEntry> {
        self.inner.get(key.inner).map(PyBookEntry::from_book_entry)
    }

    fn contains(&self, key: &PyBookKey) -> bool {
        self.inner.contains(key.inner)
    }

    fn __len__(&self) -> usize {
        self.inner.len()
    }

    fn __repr__(&self) -> String {
        format!("StaticBook(len={})", self.inner.len())
    }
}

#[pyclass(name = "BookBuilder")]
struct PyBookBuilder {
    inner: BookBuilder,
}

#[pymethods]
impl PyBookBuilder {
    #[new]
    fn new() -> Self {
        Self { inner: BookBuilder::new() }
    }

    fn insert(&mut self, key: &PyBookKey, move16: &PyMove16, score: i16, depth: u16) {
        self.inner.insert(key.inner, move16.inner, score, depth);
    }

    fn insert_for_position(&mut self, board: &PyBoard, move16: &PyMove16, score: i16, depth: u16) {
        self.inner.insert_for_position(&board.position, move16.inner, score, depth);
    }

    #[pyo3(signature = (record, default_score=0, default_depth=0))]
    fn extend_from_game_record(
        &mut self,
        record: &PyGameRecord,
        default_score: i16,
        default_depth: u16,
    ) -> PyResult<()> {
        self.inner
            .extend_from_game_record(&record.inner, default_score, default_depth)
            .map_err(|err| PyValueError::new_err(format!("book error: {err}")))
    }

    #[allow(clippy::wrong_self_convention)]
    fn into_memory(&mut self) -> PyMemoryBook {
        let builder = std::mem::take(&mut self.inner);
        PyMemoryBook { inner: builder.into_memory() }
    }

    fn build_static(&mut self) -> PyStaticBook {
        let builder = std::mem::take(&mut self.inner);
        PyStaticBook { inner: builder.build_static() }
    }

    fn __repr__(&self) -> String {
        "BookBuilder()".to_string()
    }
}

#[pyclass(name = "Move")]
#[derive(Clone, Copy)]
struct PyMove {
    inner: Move,
}

#[pymethods]
impl PyMove {
    #[new]
    fn new(value: u32) -> Self {
        Self { inner: Move::from_raw(value) }
    }

    #[classmethod]
    fn from_usi(_cls: &Bound<'_, PyType>, usi: &str) -> PyResult<Self> {
        Move::from_usi(usi)
            .filter(|mv| mv.is_ok())
            .map(|mv| Self { inner: mv })
            .ok_or_else(|| PyValueError::new_err("invalid USI move"))
    }

    #[classattr]
    #[pyo3(name = "MOVE_NONE")]
    fn move_none(py: Python<'_>) -> PyResult<Py<PyMove>> {
        Py::new(py, Self { inner: MOVE_NONE })
    }

    #[classattr]
    #[pyo3(name = "MOVE_NULL")]
    fn move_null(py: Python<'_>) -> PyResult<Py<PyMove>> {
        Py::new(py, Self { inner: MOVE_NULL })
    }

    #[classattr]
    #[pyo3(name = "MOVE_RESIGN")]
    fn move_resign(py: Python<'_>) -> PyResult<Py<PyMove>> {
        Py::new(py, Self { inner: MOVE_RESIGN })
    }

    #[classattr]
    #[pyo3(name = "MOVE_WIN")]
    fn move_win(py: Python<'_>) -> PyResult<Py<PyMove>> {
        Py::new(py, Self { inner: MOVE_WIN })
    }

    #[classattr]
    #[pyo3(name = "MOVE_END")]
    fn move_end(py: Python<'_>) -> PyResult<Py<PyMove>> {
        Py::new(py, Self { inner: MOVE_END })
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_usi(&self) -> String {
        self.inner.to_usi()
    }

    fn is_drop(&self) -> bool {
        self.inner.is_drop()
    }

    fn is_promote(&self) -> bool {
        self.inner.is_promote()
    }

    fn is_ok(&self) -> bool {
        self.inner.is_ok()
    }

    #[allow(clippy::wrong_self_convention)]
    fn from_sq(&self) -> PySquare {
        PySquare::from_square(self.inner.from_sq())
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_sq(&self) -> PySquare {
        PySquare::from_square(self.inner.to_sq())
    }

    fn dropped_piece_type(&self) -> Option<PyPieceType> {
        self.inner.dropped_piece().map(PyPieceType::from_piece_type)
    }

    fn type_of(&self) -> PyMoveType {
        PyMoveType::from_move_type(self.inner.move_type())
    }

    fn moved_after_piece(&self) -> PyPiece {
        PyPiece::from_piece(self.inner.moved_after_piece())
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_move16(&self) -> PyMove16 {
        PyMove16::from_move16(self.inner.to_move16())
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_csa(&self) -> Option<String> {
        self.inner.to_csa()
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_ki2(&self, board: &PyBoard) -> Option<String> {
        self.inner.to_ki2(&board.position)
    }

    #[getter]
    fn value(&self) -> u32 {
        self.inner.raw()
    }

    fn __int__(&self) -> u32 {
        self.inner.raw()
    }

    fn __repr__(&self) -> String {
        format!("Move({})", self.inner.to_usi())
    }

    fn __str__(&self) -> String {
        self.inner.to_usi()
    }
}

#[pyclass(name = "GameResult")]
#[derive(Clone, Copy)]
struct PyGameResult {
    inner: GameResult,
}

#[pyclass(name = "TimeControl")]
#[derive(Clone, Copy)]
struct PyTimeControl {
    inner: TimeControl,
}

#[pymethods]
impl PyTimeControl {
    #[getter]
    fn base_seconds(&self) -> u32 {
        self.inner.base_seconds()
    }

    #[getter]
    fn byoyomi_seconds(&self) -> u32 {
        self.inner.byoyomi_seconds()
    }

    #[getter]
    fn increment_seconds(&self) -> u32 {
        self.inner.increment_seconds()
    }

    #[allow(clippy::wrong_self_convention)]
    fn to_spec(&self) -> String {
        self.inner.to_spec()
    }

    fn __repr__(&self) -> String {
        format!(
            "TimeControl(base_seconds={}, byoyomi_seconds={}, increment_seconds={})",
            self.inner.base_seconds(),
            self.inner.byoyomi_seconds(),
            self.inner.increment_seconds()
        )
    }
}

#[pymethods]
#[allow(non_snake_case)]
impl PyGameResult {
    #[new]
    fn new(value: u8) -> PyResult<Self> {
        let inner = game_result_from_value(value)
            .ok_or_else(|| PyValueError::new_err("invalid GameResult value"))?;
        Ok(Self { inner })
    }

    #[getter]
    fn value(&self) -> u8 {
        self.inner as u8
    }

    #[getter]
    fn name(&self) -> &'static str {
        game_result_name(self.inner)
    }

    fn is_black_win(&self) -> bool {
        self.inner.is_black_win()
    }

    fn is_white_win(&self) -> bool {
        self.inner.is_white_win()
    }

    fn is_draw(&self) -> bool {
        self.inner.is_draw()
    }

    fn is_win(&self) -> bool {
        self.inner.is_win()
    }

    fn is_win_by_declaration(&self) -> bool {
        self.inner.is_win_by_declaration()
    }

    fn __int__(&self) -> u8 {
        self.inner as u8
    }

    fn __repr__(&self) -> String {
        format!("GameResult.{}", game_result_name(self.inner))
    }

    fn __str__(&self) -> &'static str {
        game_result_name(self.inner)
    }

    fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
        let other_value = if let Ok(other) = other.extract::<PyRef<'_, PyGameResult>>() {
            other.inner as u8
        } else if let Ok(value) = other.extract::<u8>() {
            value
        } else if let Ok(value) = other.extract::<u32>() {
            value as u8
        } else {
            return Err(PyTypeError::new_err("unsupported comparison for GameResult"));
        };
        match op {
            CompareOp::Eq => Ok((self.inner as u8) == other_value),
            CompareOp::Ne => Ok((self.inner as u8) != other_value),
            _ => Err(PyTypeError::new_err("GameResult only supports == and !=")),
        }
    }

    #[classattr]
    fn BLACK_WIN(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::BlackWin })
    }

    #[classattr]
    fn WHITE_WIN(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::WhiteWin })
    }

    #[classattr]
    fn DRAW_BY_REPETITION(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::DrawByRepetition })
    }

    #[classattr]
    fn ERROR(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::Error })
    }

    #[classattr]
    fn BLACK_WIN_BY_DECLARATION(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::BlackWinByDeclaration })
    }

    #[classattr]
    fn WHITE_WIN_BY_DECLARATION(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::WhiteWinByDeclaration })
    }

    #[classattr]
    fn DRAW_BY_MAX_PLIES(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::DrawByMaxPlies })
    }

    #[classattr]
    fn INVALID(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::Invalid })
    }

    #[classattr]
    fn BLACK_WIN_BY_FORFEIT(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::BlackWinByForfeit })
    }

    #[classattr]
    fn WHITE_WIN_BY_FORFEIT(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::WhiteWinByForfeit })
    }

    #[classattr]
    fn DRAW_BY_IMPASSE(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::DrawByImpasse })
    }

    #[classattr]
    fn BLACK_WIN_BY_ILLEGAL_MOVE(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::BlackWinByIllegalMove })
    }

    #[classattr]
    fn WHITE_WIN_BY_ILLEGAL_MOVE(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::WhiteWinByIllegalMove })
    }

    #[classattr]
    fn PAUSED(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::Paused })
    }

    #[classattr]
    fn BLACK_WIN_BY_TIMEOUT(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::BlackWinByTimeout })
    }

    #[classattr]
    fn WHITE_WIN_BY_TIMEOUT(py: Python<'_>) -> PyResult<Py<PyGameResult>> {
        Py::new(py, PyGameResult { inner: GameResult::WhiteWinByTimeout })
    }
}

#[pyclass(name = "MoveRecord")]
#[derive(Clone)]
struct PyMoveRecord {
    inner: MoveRecord,
}

#[pymethods]
impl PyMoveRecord {
    #[getter]
    #[pyo3(name = "move")]
    fn move_(&self) -> PyMove {
        PyMove { inner: self.inner.mv() }
    }

    #[getter]
    fn eval(&self) -> Option<i32> {
        self.inner.eval().map(|eval| eval.to_i32())
    }

    #[getter]
    fn comment(&self) -> Option<String> {
        self.inner.comment().map(|value| value.to_string())
    }

    #[getter]
    fn time_ms(&self) -> Option<u32> {
        self.inner.time_ms()
    }

    fn __repr__(&self) -> String {
        let eval = self.inner.eval().map_or("None".to_string(), |v| v.to_i32().to_string());
        format!("MoveRecord(move={}, eval={})", self.inner.mv().to_usi(), eval)
    }
}

#[pyclass(name = "GameRecordResult")]
#[derive(Clone)]
struct PyGameRecordResult {
    inner: GameRecordResult,
}

#[pymethods]
impl PyGameRecordResult {
    #[getter]
    fn result(&self) -> PyGameResult {
        PyGameResult { inner: self.inner.result() }
    }

    fn __repr__(&self) -> String {
        format!("GameRecordResult(result={})", game_result_name(self.inner.result()))
    }
}

#[pyclass(name = "GameRecordMetadata")]
#[derive(Clone)]
struct PyGameRecordMetadata {
    inner: GameRecordMetadata,
}

#[pymethods]
impl PyGameRecordMetadata {
    #[getter]
    fn event(&self) -> Option<String> {
        self.inner.event().map(|value| value.to_string())
    }

    #[getter]
    fn site(&self) -> Option<String> {
        self.inner.site().map(|value| value.to_string())
    }

    #[getter]
    fn black_player(&self) -> Option<String> {
        self.inner.black_player().map(|value| value.to_string())
    }

    #[getter]
    fn white_player(&self) -> Option<String> {
        self.inner.white_player().map(|value| value.to_string())
    }

    #[getter]
    fn time_control(&self) -> Option<PyTimeControl> {
        self.inner.time_control().cloned().map(|value| PyTimeControl { inner: value })
    }

    #[getter]
    fn start_date(&self) -> Option<String> {
        self.inner.start_date().map(|value| value.to_string())
    }

    #[getter]
    fn end_date(&self) -> Option<String> {
        self.inner.end_date().map(|value| value.to_string())
    }

    #[getter]
    fn comment(&self) -> Option<String> {
        self.inner.comment().map(|value| value.to_string())
    }

    fn attributes(&self) -> Vec<(String, String)> {
        self.inner.attributes().iter().map(|(k, v)| (k.clone(), v.clone())).collect()
    }

    fn __repr__(&self) -> String {
        format!(
            "GameRecordMetadata(event={:?}, site={:?}, players={:?}/{:?})",
            self.inner.event(),
            self.inner.site(),
            self.inner.black_player(),
            self.inner.white_player()
        )
    }
}

#[pyclass(name = "GameRecord")]
#[derive(Clone)]
struct PyGameRecord {
    inner: GameRecord,
}

#[pymethods]
impl PyGameRecord {
    #[getter]
    fn init_position_to_sfen(&self) -> &str {
        self.inner.init_position_to_sfen()
    }

    #[getter]
    fn moves(&self) -> Vec<PyMoveRecord> {
        self.inner.moves().iter().cloned().map(|mv| PyMoveRecord { inner: mv }).collect()
    }

    #[getter]
    fn metadata(&self) -> PyGameRecordMetadata {
        PyGameRecordMetadata { inner: self.inner.metadata().clone() }
    }

    #[getter]
    fn result(&self) -> PyGameRecordResult {
        PyGameRecordResult { inner: self.inner.result().clone() }
    }

    fn num_moves(&self) -> usize {
        self.inner.num_moves()
    }

    #[classmethod]
    fn from_kif(_cls: &Bound<'_, PyType>, text: &str) -> PyResult<Self> {
        let record =
            parse_kif_str_record(text).map_err(|err| PyValueError::new_err(err.to_string()))?;
        Ok(Self { inner: record })
    }

    #[classmethod]
    fn from_ki2(_cls: &Bound<'_, PyType>, text: &str) -> PyResult<Self> {
        let record =
            parse_ki2_str_record(text).map_err(|err| PyValueError::new_err(err.to_string()))?;
        Ok(Self { inner: record })
    }

    #[classmethod]
    fn from_csa(_cls: &Bound<'_, PyType>, text: &str) -> PyResult<Self> {
        let record =
            parse_csa_str_record(text).map_err(|err| PyValueError::new_err(err.to_string()))?;
        Ok(Self { inner: record })
    }

    #[classmethod]
    fn from_sbinpack(_cls: &Bound<'_, PyType>, data: &Bound<'_, PyAny>) -> PyResult<Self> {
        let bytes: Vec<u8> = data.extract()?;
        let file = deserialize_sbinpack_file(&bytes)
            .map_err(|err| PyValueError::new_err(format!("sbinpack: {err:?}")))?;
        let chain = if file.chains.is_empty() {
            return Err(PyValueError::new_err("sbinpack: no chains found"));
        } else if file.chains.len() > 1 {
            return Err(PyValueError::new_err("sbinpack: multiple chains not supported"));
        } else {
            &file.chains[0]
        };

        let record = sbinpack_chain_to_record(chain)
            .map_err(|err| PyValueError::new_err(format!("sbinpack: {err}")))?;
        Ok(Self { inner: record })
    }

    fn to_kif(&self) -> PyResult<String> {
        export_kif_record(&self.inner).map_err(|err| PyValueError::new_err(err.to_string()))
    }

    fn to_ki2(&self) -> PyResult<String> {
        export_ki2_record(&self.inner).map_err(|err| PyValueError::new_err(err.to_string()))
    }

    fn to_csa(&self) -> PyResult<String> {
        export_csa_record(&self.inner).map_err(|err| PyValueError::new_err(err.to_string()))
    }

    fn to_jkf(&self) -> PyResult<String> {
        export_jkf_record(&self.inner).map_err(|err| PyValueError::new_err(err.to_string()))
    }

    #[classmethod]
    #[pyo3(signature = (path, encoding=None))]
    fn read_kif(
        _cls: &Bound<'_, PyType>,
        path: &Bound<'_, PyAny>,
        encoding: Option<&str>,
    ) -> PyResult<Self> {
        let text = read_text_with_encoding(path, encoding, default_kif_encoding)?;
        let record =
            parse_kif_str_record(&text).map_err(|err| PyValueError::new_err(err.to_string()))?;
        Ok(Self { inner: record })
    }

    #[classmethod]
    #[pyo3(signature = (path, encoding="shift_jis"))]
    fn read_csa(
        _cls: &Bound<'_, PyType>,
        path: &Bound<'_, PyAny>,
        encoding: Option<&str>,
    ) -> PyResult<Self> {
        let text = read_text_with_encoding(path, encoding, default_csa_encoding)?;
        let record =
            parse_csa_str_record(&text).map_err(|err| PyValueError::new_err(err.to_string()))?;
        Ok(Self { inner: record })
    }

    #[pyo3(signature = (path, encoding=None))]
    fn write_kif(&self, path: &Bound<'_, PyAny>, encoding: Option<&str>) -> PyResult<()> {
        let text = self.to_kif()?;
        write_text_with_encoding(path, &text, encoding, default_kif_encoding)
    }

    #[pyo3(signature = (path, encoding="shift_jis"))]
    fn write_csa(&self, path: &Bound<'_, PyAny>, encoding: Option<&str>) -> PyResult<()> {
        let text = self.to_csa()?;
        write_text_with_encoding(path, &text, encoding, default_csa_encoding)
    }

    #[pyo3(signature = (path, encoding="shift_jis"))]
    fn write_ki2(&self, path: &Bound<'_, PyAny>, encoding: Option<&str>) -> PyResult<()> {
        let text = self.to_ki2()?;
        write_text_with_encoding(path, &text, encoding, default_ki2_encoding)
    }

    #[pyo3(signature = (path, encoding="utf-8"))]
    fn write_jkf(&self, path: &Bound<'_, PyAny>, encoding: Option<&str>) -> PyResult<()> {
        let text = self.to_jkf()?;
        write_text_with_encoding(path, &text, encoding, default_jkf_encoding)
    }

    #[pyo3(signature = (stem_score=0, include_main=true, include_variations=false))]
    fn to_sbinpack<'py>(
        &self,
        py: Python<'py>,
        stem_score: i16,
        include_main: bool,
        include_variations: bool,
    ) -> PyResult<Bound<'py, PyBytes>> {
        let chains =
            chains_from_record_sbinpack(&self.inner, stem_score, include_main, include_variations)
                .map_err(|err| PyValueError::new_err(format!("sbinpack: {err:?}")))?;
        if chains.is_empty() {
            return Err(PyValueError::new_err("sbinpack: no chains to serialize"));
        }
        let bytes = serialize_sbinpack_file(&chains)
            .map_err(|err| PyValueError::new_err(format!("sbinpack: {err:?}")))?;
        Ok(PyBytes::new(py, &bytes))
    }

    fn root_node_id(&self) -> usize {
        self.inner.root_id().raw()
    }

    fn main_line_ids(&self) -> Vec<usize> {
        self.inner.main_line_ids().iter().map(|id| id.raw()).collect()
    }

    fn node_parent(&self, node_id: usize) -> PyResult<Option<usize>> {
        let node_id = self.node_id_or_err(node_id)?;
        Ok(self.inner.node(node_id).parent().map(|id| id.raw()))
    }

    fn node_move(&self, node_id: usize) -> PyResult<Option<PyMoveRecord>> {
        let node_id = self.node_id_or_err(node_id)?;
        Ok(self.inner.node(node_id).mv().cloned().map(|mv| PyMoveRecord { inner: mv }))
    }

    fn children(&self, node_id: usize) -> PyResult<Vec<usize>> {
        let node_id = self.node_id_or_err(node_id)?;
        Ok(self.inner.children(node_id).iter().map(|id| id.raw()).collect())
    }

    fn __repr__(&self) -> String {
        format!(
            "GameRecord(init_position_sfen='{}', moves={}, result={})",
            self.inner.init_position_to_sfen(),
            self.inner.num_moves(),
            game_result_name(self.inner.result().result())
        )
    }
}

impl PyGameRecord {
    fn node_id_or_err(&self, node_id: usize) -> PyResult<NodeId> {
        if node_id >= self.inner.node_len() {
            Err(PyIndexError::new_err("node_id out of range"))
        } else {
            Ok(NodeId::from_raw(node_id))
        }
    }
}

fn path_to_string(path: &Bound<'_, PyAny>) -> PyResult<String> {
    let os = path.py().import("os")?;
    let fspath = os.getattr("fspath")?.call1((path,))?;
    fspath.extract::<String>()
}

fn default_kif_encoding(path: &Bound<'_, PyAny>) -> PyResult<&'static str> {
    let path_str = path_to_string(path)?;
    let ext = Path::new(&path_str)
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();
    Ok(if ext == "kifu" { "utf-8" } else { "shift_jis" })
}

fn default_csa_encoding(_path: &Bound<'_, PyAny>) -> PyResult<&'static str> {
    Ok("shift_jis")
}

fn default_ki2_encoding(_path: &Bound<'_, PyAny>) -> PyResult<&'static str> {
    Ok("shift_jis")
}

fn default_jkf_encoding(_path: &Bound<'_, PyAny>) -> PyResult<&'static str> {
    Ok("utf-8")
}

fn read_text_with_encoding(
    path: &Bound<'_, PyAny>,
    encoding: Option<&str>,
    default_encoding: fn(&Bound<'_, PyAny>) -> PyResult<&'static str>,
) -> PyResult<String> {
    let py = path.py();
    let pathlib = py.import("pathlib")?;
    let path_obj = pathlib.getattr("Path")?.call1((path,))?;
    let encoding = match encoding {
        Some(value) => value,
        None => default_encoding(path)?,
    };
    let kwargs = PyDict::new(py);
    kwargs.set_item("encoding", encoding)?;
    let text = path_obj.call_method("read_text", (), Some(&kwargs))?;
    text.extract::<String>()
}

fn write_text_with_encoding(
    path: &Bound<'_, PyAny>,
    text: &str,
    encoding: Option<&str>,
    default_encoding: fn(&Bound<'_, PyAny>) -> PyResult<&'static str>,
) -> PyResult<()> {
    let py = path.py();
    let pathlib = py.import("pathlib")?;
    let path_obj = pathlib.getattr("Path")?.call1((path,))?;
    let encoding = match encoding {
        Some(value) => value,
        None => default_encoding(path)?,
    };
    let kwargs = PyDict::new(py);
    kwargs.set_item("encoding", encoding)?;
    path_obj.call_method("write_text", (text,), Some(&kwargs))?;
    Ok(())
}

#[pyclass(name = "Svg")]
#[derive(Clone)]
struct PySvg {
    inner: String,
}

#[pymethods]
impl PySvg {
    fn _repr_svg_(&self) -> &str {
        &self.inner
    }

    fn __str__(&self) -> &str {
        &self.inner
    }

    fn __repr__(&self) -> &str {
        &self.inner
    }
}

#[pyclass(unsendable, name = "Board")]
struct PyBoard {
    // `Position` は `#[repr(align(64))]` のため、PyO3 が確保する PyObject 領域に
    // そのまま埋め込むとアラインメント違反で abort する。
    // ヒープに逃がして正しいアラインメントを保証する。
    position: Box<Position>,
}

#[pymethods]
impl PyBoard {
    #[new]
    #[pyo3(signature = (sfen=None))]
    fn new(sfen: Option<&str>) -> PyResult<Self> {
        board::init();
        let mut position = Box::new(Position::empty());
        if let Some(sfen) = sfen {
            position.set_sfen(sfen).map_err(|err| PyValueError::new_err(err.to_string()))?;
        } else {
            position.set_hirate();
        }
        position.init_stack();
        Ok(Self { position })
    }

    fn to_sfen(&self) -> String {
        self.position.to_sfen(None)
    }

    fn copy(&self) -> Self {
        Self { position: self.position.clone() }
    }

    fn __copy__(&self) -> Self {
        self.copy()
    }

    #[pyo3(signature = (out=None))]
    fn to_psfen(&self, py: Python<'_>, out: Option<&Bound<'_, PyAny>>) -> PyResult<Py<PyAny>> {
        let packed = self.position.to_psfen();
        if let Some(out) = out {
            fill_psfen_output(out, &packed.data)?;
            Ok(py.None())
        } else {
            Ok(PyBytes::new(py, &packed.data).unbind().into())
        }
    }

    fn set_sfen(&mut self, sfen: &str) -> PyResult<()> {
        self.position.set_sfen(sfen).map_err(|err| PyValueError::new_err(err.to_string()))?;
        self.position.init_stack();
        Ok(())
    }

    fn set_psfen(&mut self, psfen: &Bound<'_, PyAny>) -> PyResult<()> {
        let data = extract_psfen_bytes(psfen)?;
        let packed = PackedSfen { data };
        self.position
            .set_psfen(&packed, false, 1)
            .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
        self.position.init_stack();
        Ok(())
    }

    fn zobrist_hash(&self) -> u128 {
        let key = self.position.key();
        (u128::from(key.high_u64()) << 64) | u128::from(key.low_u64())
    }

    fn can_declare_win(&self) -> bool {
        self.position.declaration_win() != MOVE_NONE
    }

    fn move_from_move16(&self, mv16: &PyMove16) -> PyResult<PyMove> {
        let mv = self.position.move_from_move16(mv16.inner);
        if !mv.is_ok() {
            return Err(PyValueError::new_err("invalid move value"));
        }
        Ok(PyMove { inner: mv })
    }

    fn move_from_csa(&self, csa: &str) -> PyResult<PyMove> {
        let mv = self.position.move_from_csa(csa);
        if !mv.is_ok() {
            return Err(PyValueError::new_err("invalid CSA move"));
        }
        Ok(PyMove { inner: mv })
    }

    fn apply_move(&mut self, mv: &Bound<'_, PyAny>) -> PyResult<()> {
        let move_value = parse_move(mv, &self.position)?;
        if !self.position.is_legal(move_value) {
            return Err(PyValueError::new_err("illegal move"));
        }
        self.position.apply_move(move_value);
        Ok(())
    }

    fn push(&mut self, mv: &Bound<'_, PyAny>) -> PyResult<PyMove> {
        let move_value = parse_move(mv, &self.position)?;
        if !self.position.is_legal(move_value) {
            return Err(PyValueError::new_err("illegal move"));
        }
        self.position.apply_move(move_value);
        Ok(PyMove { inner: move_value })
    }

    fn apply_usi(&mut self, usi: &str) -> PyResult<()> {
        let mv16 =
            Move16::from_usi(usi).ok_or_else(|| PyValueError::new_err("invalid USI move"))?;
        let move_value = self.position.move_from_move16(mv16);
        if !move_value.is_ok() {
            return Err(PyValueError::new_err("invalid move value"));
        }
        if !self.position.is_legal(move_value) {
            return Err(PyValueError::new_err("illegal move"));
        }
        self.position.apply_move(move_value);
        Ok(())
    }

    fn push_usi(&mut self, usi: &str) -> PyResult<PyMove> {
        let mv16 =
            Move16::from_usi(usi).ok_or_else(|| PyValueError::new_err("invalid USI move"))?;
        let move_value = self.position.move_from_move16(mv16);
        if !move_value.is_ok() {
            return Err(PyValueError::new_err("invalid move value"));
        }
        if !self.position.is_legal(move_value) {
            return Err(PyValueError::new_err("illegal move"));
        }
        self.position.apply_move(move_value);
        Ok(PyMove { inner: move_value })
    }

    fn apply_csa(&mut self, csa: &str) -> PyResult<()> {
        let move_value = self.position.move_from_csa(csa);
        if !move_value.is_ok() {
            return Err(PyValueError::new_err("invalid CSA move"));
        }
        if !self.position.is_legal(move_value) {
            return Err(PyValueError::new_err("illegal move"));
        }
        self.position.apply_move(move_value);
        Ok(())
    }

    fn undo_move(&mut self, mv: &Bound<'_, PyAny>) -> PyResult<()> {
        let move_value = parse_move(mv, &self.position)?;
        let last = self.position.last_move();
        if !last.is_ok() || last != move_value {
            return Err(PyValueError::new_err("can only undo the last move"));
        }
        self.position
            .undo_move(move_value)
            .map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
        Ok(())
    }

    fn pop(&mut self) -> PyResult<Option<PyMove>> {
        let last = self.position.last_move();
        if !last.is_ok() {
            return Ok(None);
        }
        self.position.undo_move(last).map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
        Ok(Some(PyMove { inner: last }))
    }

    fn peek(&self) -> Option<PyMove> {
        let mv = self.position.last_move();
        if mv.is_ok() {
            Some(PyMove { inner: mv })
        } else {
            None
        }
    }

    fn last_move(&self) -> Option<PyMove> {
        let mv = self.position.last_move();
        if mv.is_ok() {
            Some(PyMove { inner: mv })
        } else {
            None
        }
    }

    fn piece_on(&self, square: &PySquare) -> PyPiece {
        PyPiece::from_piece(self.position.piece_on(square.inner()))
    }

    fn is_square_empty(&self, square: &PySquare) -> bool {
        self.position.is_square_empty(square.inner())
    }

    fn empties(&self) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.empties())
    }

    fn hand(&self, color: &PyColor) -> PyHand {
        PyHand::from_hand(self.position.hand(color.inner()))
    }

    fn king_square(&self, color: &PyColor) -> PySquare {
        PySquare::from_square(self.position.king_square(color.inner()))
    }

    fn turn(&self) -> PyColor {
        PyColor::from_color(self.position.turn())
    }

    fn game_ply(&self) -> u32 {
        self.position.game_ply() as u32
    }

    fn pieces(&self) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.pieces())
    }

    fn pieces_by_type(&self, piece_type: &PyPieceType) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.pieces_by_type(piece_type.inner()))
    }

    fn pieces_by_color(&self, color: &PyColor) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.pieces_by_color(color.inner()))
    }

    fn pieces_for(&self, piece_type: &PyPieceType, color: &PyColor) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.pieces_for(piece_type.inner(), color.inner()))
    }

    fn golds(&self) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.golds())
    }

    fn bishop_horse(&self) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.bishop_horse())
    }

    fn rook_dragon(&self) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.rook_dragon())
    }

    fn attackers_to(&self, square: &PySquare, occupied: &PyBitboard) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.attackers_to(square.inner(), occupied.inner()))
    }

    fn attackers_to_color(
        &self,
        color: &PyColor,
        square: &PySquare,
        occupied: &PyBitboard,
    ) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.attackers_to_color(
            color.inner(),
            square.inner(),
            occupied.inner(),
        ))
    }

    fn attackers_to_color_current(&self, color: &PyColor, square: &PySquare) -> PyBitboard {
        PyBitboard::from_bitboard(
            self.position.attackers_to_color_current(color.inner(), square.inner()),
        )
    }

    fn is_attacked_by(&self, square: &PySquare, by_color: &PyColor) -> bool {
        self.position.is_attacked_by(square.inner(), by_color.inner())
    }

    fn attacks_by(&self, color: &PyColor, piece_type: &PyPieceType) -> PyBitboard {
        PyBitboard::from_bitboard(self.position.attacks_by(color.inner(), piece_type.inner()))
    }

    fn is_legal(&self, mv: &Bound<'_, PyAny>) -> PyResult<bool> {
        let move_value = parse_move(mv, &self.position)?;
        Ok(self.position.is_legal(move_value))
    }

    fn legal_moves(&self) -> Vec<PyMove16> {
        let mut list = MoveList::new();
        generate_moves::<LegalAll>(&self.position, &mut list);
        collect_py_move16s(&list)
    }

    fn legal_moves32(&self) -> Vec<PyMove> {
        let mut list = MoveList::new();
        generate_moves::<LegalAll>(&self.position, &mut list);
        collect_py_moves32(&self.position, &list)
    }

    fn pseudo_legal_moves(&self) -> Vec<PyMove16> {
        let mut list = MoveList::new();
        if self.position.in_check() {
            generate_moves::<Evasions>(&self.position, &mut list);
        } else {
            generate_moves::<NonEvasions>(&self.position, &mut list);
        }
        collect_py_move16s(&list)
    }

    fn pseudo_legal_moves32(&self) -> Vec<PyMove> {
        let mut list = MoveList::new();
        if self.position.in_check() {
            generate_moves::<Evasions>(&self.position, &mut list);
        } else {
            generate_moves::<NonEvasions>(&self.position, &mut list);
        }
        collect_py_moves32(&self.position, &list)
    }

    fn in_check(&self) -> bool {
        self.position.in_check()
    }

    fn is_mated(&self) -> bool {
        self.position.is_mated()
    }

    fn repetition_state(&self) -> PyRepetitionState {
        PyRepetitionState::from_state(self.position.repetition_state())
    }

    #[pyo3(signature = (threshold=3))]
    fn is_repetition(&self, threshold: u8) -> bool {
        self.position.is_repetition(threshold)
    }

    #[pyo3(signature = (lastmove=None, scale=1.0))]
    fn to_svg(
        &self,
        py: Python<'_>,
        lastmove: Option<&Bound<'_, PyAny>>,
        scale: f32,
    ) -> PyResult<Py<PySvg>> {
        let last_move = match lastmove {
            Some(value) => Some(parse_move(value, &self.position)?),
            None => None,
        };
        let svg = position_to_svg(&self.position, last_move, scale);
        Py::new(py, PySvg { inner: svg })
    }

    fn to_bod(&self) -> PyResult<String> {
        let last = self.position.last_move();
        if !last.is_ok() {
            return Ok(board_to_bod(&self.position));
        }
        let mut prev = self.position.clone();
        prev.undo_move(last).map_err(|err| PyValueError::new_err(format!("{err:?}")))?;
        let move_text =
            move_to_bod(&prev, last).ok_or_else(|| PyValueError::new_err("invalid move"))?;
        Ok(format!(
            "{}\n手数＝{}　{}　まで",
            board_to_bod(&self.position),
            prev.game_ply(),
            move_text
        ))
    }

    fn to_csa(&self) -> String {
        board_to_csa(&self.position)
    }

    fn reset_sfen(&mut self) {
        self.position.set_hirate();
        self.position.init_stack();
    }
}

#[pymodule]
fn _rshogi(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PyColor>()?;
    m.add_class::<PyPieceType>()?;
    m.add_class::<PyPiece>()?;
    m.add_class::<PySquare>()?;
    m.add_class::<PyBitboard>()?;
    m.add_class::<PyHand>()?;
    m.add_class::<PyMoveType>()?;
    m.add_class::<PyRepetitionState>()?;
    m.add_class::<PySvg>()?;
    m.add_class::<PyBoard>()?;
    m.add_class::<PyMove>()?;
    m.add_class::<PyMove16>()?;
    m.add_class::<PyGameResult>()?;
    m.add_class::<PyTimeControl>()?;
    m.add_class::<PyMoveRecord>()?;
    m.add_class::<PyGameRecordResult>()?;
    m.add_class::<PyGameRecordMetadata>()?;
    m.add_class::<PyGameRecord>()?;
    m.add_class::<PyBookKey>()?;
    m.add_class::<PyBookMove>()?;
    m.add_class::<PyBookEntry>()?;
    m.add_class::<PyMemoryBook>()?;
    m.add_class::<PyStaticBook>()?;
    m.add_class::<PyBookBuilder>()?;
    m.add_function(wrap_pyfunction!(book_key_from_position, m)?)?;
    m.add_function(wrap_pyfunction!(book_key_after, m)?)?;
    m.add_function(wrap_pyfunction!(book_builder_from_game_record, m)?)?;
    m.add_function(wrap_pyfunction!(move16, m)?)?;
    add_dtype_module(_py, m)?;
    Ok(())
}

fn add_dtype_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    let dtype_module = PyModule::new(py, "dtype")?;
    dtype_module.add_function(wrap_pyfunction!(dtype_getattr, dtype_module.clone())?)?;
    let getattr = dtype_module.getattr("dtype_getattr")?;
    dtype_module.setattr("__getattr__", getattr)?;

    if let Ok((packed_sfen, packed_sfen_value)) = build_numpy_dtypes(py) {
        dtype_module.add("PackedSfen", packed_sfen)?;
        dtype_module.add("PackedSfenValue", packed_sfen_value)?;
    }

    m.add_submodule(&dtype_module)?;
    Ok(())
}

fn build_numpy_dtypes(py: Python<'_>) -> PyResult<(Py<PyAny>, Py<PyAny>)> {
    let numpy = py.import("numpy")?;
    let locals = PyDict::new(py);
    locals.set_item("np", &numpy)?;

    let packed_sfen_expr = CString::new("np.dtype([('sfen', np.uint8, 32)])")?;
    let packed_sfen = py.eval(packed_sfen_expr.as_c_str(), Some(&locals), None)?.unbind();

    let packed_sfen_value_expr = CString::new(
        "np.dtype([('sfen', np.uint8, 32), ('score', np.int16), ('move', np.uint16), ('gamePly', np.uint16), ('game_result', np.int8), ('padding', np.uint8)])",
    )?;
    let packed_sfen_value =
        py.eval(packed_sfen_value_expr.as_c_str(), Some(&locals), None)?.unbind();

    Ok((packed_sfen, packed_sfen_value))
}

#[pyfunction]
fn dtype_getattr(py: Python<'_>, name: &str) -> PyResult<Py<PyAny>> {
    let (packed_sfen, packed_sfen_value) = build_numpy_dtypes(py).map_err(|_| {
        PyImportError::new_err(format!("numpy is required for rshogi.numpy.{name}"))
    })?;

    match name {
        "PackedSfen" => Ok(packed_sfen),
        "PackedSfenValue" => Ok(packed_sfen_value),
        _ => Err(PyAttributeError::new_err(format!(
            "module 'rshogi.numpy' has no attribute '{name}'"
        ))),
    }
}

fn sbinpack_chain_to_record(
    chain: &::rshogi::records::formats::sbinpack::SbinpackChain,
) -> Result<GameRecord, String> {
    let (result, ply) = unpack_ply_result(chain.stem.ply_result);
    let mut pos = Position::empty();
    pos.set_psfen(&chain.stem.packed_sfen, false, ply).map_err(|err| format!("{err:?}"))?;
    let init_position_sfen = pos.to_sfen(None);

    let moves: Vec<MoveRecord> = chain
        .moves
        .iter()
        .map(|entry| MoveRecord::new(entry.mv, Some(Eval::from_i32(entry.eval))))
        .collect();

    let result = result.unwrap_or(GameResult::Invalid);
    let record_result = GameRecordResult::new(result);

    GameRecord::from_main_line(init_position_sfen, moves, record_result)
        .map_err(|err| err.to_string())
}

#[pyfunction]
fn book_key_from_position(board: &PyBoard) -> PyBookKey {
    PyBookKey::from_book_key(book_key_from_position_inner(&board.position))
}

#[pyfunction]
fn book_key_after(board: &PyBoard, move16: &PyMove16) -> PyBookKey {
    PyBookKey::from_book_key(book_key_after_inner(&board.position, move16.inner))
}

#[pyfunction]
#[pyo3(signature = (record, default_score=0, default_depth=0))]
fn book_builder_from_game_record(
    record: &PyGameRecord,
    default_score: i16,
    default_depth: u16,
) -> PyResult<PyMemoryBook> {
    let mut builder = BookBuilder::new();
    builder
        .extend_from_game_record(&record.inner, default_score, default_depth)
        .map_err(|err| PyValueError::new_err(format!("book error: {err}")))?;
    Ok(PyMemoryBook { inner: builder.into_memory() })
}

#[pyfunction]
fn move16(mv: &Bound<'_, PyAny>) -> PyResult<PyMove16> {
    if let Ok(mv16) = mv.extract::<PyRef<'_, PyMove16>>() {
        return Ok(PyMove16::from_move16(mv16.inner));
    }
    if let Ok(mv32) = mv.extract::<PyRef<'_, PyMove>>() {
        return Ok(PyMove16::from_move16(mv32.inner.to_move16()));
    }
    if let Ok(value) = mv.extract::<u16>() {
        return Ok(PyMove16::from_move16(Move16::from_raw(value)));
    }
    if let Ok(value) = mv.extract::<u32>() {
        return Ok(PyMove16::from_move16(Move16::from_raw(value as u16)));
    }

    Err(PyTypeError::new_err("move16() expects Move16, Move, or int"))
}

fn parse_move(mv: &Bound<'_, PyAny>, position: &Position) -> PyResult<Move> {
    let move_value = if let Ok(mv16) = mv.extract::<PyRef<'_, PyMove16>>() {
        position.move_from_move16(mv16.inner)
    } else if let Ok(mv32) = mv.extract::<PyRef<'_, PyMove>>() {
        mv32.inner
    } else {
        return Err(PyTypeError::new_err("move must be Move or Move16"));
    };

    if !move_value.is_ok() {
        return Err(PyValueError::new_err("invalid move value"));
    }

    Ok(move_value)
}

fn psfen_from_vec(data: Vec<u8>) -> PyResult<[u8; 32]> {
    if data.len() != 32 {
        return Err(PyValueError::new_err("psfen must be 32 bytes"));
    }
    let mut packed = [0u8; 32];
    packed.copy_from_slice(&data);
    Ok(packed)
}

fn extract_psfen_bytes(psfen: &Bound<'_, PyAny>) -> PyResult<[u8; 32]> {
    if let Ok(data) = psfen.extract::<Vec<u8>>() {
        return psfen_from_vec(data);
    }

    if let Ok(field) = psfen.get_item("sfen") {
        if let Ok(data) = field.extract::<Vec<u8>>() {
            return psfen_from_vec(data);
        }
    }

    if let Ok(first) = psfen.get_item(0) {
        if let Ok(field) = first.get_item("sfen") {
            if let Ok(data) = field.extract::<Vec<u8>>() {
                return psfen_from_vec(data);
            }
        }
        if let Ok(data) = first.extract::<Vec<u8>>() {
            return psfen_from_vec(data);
        }
    }

    Err(PyTypeError::new_err(
        "psfen must be bytes-like, uint8[32], or PackedSfen ndarray with 'sfen' field",
    ))
}

fn fill_psfen_output(out: &Bound<'_, PyAny>, data: &[u8; 32]) -> PyResult<()> {
    if let Ok(len) = out.len() {
        if len == 32 {
            for (idx, byte) in data.iter().copied().enumerate() {
                out.set_item(idx, byte)?;
            }
            return Ok(());
        }
    }

    if let Ok(first) = out.get_item(0) {
        if let Ok(field) = first.get_item("sfen") {
            if field.len().ok() == Some(32) {
                for (idx, byte) in data.iter().copied().enumerate() {
                    field.set_item(idx, byte)?;
                }
                return Ok(());
            }
        }
    }

    Err(PyTypeError::new_err(
        "out must be writable uint8[32] or PackedSfen ndarray with 'sfen' field",
    ))
}

fn collect_py_move16s(list: &MoveList) -> Vec<PyMove16> {
    list.iter().copied().map(PyMove16::from_move16).collect()
}

fn collect_py_moves32(position: &Position, list: &MoveList) -> Vec<PyMove> {
    list.iter().copied().map(|mv| PyMove { inner: position.move_from_move16(mv) }).collect()
}

fn game_result_name(result: GameResult) -> &'static str {
    match result {
        GameResult::BlackWin => "BLACK_WIN",
        GameResult::WhiteWin => "WHITE_WIN",
        GameResult::DrawByRepetition => "DRAW_BY_REPETITION",
        GameResult::Error => "ERROR",
        GameResult::BlackWinByDeclaration => "BLACK_WIN_BY_DECLARATION",
        GameResult::WhiteWinByDeclaration => "WHITE_WIN_BY_DECLARATION",
        GameResult::DrawByMaxPlies => "DRAW_BY_MAX_PLIES",
        GameResult::Invalid => "INVALID",
        GameResult::BlackWinByForfeit => "BLACK_WIN_BY_FORFEIT",
        GameResult::WhiteWinByForfeit => "WHITE_WIN_BY_FORFEIT",
        GameResult::DrawByImpasse => "DRAW_BY_IMPASSE",
        GameResult::BlackWinByIllegalMove => "BLACK_WIN_BY_ILLEGAL_MOVE",
        GameResult::WhiteWinByIllegalMove => "WHITE_WIN_BY_ILLEGAL_MOVE",
        GameResult::Paused => "PAUSED",
        GameResult::BlackWinByTimeout => "BLACK_WIN_BY_TIMEOUT",
        GameResult::WhiteWinByTimeout => "WHITE_WIN_BY_TIMEOUT",
    }
}

fn game_result_from_value(value: u8) -> Option<GameResult> {
    match value {
        0 => Some(GameResult::BlackWin),
        1 => Some(GameResult::WhiteWin),
        2 => Some(GameResult::DrawByRepetition),
        3 => Some(GameResult::Error),
        4 => Some(GameResult::BlackWinByDeclaration),
        5 => Some(GameResult::WhiteWinByDeclaration),
        6 => Some(GameResult::DrawByMaxPlies),
        7 => Some(GameResult::Invalid),
        8 => Some(GameResult::BlackWinByForfeit),
        9 => Some(GameResult::WhiteWinByForfeit),
        10 => Some(GameResult::DrawByImpasse),
        12 => Some(GameResult::BlackWinByIllegalMove),
        13 => Some(GameResult::WhiteWinByIllegalMove),
        11 => Some(GameResult::Paused),
        16 => Some(GameResult::BlackWinByTimeout),
        17 => Some(GameResult::WhiteWinByTimeout),
        _ => None,
    }
}

#[cfg(all(test, feature = "python-tests"))]
mod tests {
    use super::*;
    use ::rshogi::board::movegen::{generate_moves, Evasions, NonEvasions};

    const STARTPOS_SFEN: &str = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
    const DOUBLE_CHECK_SFEN: &str =
        "ln2+r1r2/5s+Pkl/3+B1p1p1/p4B2p/2P6/P6PP/1PNP1P3/2G3SK1/L4G1NL w 2GSN3Ps3p 76";

    #[test]
    fn py_move_from_usi_rejects_special_moves() {
        Python::attach(|py| {
            let cls = py.get_type::<PyMove>();
            assert!(PyMove::from_usi(&cls, "7g7f").is_ok());
            assert!(PyMove::from_usi(&cls, "none").is_err());
            assert!(PyMove::from_usi(&cls, "null").is_err());
        });
    }

    #[test]
    fn pseudo_legal_moves_in_check_uses_evasions() {
        let board = PyBoard::new(Some(DOUBLE_CHECK_SFEN)).expect("board init");
        assert!(board.position.in_check(), "position should be in check for this test");

        let mut expected = MoveList::new();
        generate_moves::<Evasions>(&board.position, &mut expected);
        let actual = board.pseudo_legal_moves();

        assert_eq!(actual.len(), expected.len());
    }

    #[test]
    fn pseudo_legal_moves_not_in_check_uses_non_evasions() {
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        assert!(!board.position.in_check(), "position should not be in check");

        let mut expected = MoveList::new();
        generate_moves::<NonEvasions>(&board.position, &mut expected);
        let actual = board.pseudo_legal_moves();

        assert_eq!(actual.len(), expected.len());
    }

    #[test]
    fn in_check_and_is_mated_match_core_semantics() {
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        assert_eq!(board.in_check(), board.position.in_check());
        assert_eq!(board.is_mated(), board.position.is_mated());

        let board = PyBoard::new(Some(DOUBLE_CHECK_SFEN)).expect("board init");
        assert_eq!(board.in_check(), board.position.in_check());
        assert_eq!(board.is_mated(), board.position.is_mated());
    }

    #[test]
    fn repetition_helpers_default_to_none() {
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        assert_eq!(board.repetition_state().to_usi(), "rep_none");
        assert!(!board.is_repetition(3));
    }

    #[test]
    fn game_result_class_attrs_match_values() {
        Python::attach(|py| {
            let cls = py.get_type::<PyGameResult>();
            let black = cls
                .getattr("BLACK_WIN")
                .expect("class attr")
                .extract::<PyRef<'_, PyGameResult>>()
                .expect("game result");
            assert_eq!(black.value(), GameResult::BlackWin as u8);
        });
    }

    #[test]
    fn parse_kif_exposes_record_result() {
        let kif = "\
手合割：平手
手数----指手---------消費時間--
 1 ７六歩(77)
 2 投了
まで1手で先手の勝ち";
        let inner = parse_kif_str_record(kif).expect("parse");
        let record = PyGameRecord { inner };
        assert_eq!(record.result().result().value(), GameResult::BlackWin as u8);
        assert_eq!(record.num_moves(), 1);
    }

    #[test]
    fn book_key_from_position_works() {
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        let key = book_key_from_position(&board);
        assert_ne!(key.low(), 0);
    }

    #[test]
    fn book_key_after_matches_push() {
        Python::attach(|py| {
            let mut board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
            let move16 = PyMove16::from_move16(Move16::from_usi("7g7f").expect("valid move"));

            let key_after = book_key_after(&board, &move16);

            let py_move16 = Py::new(py, move16).expect("py wrap");
            board.apply_move(py_move16.bind(py)).expect("legal move");
            let key_current = book_key_from_position(&board);

            assert_eq!(key_after.low(), key_current.low());
            assert_eq!(key_after.high(), key_current.high());
        });
    }

    #[test]
    fn memory_book_insert_and_get() {
        let mut book = PyMemoryBook::new();
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        let key = book_key_from_position(&board);

        let move16 = PyMove16::from_move16(Move16::from_usi("7g7f").expect("valid move"));
        let book_move = PyBookMove::from_book_move(BookMove::new(move16.inner, 100, 10));

        book.insert_move(&key, &book_move);

        let entry = book.get(&key).expect("entry exists");
        assert_eq!(entry.moves().len(), 1);
        assert_eq!(entry.moves()[0].score(), 100);
    }

    #[test]
    fn static_book_roundtrip() {
        let mut memory_book = PyMemoryBook::new();
        let board = PyBoard::new(Some(STARTPOS_SFEN)).expect("board init");
        let key = book_key_from_position(&board);

        let move16 = PyMove16::from_move16(Move16::from_usi("7g7f").expect("valid move"));
        let book_move = PyBookMove::from_book_move(BookMove::new(move16.inner, 100, 10));
        memory_book.insert_move(&key, &book_move);

        Python::attach(|py| {
            let cls = py.get_type::<PyStaticBook>();
            let static_book = PyStaticBook::from_memory_book(&cls, &memory_book);
            let bytes = static_book.to_bytes(py);

            let restored =
                PyStaticBook::from_bytes(&cls, bytes.as_any()).expect("roundtrip succeeds");

            assert_eq!(restored.inner.len(), 1);
            let entry = restored.get(&key).expect("entry exists");
            assert_eq!(entry.moves().len(), 1);
        });
    }
}
