use crate::records::formats::sbinpack::{pack_ply_result, unpack_ply_result};
use crate::records::record::{GameRecord, GameRecordResult, MoveRecord};
use crate::types::Eval;
use crate::{
    board::{PackedSfen, Position, SfenError},
    types::{GameResult, Move16, MOVE_NONE},
};
use thiserror::Error;

/// Pack 形式のシリアライゼーションエラー。
#[derive(Debug, Error)]
pub enum PackError {
    #[error("SFEN parsing failed: {0}")]
    Sfen(#[from] SfenError),

    #[error("PackedSfen decoding failed")]
    PackedSfen,

    #[error("record build failed: {0}")]
    Record(#[from] crate::records::error::RecordError),

    #[error("move {0} missing eval")]
    MissingEval(usize),

    #[error("move {index} is not legal")]
    IllegalMove { index: usize },

    #[error("move {index} is invalid")]
    InvalidMove { index: usize },

    #[error("input is truncated")]
    Truncated,
}

/// `GameRecord` を pack 形式バイト列にシリアライズします。
///
/// Format (v2):
/// - Stem: PackedSfen(32) + score(i16) + best_move16(u16) + ply_result(u16)
/// - Count: u16 (number of moves)
/// - MoveText: (move16(u16), eval(i16)) * Count
pub fn serialize(record: &GameRecord) -> Result<Vec<u8>, PackError> {
    let mut pos = Position::new();
    pos.set(record.init_position_sfen())?;

    let mut data = Vec::new();
    let packed = pos.sfen_pack();
    let stem_score = stem_score_from_record(record);
    let moves: Vec<&MoveRecord> = record.main_moves().collect();
    let best_move16 = moves.first().map(|mv| mv.mv().to_move16()).unwrap_or(Move16::MOVE_NONE);
    let ply_result = pack_ply_result(record.result().result(), pos.game_ply());

    data.extend_from_slice(&packed.data);
    data.extend_from_slice(&stem_score.to_le_bytes());
    data.extend_from_slice(&best_move16.0.to_le_bytes());
    data.extend_from_slice(&ply_result.to_le_bytes());

    let count = u16::try_from(moves.len()).unwrap_or(u16::MAX);
    data.extend_from_slice(&count.to_le_bytes());

    for (index, mv_record) in moves.iter().take(count as usize).enumerate() {
        let eval = mv_record.eval().ok_or(PackError::MissingEval(index))?;
        let mv = mv_record.mv();
        if !pos.is_legal(mv) {
            return Err(PackError::IllegalMove { index });
        }
        let mv16 = mv.to_move16();
        data.extend_from_slice(&mv16.0.to_le_bytes());
        data.extend_from_slice(&eval.raw().to_le_bytes());
        pos.do_move(mv);
    }

    Ok(data)
}

/// pack 形式バイト列から `GameRecord` を復元します。
pub fn deserialize(bytes: &[u8]) -> Result<GameRecord, PackError> {
    if bytes.len() < 40 {
        return Err(PackError::Truncated);
    }
    let mut packed = PackedSfen::default();
    packed.data.copy_from_slice(&bytes[0..32]);
    let _score = i16::from_le_bytes([bytes[32], bytes[33]]);
    let _best_move16 = Move16(u16::from_le_bytes([bytes[34], bytes[35]]));
    let ply_result = u16::from_le_bytes([bytes[36], bytes[37]]);
    let count = u16::from_le_bytes([bytes[38], bytes[39]]) as usize;

    let (result_opt, ply) = unpack_ply_result(ply_result);
    let result = result_opt.unwrap_or(GameResult::Invalid);

    let mut pos = Position::new();
    pos.set_from_packed_sfen(&packed, false, ply).map_err(|_| PackError::PackedSfen)?;
    let init_sfen = pos.sfen(None);

    let mut offset = 40usize;
    let mut moves = Vec::with_capacity(count);
    for index in 0..count {
        if offset + 4 > bytes.len() {
            return Err(PackError::Truncated);
        }
        let mv16 = Move16(u16::from_le_bytes([bytes[offset], bytes[offset + 1]]));
        let eval = i16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]);
        offset += 4;

        let mv = pos.move_from_move16(mv16);
        if mv == MOVE_NONE {
            return Err(PackError::InvalidMove { index });
        }
        if !pos.is_legal(mv) {
            return Err(PackError::IllegalMove { index });
        }
        moves.push(MoveRecord::new(mv, Some(Eval::from_raw(eval))));
        pos.do_move(mv);
    }

    let record_result = GameRecordResult::new(result);
    Ok(GameRecord::from_main_line(init_sfen, moves, record_result)?)
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::records::record::{GameRecord, GameRecordResult, MoveRecord};
    use crate::{
        board,
        records::formats::sbinpack::pack_ply_result,
        types::{Eval, GameResult, Move16},
    };

    const STARTING_SFEN: &str = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";

    #[test]
    fn serialize_start_position_without_moves() {
        let record = GameRecord::from_main_line(
            STARTING_SFEN.to_string(),
            Vec::new(),
            GameRecordResult::new(GameResult::DrawByMaxPlies),
        )
        .unwrap();

        let bytes = serialize(&record).unwrap();
        let pos = board::hirate_position();
        let expected_ply_result = pack_ply_result(GameResult::DrawByMaxPlies, pos.game_ply());
        assert_eq!(bytes.len(), 38 + 2);
        assert_eq!(&bytes[0..32], &pos.sfen_pack().data);
        assert_eq!(i16::from_le_bytes([bytes[32], bytes[33]]), 0);
        assert_eq!(u16::from_le_bytes([bytes[34], bytes[35]]), Move16::MOVE_NONE.0);
        assert_eq!(u16::from_le_bytes([bytes[36], bytes[37]]), expected_ply_result);
        assert_eq!(u16::from_le_bytes([bytes[38], bytes[39]]), 0);
    }

    #[test]
    fn missing_eval_is_error() {
        let pos = board::hirate_position();
        let mv16 = Move16::from_usi("7g7f").unwrap();
        let mv = pos.move_from_move16(mv16);
        let record = GameRecord::from_main_line(
            STARTING_SFEN.to_string(),
            vec![MoveRecord::new(mv, None)],
            GameRecordResult::new(GameResult::BlackWin),
        )
        .unwrap();

        assert!(matches!(serialize(&record), Err(PackError::MissingEval(0))));
    }

    #[test]
    fn non_start_position_writes_ply_result() {
        let extra_sfen = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPP1PPPP/1B5R1/LNSGKGSNL b - 3";

        let record = GameRecord::from_main_line(
            extra_sfen.to_string(),
            Vec::new(),
            GameRecordResult::new(GameResult::DrawByRepetition),
        )
        .unwrap();

        let bytes = serialize(&record).unwrap();
        let mut pos = board::hirate_position();
        pos.set(extra_sfen).unwrap();
        let expected_ply_result = pack_ply_result(GameResult::DrawByRepetition, pos.game_ply());
        assert_eq!(&bytes[0..32], &pos.sfen_pack().data);
        assert_eq!(u16::from_le_bytes([bytes[36], bytes[37]]), expected_ply_result);
        assert_eq!(u16::from_le_bytes([bytes[38], bytes[39]]), 0);
    }

    #[test]
    fn invalid_result_is_allowed() {
        let record = GameRecord::from_main_line(
            STARTING_SFEN.to_string(),
            Vec::new(),
            GameRecordResult::new(GameResult::Error),
        )
        .unwrap();

        assert!(serialize(&record).is_ok());
    }

    #[test]
    fn serialize_deserialize_roundtrip() {
        let pos = board::hirate_position();
        let mv16 = Move16::from_usi("7g7f").unwrap();
        let mv = pos.move_from_move16(mv16);
        let record = GameRecord::from_main_line(
            STARTING_SFEN.to_string(),
            vec![MoveRecord::new(mv, Some(Eval::from_i32(120)))],
            GameRecordResult::new(GameResult::BlackWin),
        )
        .unwrap();

        let bytes = serialize(&record).unwrap();
        let decoded = deserialize(&bytes).unwrap();
        assert_eq!(decoded.init_position_sfen(), record.init_position_sfen());
        let decoded_moves = decoded.moves();
        let original_moves = record.moves();
        assert_eq!(decoded_moves.len(), original_moves.len());
        assert_eq!(decoded_moves[0].mv().to_usi(), original_moves[0].mv().to_usi());
        assert_eq!(decoded_moves[0].eval(), original_moves[0].eval());
    }
}
