use crate::board::{Position, SfenError};
use crate::records::formats::common::refresh_position_if_needed;
use crate::records::record::{GameRecord, MoveRecord, NodeId};
use crate::records::time_control::TimeControl;
use crate::types::{Color, GameResult, HandPiece, Move, Piece, PieceType, Square};
use serde_json::{json, Map, Value};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum JkfError {
    #[error("SFEN parsing failed: {0}")]
    Sfen(#[from] SfenError),
    #[error("invalid move: {0}")]
    InvalidMove(String),
    #[error("illegal move at index {index}")]
    IllegalMove { index: usize },
    #[error("json serialization failed: {0}")]
    Json(#[from] serde_json::Error),
}

fn piece_type_to_csa(piece_type: PieceType) -> Option<&'static str> {
    match piece_type {
        PieceType::PAWN => Some("FU"),
        PieceType::LANCE => Some("KY"),
        PieceType::KNIGHT => Some("KE"),
        PieceType::SILVER => Some("GI"),
        PieceType::GOLD => Some("KI"),
        PieceType::BISHOP => Some("KA"),
        PieceType::ROOK => Some("HI"),
        PieceType::KING => Some("OU"),
        PieceType::PRO_PAWN => Some("TO"),
        PieceType::PRO_LANCE => Some("NY"),
        PieceType::PRO_KNIGHT => Some("NK"),
        PieceType::PRO_SILVER => Some("NG"),
        PieceType::HORSE => Some("UM"),
        PieceType::DRAGON => Some("RY"),
        _ => None,
    }
}

fn square_to_place(sq: Square) -> Value {
    let file = sq.file().raw() + 1;
    let rank = sq.rank().raw() + 1;
    json!({ "x": file, "y": rank })
}

fn time_ms_to_jkf(time_ms: u32) -> Value {
    let total_seconds = time_ms / 1_000;
    let hours = total_seconds / 3600;
    let minutes = (total_seconds % 3600) / 60;
    let seconds = total_seconds % 60;
    let mut now = Map::new();
    if hours > 0 {
        now.insert("h".to_string(), json!(hours));
    }
    now.insert("m".to_string(), json!(minutes));
    now.insert("s".to_string(), json!(seconds));
    let mut time = Map::new();
    time.insert("now".to_string(), Value::Object(now));
    Value::Object(time)
}

fn time_ms_to_jkf_with_total(now_ms: u32, total_ms: u32) -> Value {
    let now = time_ms_to_jkf(now_ms);
    let total_seconds = total_ms / 1_000;
    let hours = total_seconds / 3600;
    let minutes = (total_seconds % 3600) / 60;
    let seconds = total_seconds % 60;
    let mut total = Map::new();
    if hours > 0 {
        total.insert("h".to_string(), json!(hours));
    }
    total.insert("m".to_string(), json!(minutes));
    total.insert("s".to_string(), json!(seconds));
    let mut time = now.as_object().cloned().unwrap_or_else(Map::new);
    time.insert("total".to_string(), Value::Object(total));
    Value::Object(time)
}

fn format_kif_time_control_text(time_control: &TimeControl) -> Option<String> {
    let base = time_control.base_seconds();
    let byoyomi = time_control.byoyomi_seconds();
    let increment = time_control.increment_seconds();
    if base == 0 && byoyomi == 0 && increment == 0 {
        return None;
    }
    let mut parts = Vec::new();
    if base > 0 {
        let hours = base / 3600;
        let minutes = (base % 3600) / 60;
        if hours > 0 {
            parts.push(format!("{hours}時間{minutes}分"));
        } else {
            parts.push(format!("{minutes}分"));
        }
    }
    if byoyomi > 0 {
        parts.push(format!("秒読み{byoyomi}秒"));
    }
    if increment > 0 {
        parts.push(format!("加算{increment}秒"));
    }
    Some(parts.join(""))
}

fn build_move_entry(
    pos: &Position,
    mv_record: &MoveRecord,
    last_to: Option<Square>,
    total_ms: u32,
) -> Result<Value, JkfError> {
    let mv = mv_record.mv();
    let mut move_obj = Map::new();
    let color = if pos.side_to_move() == Color::BLACK { 0 } else { 1 };
    let to_sq = mv.to_sq();
    move_obj.insert("to".to_string(), square_to_place(to_sq));
    move_obj.insert("color".to_string(), json!(color));

    if mv.is_drop() {
        let dropped = mv
            .dropped_piece()
            .ok_or_else(|| JkfError::InvalidMove("drop move missing piece".to_string()))?;
        let piece_code = piece_type_to_csa(dropped)
            .ok_or_else(|| JkfError::InvalidMove("invalid drop piece".to_string()))?;
        move_obj.insert("piece".to_string(), json!(piece_code));
        move_obj.insert("relative".to_string(), json!("H"));
    } else {
        let moved = pos.moved_piece_after(mv).piece_type();
        let piece_type = if mv.is_promote() { moved.demote() } else { moved };
        let piece_code = piece_type_to_csa(piece_type)
            .ok_or_else(|| JkfError::InvalidMove("invalid piece".to_string()))?;
        move_obj.insert("piece".to_string(), json!(piece_code));
        move_obj.insert("from".to_string(), square_to_place(mv.from_sq()));
    }

    if mv.is_promote() {
        move_obj.insert("promote".to_string(), json!(true));
    } else if should_output_promote_false(pos, mv) {
        move_obj.insert("promote".to_string(), json!(false));
    }
    if last_to == Some(to_sq) {
        move_obj.insert("same".to_string(), json!(true));
    }

    let captured = pos.piece_on(to_sq);
    if captured != Piece::NO_PIECE && captured.color() != pos.side_to_move() {
        if let Some(code) = piece_type_to_csa(captured.piece_type().demote()) {
            move_obj.insert("capture".to_string(), json!(code));
        }
    }

    if !mv.is_drop() {
        let ki2 =
            mv.to_ki2(pos).ok_or_else(|| JkfError::InvalidMove("invalid move".to_string()))?;
        let relative = relative_from_ki2(&ki2);
        if !relative.is_empty() {
            move_obj.insert("relative".to_string(), json!(relative));
        }
    }

    let mut entry = Map::new();
    entry.insert("move".to_string(), Value::Object(move_obj));
    if let Some(comment) = mv_record.comment() {
        let comments: Vec<String> = comment
            .lines()
            .map(|line| line.trim().to_string())
            .filter(|line| !line.is_empty())
            .collect();
        if !comments.is_empty() {
            entry.insert("comments".to_string(), json!(comments));
        }
    }
    if let Some(time_ms) = mv_record.time_ms() {
        entry.insert("time".to_string(), time_ms_to_jkf_with_total(time_ms, total_ms));
    } else {
        entry.insert("time".to_string(), time_ms_to_jkf_with_total(0, total_ms));
    }

    Ok(Value::Object(entry))
}

fn build_jkf_line(
    record: &GameRecord,
    start: NodeId,
    pos_start: &Position,
    last_to_start: Option<Square>,
    black_total: u32,
    white_total: u32,
) -> Result<Vec<Value>, JkfError> {
    let mut pos = pos_start.clone();
    let mut last_to = last_to_start;
    let mut refresh_counter = 0usize;
    let mut out = Vec::new();
    let mut black_total_ms = black_total;
    let mut white_total_ms = white_total;
    let mut current = Some(start);
    while let Some(node_id) = current {
        let node = record.node(node_id);
        let mv_record = node
            .mv()
            .ok_or_else(|| JkfError::InvalidMove("variation node missing move".to_string()))?;
        let mv = mv_record.mv();
        if !pos.is_legal(mv) {
            return Err(JkfError::InvalidMove("illegal variation move".to_string()));
        }
        let pos_before = pos.clone();
        let last_to_before = last_to;
        let elapsed = mv_record.time_ms().unwrap_or(0);
        if pos_before.side_to_move() == Color::BLACK {
            black_total_ms = black_total_ms.saturating_add(elapsed);
        } else {
            white_total_ms = white_total_ms.saturating_add(elapsed);
        }
        let total =
            if pos_before.side_to_move() == Color::BLACK { black_total_ms } else { white_total_ms };
        let mut entry = build_move_entry(&pos_before, mv_record, last_to_before, total)?;
        if let Some(parent) = node.parent() {
            let siblings = record.children(parent);
            if siblings.first() == Some(&node_id) && siblings.len() > 1 {
                let mut forks = Vec::new();
                for sibling in siblings.iter().skip(1) {
                    let fork_moves = build_jkf_line(
                        record,
                        *sibling,
                        &pos_before,
                        last_to_before,
                        black_total_ms,
                        white_total_ms,
                    )?;
                    forks.push(fork_moves);
                }
                if let Some(obj) = entry.as_object_mut() {
                    obj.insert("forks".to_string(), json!(forks));
                }
            }
        }
        out.push(entry);
        pos.do_move(mv);
        last_to = Some(mv.to_sq());
        refresh_position_if_needed(&mut pos, &mut refresh_counter)?;
        current = record.children(node_id).first().copied();
    }
    Ok(out)
}

fn result_to_jkf_special(result: GameResult) -> Option<&'static str> {
    match result {
        GameResult::BlackWin | GameResult::WhiteWin => Some("TORYO"),
        GameResult::DrawByRepetition => Some("SENNICHITE"),
        GameResult::DrawByImpasse => Some("JISHOGI"),
        GameResult::DrawByMaxPlies => Some("MAX_MOVES"),
        GameResult::BlackWinByDeclaration | GameResult::WhiteWinByDeclaration => Some("KACHI"),
        GameResult::BlackWinByForfeit | GameResult::WhiteWinByForfeit => Some("KACHI"),
        GameResult::BlackWinByIllegalMove | GameResult::WhiteWinByIllegalMove => {
            Some("ILLEGAL_MOVE")
        }
        GameResult::BlackWinByTimeout | GameResult::WhiteWinByTimeout => Some("TIME_UP"),
        GameResult::Error => Some("ERROR"),
        GameResult::Invalid | GameResult::Paused => Some("CHUDAN"),
    }
}

fn build_initial_data(pos: &Position) -> Result<Value, JkfError> {
    let mut board: Vec<Vec<Value>> = Vec::with_capacity(9);
    for file in 1..=9u8 {
        let mut column: Vec<Value> = Vec::with_capacity(9);
        for rank in 1..=9u8 {
            let file_char = char::from(b'0' + file);
            let rank_char = char::from(b'a' + (rank - 1));
            let sq = Square::from_usi(&format!("{file_char}{rank_char}"))
                .ok_or_else(|| JkfError::InvalidMove("invalid square".to_string()))?;
            let piece = pos.piece_on(sq);
            if piece == Piece::NO_PIECE {
                column.push(json!({}));
                continue;
            }
            let color = if piece.color() == Color::BLACK { 0 } else { 1 };
            let kind = piece_type_to_csa(piece.piece_type())
                .ok_or_else(|| JkfError::InvalidMove("invalid piece".to_string()))?;
            column.push(json!({ "color": color, "kind": kind }));
        }
        board.push(column);
    }

    let mut hands = Vec::new();
    for color in [Color::BLACK, Color::WHITE] {
        let hand = pos.hand_of(color);
        let mut map = Map::new();
        for (piece_type, code) in [
            (PieceType::PAWN, "FU"),
            (PieceType::LANCE, "KY"),
            (PieceType::KNIGHT, "KE"),
            (PieceType::SILVER, "GI"),
            (PieceType::GOLD, "KI"),
            (PieceType::BISHOP, "KA"),
            (PieceType::ROOK, "HI"),
        ] {
            let hand_piece = HandPiece::from_piece_type(piece_type)
                .ok_or_else(|| JkfError::InvalidMove("invalid hand piece".to_string()))?;
            let count = hand.count(hand_piece);
            map.insert(code.to_string(), json!(count));
        }
        hands.push(Value::Object(map));
    }

    let color = if pos.side_to_move() == Color::BLACK { 0 } else { 1 };
    Ok(json!({ "board": board, "color": color, "hands": hands }))
}

fn preset_from_sfen(sfen: &str) -> Option<&'static str> {
    match sfen {
        "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1" => Some("HIRATE"),
        "lnsgkgsn1/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("KY"),
        "1nsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("KY_R"),
        "lnsgkgsnl/1r7/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("KA"),
        "lnsgkgsnl/7b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("HI"),
        "lnsgkgsn1/7b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("HIKY"),
        "lnsgkgsnl/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("2"),
        "1nsgkgsn1/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("4"),
        "2sgkgs2/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("6"),
        "3gkg3/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("8"),
        "4k4/9/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL w - 1" => Some("10"),
        _ => None,
    }
}

fn piece_type_is_promotable(piece_type: PieceType) -> bool {
    matches!(
        piece_type,
        PieceType::PAWN
            | PieceType::LANCE
            | PieceType::KNIGHT
            | PieceType::SILVER
            | PieceType::BISHOP
            | PieceType::ROOK
    )
}

fn is_promotable_rank(color: Color, rank: u8) -> bool {
    if color == Color::BLACK {
        rank <= 3
    } else {
        rank >= 7
    }
}

fn should_output_promote_false(pos: &Position, mv: Move) -> bool {
    if mv.is_drop() {
        return false;
    }
    let moved = pos.moved_piece_after(mv).piece_type();
    let piece_type = moved;
    if !piece_type_is_promotable(piece_type) {
        return false;
    }
    let from = mv.from_sq();
    let to = mv.to_sq();
    let from_rank = u8::try_from(from.rank().raw() + 1).unwrap_or(0);
    let to_rank = u8::try_from(to.rank().raw() + 1).unwrap_or(0);
    is_promotable_rank(pos.side_to_move(), from_rank)
        || is_promotable_rank(pos.side_to_move(), to_rank)
}

fn relative_from_ki2(ki2: &str) -> String {
    let mut out = String::new();
    for ch in ki2.chars() {
        match ch {
            '左' => out.push('L'),
            '直' => out.push('C'),
            '右' => out.push('R'),
            '上' => out.push('U'),
            '寄' => out.push('M'),
            '引' => out.push('D'),
            '打' => out.push('H'),
            _ => {}
        }
    }
    out
}

pub fn export_jkf(record: &GameRecord) -> Result<String, JkfError> {
    let mut pos = Position::new();
    pos.set(record.init_position_sfen())?;

    let mut header = Map::new();
    if let Some(black) = record.metadata().black_player() {
        header.insert("先手".to_string(), json!(black));
    }
    if let Some(white) = record.metadata().white_player() {
        header.insert("後手".to_string(), json!(white));
    }
    if let Some(event) = record.metadata().event() {
        header.insert("棋戦".to_string(), json!(event));
    }
    if let Some(site) = record.metadata().site() {
        header.insert("場所".to_string(), json!(site));
    }
    if let Some(start) = record.metadata().start_date() {
        header.insert("開始日時".to_string(), json!(start));
    }
    if let Some(end) = record.metadata().end_date() {
        header.insert("終了日時".to_string(), json!(end));
    }
    if let Some(tc) = record.metadata().time_control() {
        if let Some(text) = format_kif_time_control_text(tc) {
            header.insert("持ち時間".to_string(), json!(text));
        }
    }
    if !record.metadata().attributes().is_empty() {
        let mut keys: Vec<&String> = record.metadata().attributes().keys().collect();
        keys.sort();
        for key in keys {
            if header.contains_key(key) {
                continue;
            }
            if let Some(value) = record.metadata().attributes().get(key) {
                header.insert(key.to_string(), json!(value));
            }
        }
    }

    let initial = if let Some(preset) = preset_from_sfen(record.init_position_sfen()) {
        json!({ "preset": preset })
    } else {
        let data = build_initial_data(&pos)?;
        json!({ "preset": "OTHER", "data": data })
    };

    let mut moves: Vec<Value> = Vec::new();
    if let Some(comment) = record.metadata().comment() {
        let comments: Vec<String> = comment
            .lines()
            .map(|line| line.trim().to_string())
            .filter(|line| !line.is_empty())
            .collect();
        if !comments.is_empty() {
            moves.push(json!({ "comments": comments }));
        } else {
            moves.push(json!({}));
        }
    } else {
        moves.push(json!({}));
    }

    let main_ids = record.main_line_ids();
    let mut last_to: Option<Square> = None;
    let mut refresh_counter = 0usize;
    let mut black_total_ms: u32 = 0;
    let mut white_total_ms: u32 = 0;
    for (index, node_id) in main_ids.iter().enumerate() {
        let node = record.node(*node_id);
        let mv_record = node
            .mv()
            .ok_or_else(|| JkfError::InvalidMove("main line node missing move".to_string()))?;
        let mv = mv_record.mv();
        if !pos.is_legal(mv) {
            return Err(JkfError::IllegalMove { index });
        }
        let pos_before = pos.clone();
        let last_to_before = last_to;
        let elapsed = mv_record.time_ms().unwrap_or(0);
        if pos_before.side_to_move() == Color::BLACK {
            black_total_ms = black_total_ms.saturating_add(elapsed);
        } else {
            white_total_ms = white_total_ms.saturating_add(elapsed);
        }
        let total =
            if pos_before.side_to_move() == Color::BLACK { black_total_ms } else { white_total_ms };
        let mut entry = build_move_entry(&pos_before, mv_record, last_to_before, total)?;
        if let Some(parent) = node.parent() {
            let siblings = record.children(parent);
            if siblings.first() == Some(node_id) && siblings.len() > 1 {
                let mut forks = Vec::new();
                for sibling in siblings.iter().skip(1) {
                    let fork_moves = build_jkf_line(
                        record,
                        *sibling,
                        &pos_before,
                        last_to_before,
                        black_total_ms,
                        white_total_ms,
                    )?;
                    forks.push(fork_moves);
                }
                if let Some(obj) = entry.as_object_mut() {
                    obj.insert("forks".to_string(), json!(forks));
                }
            }
        }
        moves.push(entry);
        pos.do_move(mv);
        last_to = Some(mv.to_sq());
        refresh_position_if_needed(&mut pos, &mut refresh_counter)?;
    }

    if let Some(special) = result_to_jkf_special(record.result().result()) {
        moves.push(json!({ "special": special }));
    }

    let mut root = Map::new();
    if !header.is_empty() {
        root.insert("header".to_string(), Value::Object(header));
    }
    root.insert("initial".to_string(), initial);
    root.insert("moves".to_string(), Value::Array(moves));

    Ok(serde_json::to_string(&Value::Object(root))?)
}

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

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

        let jkf = export_jkf(&record).unwrap();
        let value: Value = serde_json::from_str(&jkf).unwrap();
        let moves = value["moves"].as_array().unwrap();
        assert_eq!(moves.len(), 3);
        assert_eq!(moves[1]["move"]["to"]["x"], 7);
        assert_eq!(moves[1]["move"]["to"]["y"], 6);
        assert_eq!(moves[2]["special"], "TORYO");
    }
}
