use std::io::{self, IsTerminal};
use std::net::SocketAddr;
use std::time::{Duration, Instant};

use clap::Parser;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use flatland_client_lib::{
    connect, default_server_addr, load_client_settings, AutoNavigator, ConnectOptions, GameClient,
    RemoteSession, RotationEditorMode,
};
use flatland_protocol::ChatChannel;
use flatland_tui::{
    draw, mouse_to_world, spawn_input_reader, ActiveOverlay, HudViewMode, InputAction,
    MapTargetState, MovementState, PlayHud, EDITOR_ABILITIES, next_custom_preset,
    preset_deletable,
};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Alignment, Constraint, Direction, Layout};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Terminal;
use tracing::info;

const RECONNECT_INITIAL: Duration = Duration::from_millis(300);
const RECONNECT_MAX: Duration = Duration::from_secs(4);

#[derive(Debug, Parser)]
#[command(about = "Ratatui play client — connect to flatland-server")]
pub struct PlayArgs {
    /// Existing character name (must already exist — use `flatland3 character create`)
    #[arg(short, long)]
    pub name: Option<String>,

    /// Game server address (flatland-server TCP)
    #[arg(long, default_value_t = default_server_addr())]
    pub server: SocketAddr,

    /// Exit when the server disconnects instead of auto-reconnecting
    #[arg(long)]
    pub no_reconnect: bool,
}

#[derive(Debug, Clone)]
enum PlayExit {
    UserQuit,
    Disconnected,
    Interrupt,
}

pub async fn run_with_options(opts: ConnectOptions, auto_reconnect: bool) -> anyhow::Result<()> {
    if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
        anyhow::bail!(
            "play requires an interactive terminal (stdin/stdout TTY).\n\
             Run from kitty/iTerm/Terminal.app, not a pipe or background job.\n\
             To verify the server: cargo test -p flatland-client-lib --test connect_stress -- --nocapture"
        );
    }

    let keyboard_enhanced = crate::terminal::enter()?;

    let (exit, disconnect_reason) = {
        let _terminal = crate::terminal::Guard;

        while event::poll(Duration::from_millis(0)).unwrap_or(false) {
            let _ = event::read();
        }

        let mut key_rx = spawn_input_reader(Duration::from_millis(50));
        let backend = CrosstermBackend::new(io::stdout());
        let mut ratatui = Terminal::new(backend)?;

        let mut movement_state = MovementState::with_keys(load_client_settings().keys);
        let mut frame_interval = tokio::time::interval(Duration::from_millis(33));
        frame_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        frame_interval.tick().await;
        let mut intent_interval = tokio::time::interval(Duration::from_millis(100));
        intent_interval.tick().await;
        let mut chat_buffer = String::new();
        let mut chat_whisper = false;
        let mut chat_mode = false;
        let mut show_help = false;
        let mut hud_view_mode = HudViewMode::default();
        let mut block_active = false;
        let mut map_target = MapTargetState::default();
        let mut auto_nav: Option<AutoNavigator> = None;
        let mut mouse_hover: Option<(f32, f32)> = None;
        let session_started = Instant::now();
        let mut was_moving = false;

        let mut client = if auto_reconnect {
            wait_for_server(&opts, &mut ratatui).await?
        } else {
            connect_client(&opts).await?
        };
        paint_world(
            &mut ratatui,
            &client,
            &movement_state,
            &map_target,
            &auto_nav,
            mouse_hover,
            "",
            false,
            hud_view_mode,
        )?;

        info!(
            entity_id = client.entity_id(),
            keyboard_enhanced,
            "connected — ? for keys, Ctrl+q quit"
        );
        client.state.push_log("Click map to walk; Q/E/Z/C diagonals; , harvest; [ craft.");
        if !keyboard_enhanced {
            client.state.push_log(
                "Terminal may omit key-release — hold briefly for key-repeat; Space hard-stops. iTerm/Kitty/WezTerm feel snappier.",
            );
        }

        let mut disconnect_reason: Option<String> = None;
        let exit = 'outer: loop {
            let mut last_server_tick = client.state.tick;
            let mut stall_logged = false;
            let mut stall_since = Instant::now();

            let session_exit = 'session: {
                loop {
                    let tick_before = client.state.tick;
                    client.drain_events();
                    if client.state.tick != tick_before {
                        last_server_tick = client.state.tick;
                        stall_logged = false;
                        stall_since = Instant::now();
                    } else if client.state.connected
                        && !stall_logged
                        && stall_since.elapsed() >= Duration::from_secs(3)
                    {
                        client.state.push_log(format!(
                            "[dbg] no server tick for 3s (stuck at tick {}, received {} ticks)",
                            last_server_tick, client.state.ticks_received
                        ));
                        stall_logged = true;
                    }

                    let chat_line = if chat_mode {
                        let prefix = if chat_whisper { "whisper> " } else { "say> " };
                        format!("{prefix}{chat_buffer}")
                    } else {
                        String::new()
                    };

                    if !client.state.connected {
                        paint_world(
                            &mut ratatui,
                            &client,
                            &movement_state,
                            &map_target,
                            &auto_nav,
                            mouse_hover,
                            &chat_line,
                            false,
                            hud_view_mode,
                        )?;
                        break 'session PlayExit::Disconnected;
                    }

                    let movement = movement_state.current();
                    tokio::select! {
                        _ = frame_interval.tick() => {
                            client.drain_events();
                        }
                        _ = intent_interval.tick() => {
                            movement_state.expire_idle(movement_state.idle_timeout());
                            if !chat_mode
                                && !client.state.show_craft_menu
                                && !client.state.show_inventory_menu
                                && !client.state.show_loadout_menu
                                && !client.state.show_rotation_editor
                                && !map_target.active
                                && client.state.connected
                                && client.state.is_alive()
                            {
                                let (forward, strafe, vertical, sprint) =
                                    if let Some(mut nav) = auto_nav.take() {
                                        let (px, py) = client.state.player_position();
                                        let blocked = if nav.note_progress(px, py) {
                                            if nav.replan(&client.state) {
                                                client.state.push_log("Re-routing around obstacle…");
                                                false
                                            } else {
                                                client.state.push_log("Auto-walk blocked.");
                                                true
                                            }
                                        } else {
                                            false
                                        };
                                        if blocked {
                                            (0.0, 0.0, 0.0, false)
                                        } else {
                                            match nav.steer(px, py) {
                                                Some((f, s, sprint)) => {
                                                    auto_nav = Some(nav);
                                                    (f, s, 0.0, sprint)
                                                }
                                                None => {
                                                    client.state.push_log("Arrived at map target.");
                                                    (0.0, 0.0, 0.0, false)
                                                }
                                            }
                                        }
                                    } else {
                                        let (forward, strafe) = movement.components();
                                        (
                                            forward,
                                            strafe,
                                            movement_state.vertical_axis(),
                                            movement_state.sprinting(),
                                        )
                                    };
                                let moving = forward.abs() > f32::EPSILON
                                    || strafe.abs() > f32::EPSILON
                                    || vertical.abs() > f32::EPSILON;
                                if moving {
                                    if let Err(err) = client
                                        .move_by(forward, strafe, vertical, sprint)
                                        .await
                                    {
                                        client.state.push_log(format!("Move failed: {err}"));
                                    }
                                    was_moving = true;
                                } else if was_moving {
                                    if let Err(err) = client.stop().await {
                                        client.state.push_log(format!("Stop failed: {err}"));
                                    }
                                    was_moving = false;
                                }
                            }
                        }
                        _ = tokio::signal::ctrl_c() => {
                            break 'session PlayExit::Interrupt;
                        }
                        maybe_ev = key_rx.recv() => {
                            // Drain queued mouse-moves so hover/map cursor uses the
                            // latest position (avoids lag when the channel backs up).
                            // Clicks and keys are never skipped.
                            let mut ev = maybe_ev;
                            let mut last_move: Option<Event> = None;
                            if let Some(Event::Mouse(m)) = &ev {
                                if matches!(
                                    m.kind,
                                    MouseEventKind::Moved | MouseEventKind::Drag(_)
                                ) {
                                    last_move = ev.take();
                                }
                            }
                            while let Ok(next) = key_rx.try_recv() {
                                match &next {
                                    Event::Mouse(m)
                                        if matches!(
                                            m.kind,
                                            MouseEventKind::Moved | MouseEventKind::Drag(_)
                                        ) =>
                                    {
                                        last_move = Some(next);
                                    }
                                    _ => {
                                        ev = Some(next);
                                        break;
                                    }
                                }
                            }
                            if ev.is_none() {
                                ev = last_move.take();
                            } else if let Some(moved) = last_move.take() {
                                // Apply latest hover before the click/key that followed.
                                if let Event::Mouse(mouse) = moved {
                                    let area = ratatui.size()?;
                                    let term = ratatui::layout::Rect {
                                        x: 0,
                                        y: 0,
                                        width: area.width,
                                        height: area.height,
                                    };
                                    let hud = play_hud(
                                        &movement_state,
                                        &client,
                                        &map_target,
                                        &auto_nav,
                                        mouse_hover,
                                        hud_view_mode,
                                    );
                                    if let Some((hx, hy)) = mouse_to_world(
                                        term,
                                        mouse.column,
                                        mouse.row,
                                        &client.state,
                                        &hud,
                                    ) {
                                        mouse_hover = Some((hx, hy));
                                        if map_target.active {
                                            map_target.cursor_x = hx;
                                            map_target.cursor_y = hy;
                                        }
                                    } else {
                                        mouse_hover = None;
                                    }
                                }
                            }
                            match ev {
                            Some(Event::Mouse(mouse)) => {
                                let area = ratatui.size()?;
                                let term = ratatui::layout::Rect {
                                    x: 0,
                                    y: 0,
                                    width: area.width,
                                    height: area.height,
                                };
                                let hud = play_hud(
                                    &movement_state,
                                    &client,
                                    &map_target,
                                    &auto_nav,
                                    mouse_hover,
                                    hud_view_mode,
                                );
                                match mouse.kind {
                                    MouseEventKind::Moved | MouseEventKind::Drag(_) => {
                                        if let Some((hx, hy)) = mouse_to_world(
                                            term,
                                            mouse.column,
                                            mouse.row,
                                            &client.state,
                                            &hud,
                                        ) {
                                            mouse_hover = Some((hx, hy));
                                            if map_target.active {
                                                map_target.cursor_x = hx;
                                                map_target.cursor_y = hy;
                                            }
                                        } else {
                                            mouse_hover = None;
                                        }
                                    }
                                    MouseEventKind::Down(MouseButton::Left) => {
                                        if chat_mode
                                            || client.state.show_craft_menu
                                            || client.state.show_inventory_menu
                                            || client.state.show_loadout_menu
                                            || client.state.show_rotation_editor
                                            || show_help
                                        {
                                            continue;
                                        }
                                        if let Some((gx, gy)) = mouse_to_world(
                                            term,
                                            mouse.column,
                                            mouse.row,
                                            &client.state,
                                            &hud,
                                        ) {
                                            if map_target.active {
                                                // Place the yellow X; Enter walks.
                                                map_target.cursor_x = gx;
                                                map_target.cursor_y = gy;
                                                mouse_hover = Some((gx, gy));
                                                client.state.push_log(format!(
                                                    "Map target set ({gx:.1}, {gy:.1}) — Enter to walk."
                                                ));
                                                continue;
                                            }
                                            // Plan first (CPU-only), then stop — keeps UI snappy.
                                            match AutoNavigator::plan(&client.state, gx, gy) {
                                                Some(nav) => {
                                                    movement_state.reset();
                                                    was_moving = false;
                                                    auto_nav = Some(nav);
                                                    let _ = client.stop().await;
                                                    client.state.push_log(format!(
                                                        "Walking to ({gx:.1}, {gy:.1})…"
                                                    ));
                                                }
                                                None => {
                                                    client.state.push_log(
                                                        "No path to that map cell.",
                                                    );
                                                }
                                            }
                                        }
                                    }
                                    MouseEventKind::Down(MouseButton::Right) => {
                                        if map_target.active {
                                            map_target.deactivate();
                                            movement_state.reset();
                                            was_moving = false;
                                            client.state.push_log("Map target cancelled.");
                                            continue;
                                        }
                                        auto_nav = None;
                                        movement_state.reset();
                                        was_moving = false;
                                        if let Err(err) = client.stop().await {
                                            client.state.push_log(format!("Stop failed: {err}"));
                                        }
                                    }
                                    _ => {}
                                }
                            }
                            Some(Event::Key(key)) => {
                                if key.kind == KeyEventKind::Press && key.code == KeyCode::Esc {
                                    if map_target.active {
                                        map_target.deactivate();
                                        movement_state.reset();
                                        was_moving = false;
                                    } else if chat_mode {
                                        chat_mode = false;
                                        chat_buffer.clear();
                                    } else if show_help {
                                        show_help = false;
                                    } else if client.back_on_esc() {
                                        movement_state.reset();
                                        was_moving = false;
                                    }
                                    continue;
                                }

                                if show_help {
                                    if key.kind == KeyEventKind::Press
                                        && (key.code == KeyCode::Char('?')
                                            || key.code == KeyCode::Esc)
                                    {
                                        show_help = false;
                                    }
                                    continue;
                                }

                                if chat_mode {
                                    match key.kind {
                                        KeyEventKind::Press => match key.code {
                                            KeyCode::Enter => {
                                                if !chat_buffer.trim().is_empty() {
                                                    let channel = if chat_whisper {
                                                        ChatChannel::WhisperStone
                                                    } else {
                                                        ChatChannel::Nearby
                                                    };
                                                    if let Err(err) =
                                                        client.say(channel, chat_buffer.trim()).await
                                                    {
                                                        client.state.push_log(format!("Chat failed: {err}"));
                                                    }
                                                }
                                                chat_mode = false;
                                                chat_buffer.clear();
                                            }
                                            KeyCode::Backspace => {
                                                chat_buffer.pop();
                                            }
                                            KeyCode::Char(c) => {
                                                if chat_buffer.len() < 200 {
                                                    chat_buffer.push(c);
                                                }
                                            }
                                            _ => {}
                                        },
                                        _ => {}
                                    }
                                    continue;
                                }

                                if client.state.show_inventory_menu {
                                    if key.kind == KeyEventKind::Press {
                                        if client.state.show_rename_prompt {
                                            match key.code {
                                                KeyCode::Esc => client.cancel_rename_prompt(),
                                                KeyCode::Enter => {
                                                    if let Err(err) =
                                                        client.confirm_rename_prompt().await
                                                    {
                                                        client.state.push_log(format!("Rename: {err}"));
                                                    }
                                                }
                                                KeyCode::Backspace => {
                                                    client.state.rename_buffer.pop();
                                                }
                                                KeyCode::Char(c) => {
                                                    if client.state.rename_buffer.len() < 32 {
                                                        client.state.rename_buffer.push(c);
                                                    }
                                                }
                                                _ => {}
                                            }
                                        } else if client.state.show_move_picker {
                                            match key.code {
                                                KeyCode::Esc => client.close_move_picker(),
                                                KeyCode::Up | KeyCode::Char('k') => {
                                                    client.inventory_menu_move(-1);
                                                }
                                                KeyCode::Down | KeyCode::Char('j') => {
                                                    client.inventory_menu_move(1);
                                                }
                                                KeyCode::Char('[') | KeyCode::Char('-') => {
                                                    client.move_picker_adjust_quantity(-1);
                                                }
                                                KeyCode::Char(']') | KeyCode::Char('=') => {
                                                    client.move_picker_adjust_quantity(1);
                                                }
                                                KeyCode::Char('a') => {
                                                    client.move_picker_set_quantity_max();
                                                }
                                                KeyCode::Enter => {
                                                    if let Err(err) =
                                                        client.activate_inventory_selection().await
                                                    {
                                                        client
                                                            .state
                                                            .push_log(format!("Move failed: {err}"));
                                                    }
                                                }
                                                _ => {}
                                            }
                                        } else {
                                            match key.code {
                                                KeyCode::Char('b') => {
                                                    client.close_inventory_menu();
                                                }
                                                KeyCode::Up | KeyCode::Char('k') => {
                                                    client.inventory_menu_move(-1);
                                                }
                                                KeyCode::Down | KeyCode::Char('j') => {
                                                    client.inventory_menu_move(1);
                                                }
                                                KeyCode::Enter => {
                                                    if let Err(err) =
                                                        client.activate_inventory_selection().await
                                                    {
                                                        client.state.push_log(format!("{err}"));
                                                    }
                                                }
                                                KeyCode::Char('m') => {
                                                    if let Err(err) = client.open_move_picker() {
                                                        client.state.push_log(format!("{err}"));
                                                    }
                                                }
                                                KeyCode::Char('n') => {
                                                    if let Err(err) = client.open_rename_prompt() {
                                                        client.state.push_log(format!("{err}"));
                                                    }
                                                }
                                                KeyCode::Char('d') => {
                                                    if let Err(err) = client.drop_selected().await {
                                                        client.state.push_log(format!("Drop: {err}"));
                                                    }
                                                }
                                                KeyCode::Char('l') => {
                                                    if let Err(err) =
                                                        client.toggle_chest_lock_for_selection().await
                                                    {
                                                        client.state.push_log(format!("Lock: {err}"));
                                                    }
                                                }
                                                KeyCode::Char('u') => {
                                                    if let Err(err) = client.unequip_mainhand().await
                                                    {
                                                        client
                                                            .state
                                                            .push_log(format!("Unequip: {err}"));
                                                    }
                                                }
                                                _ => {}
                                            }
                                        }
                                    }
                                    continue;
                                }

                                if client.state.show_craft_menu {
                                    if key.kind == KeyEventKind::Press {
                                        match key.code {
                                            KeyCode::Esc | KeyCode::Char('[') | KeyCode::Char('c') => {
                                                client.close_craft_menu();
                                            }
                                            KeyCode::Up | KeyCode::Char('k') => {
                                                client.craft_menu_move(-1);
                                            }
                                            KeyCode::Down | KeyCode::Char('j') => {
                                                client.craft_menu_move(1);
                                            }
                                            KeyCode::Char('-') => {
                                                client.craft_batch_adjust_quantity(-1);
                                            }
                                            KeyCode::Char('=') => {
                                                client.craft_batch_adjust_quantity(1);
                                            }
                                            KeyCode::Char('a') => {
                                                client.craft_batch_set_max();
                                            }
                                            KeyCode::Enter => {
                                                if let Err(err) = client.craft_menu_selection().await {
                                                    client.state.push_log(format!("Craft failed: {err}"));
                                                }
                                            }
                                            _ => {}
                                        }
                                    }
                                    continue;
                                }

                                let overlay = if client.state.show_loadout_menu {
                                    ActiveOverlay::Loadout
                                } else if client.state.show_rotation_editor {
                                    ActiveOverlay::RotationEditor(client.state.rotation_editor.mode)
                                } else {
                                    ActiveOverlay::None
                                };
                                let input_action = movement_state.apply_key(
                                    key,
                                    overlay,
                                    map_target.active,
                                );
                                match input_action {
                                    InputAction::Quit => {
                                        if session_started.elapsed() < Duration::from_millis(400) {
                                            continue;
                                        }
                                        break 'session PlayExit::UserQuit;
                                    }
                                    InputAction::Harvest => {
                                        match client.harvest_nearest().await {
                                            Ok(()) => {}
                                            Err(err) => {
                                                client.state.push_log(format!("Harvest failed: {err}"));
                                            }
                                        }
                                    }
                                    InputAction::Pickup => {
                                        match client.pickup_nearest().await {
                                            Ok(()) => {}
                                            Err(_) => {
                                                if let Err(err) =
                                                    client.pickup_nearest_container().await
                                                {
                                                    client
                                                        .state
                                                        .push_log(format!("Pickup: {err}"));
                                                }
                                            }
                                        }
                                    }
                                    InputAction::Craft => {
                                        client.open_craft_menu();
                                    }
                                    InputAction::Interact => {
                                        if let Err(err) = client.interact_nearest().await {
                                            client.state.push_log(format!("Interact failed: {err}"));
                                        }
                                    }
                                    InputAction::TestDamage => {
                                        if let Err(err) = client.test_damage(25.0).await {
                                            client.state.push_log(format!("Damage failed: {err}"));
                                        }
                                    }
                                    InputAction::CycleCombatTarget { reverse } => {
                                        if let Err(err) = client.cycle_combat_target(reverse).await {
                                            client.state.push_log(format!("T1 target: {err}"));
                                        }
                                    }
                                    InputAction::CycleCombatTargetT2 { reverse } => {
                                        if let Err(err) =
                                            client.cycle_combat_target_slot(2, reverse).await
                                        {
                                            client.state.push_log(format!("T2 target: {err}"));
                                        }
                                    }
                                    InputAction::AdvanceRotationT1 => {
                                        if let Err(err) = client.advance_rotation(1).await {
                                            client.state.push_log(format!("T1 step: {err}"));
                                        }
                                    }
                                    InputAction::AdvanceRotationT2 => {
                                        if let Err(err) = client.advance_rotation(2).await {
                                            client.state.push_log(format!("T2 step: {err}"));
                                        }
                                    }
                                    InputAction::ToggleAutoT1 => {
                                        if let Err(err) = client.toggle_auto_attack_slot(1).await {
                                            client.state.push_log(format!("T1 auto: {err}"));
                                        }
                                    }
                                    InputAction::ToggleAutoT2 => {
                                        if let Err(err) = client.toggle_auto_attack_slot(2).await {
                                            client.state.push_log(format!("T2 auto: {err}"));
                                        }
                                    }
                                    InputAction::ToggleLoadout => {
                                        client.state.show_rotation_editor = false;
                                        client.state.rotation_editor.reset();
                                        client.state.show_loadout_menu =
                                            !client.state.show_loadout_menu;
                                        if client.state.show_loadout_menu {
                                            movement_state.reset();
                                            if was_moving {
                                                if let Err(err) = client.stop().await {
                                                    client.state.push_log(format!("Stop failed: {err}"));
                                                }
                                                was_moving = false;
                                            }
                                        }
                                    }
                                    InputAction::ToggleRotationEditor => {
                                        client.state.show_loadout_menu = false;
                                        if client.state.show_rotation_editor {
                                            client.state.show_rotation_editor = false;
                                            client.state.rotation_editor.reset();
                                        } else {
                                            client.state.show_rotation_editor = true;
                                            client.state.rotation_editor.reset();
                                            let max = client.state.rotation_presets.len();
                                            if max > 0 {
                                                client.state.rotation_editor.list_index =
                                                    client.state.rotation_editor.list_index.min(max - 1);
                                            }
                                        }
                                        if client.state.show_rotation_editor {
                                            movement_state.reset();
                                            if was_moving {
                                                if let Err(err) = client.stop().await {
                                                    client.state.push_log(format!("Stop failed: {err}"));
                                                }
                                                was_moving = false;
                                            }
                                        }
                                    }
                                    InputAction::LoadoutMenuUp => {
                                        if client.state.show_loadout_menu
                                            && client.state.loadout_menu_index > 0
                                        {
                                            client.state.loadout_menu_index -= 1;
                                        }
                                    }
                                    InputAction::LoadoutMenuDown => {
                                        if client.state.show_loadout_menu {
                                            let max = client.state.rotation_presets.len();
                                            if max > 0 {
                                                client.state.loadout_menu_index =
                                                    (client.state.loadout_menu_index + 1).min(max - 1);
                                            }
                                        }
                                    }
                                    InputAction::LoadoutAssignT1 => {
                                        let preset_id = {
                                            let idx = client.state.loadout_menu_index;
                                            client
                                                .state
                                                .rotation_presets
                                                .get(idx)
                                                .map(|p| p.id.clone())
                                        };
                                        if let Some(preset_id) = preset_id {
                                            if let Err(err) =
                                                client.assign_slot_preset(1, &preset_id).await
                                            {
                                                client.state.push_log(format!("Loadout: {err}"));
                                            }
                                        }
                                    }
                                    InputAction::LoadoutAssignT2 => {
                                        let preset_id = {
                                            let idx = client.state.loadout_menu_index;
                                            client
                                                .state
                                                .rotation_presets
                                                .get(idx)
                                                .map(|p| p.id.clone())
                                        };
                                        if let Some(preset_id) = preset_id {
                                            if let Err(err) =
                                                client.assign_slot_preset(2, &preset_id).await
                                            {
                                                client.state.push_log(format!("Loadout: {err}"));
                                            }
                                        }
                                    }
                                    InputAction::CloseOverlay => {
                                        client.state.show_loadout_menu = false;
                                        client.state.show_rotation_editor = false;
                                        client.state.rotation_editor.reset();
                                        movement_state.reset();
                                        was_moving = false;
                                    }
                                    InputAction::ClearCombatTarget => {
                                        if let Err(err) = client.clear_combat_target().await {
                                            client.state.push_log(format!("Target: {err}"));
                                        }
                                    }
                                    InputAction::Dodge => {
                                        auto_nav = None;
                                        if let Err(err) = client.dodge().await {
                                            client.state.push_log(format!("Dodge: {err}"));
                                        }
                                    }
                                    InputAction::Lunge => {
                                        auto_nav = None;
                                        movement_state.reset();
                                        was_moving = false;
                                        if let Err(err) = client.lunge().await {
                                            client.state.push_log(format!("Lunge: {err}"));
                                        }
                                    }
                                    InputAction::ToggleSprintMode => {
                                        movement_state.toggle_sprint_mode();
                                        let on = if movement_state.sprint_mode() {
                                            "on"
                                        } else {
                                            "off"
                                        };
                                        client
                                            .state
                                            .push_log(format!("Sprint mode {on}."));
                                    }
                                    InputAction::ToggleMapTarget => {
                                        auto_nav = None;
                                        if map_target.active {
                                            map_target.deactivate();
                                            movement_state.reset();
                                            was_moving = false;
                                            if let Err(err) = client.stop().await {
                                                client.state.push_log(format!("Stop failed: {err}"));
                                            }
                                        } else {
                                            let (px, py) = client.state.player_position();
                                            map_target.activate_at(px, py);
                                            movement_state.reset();
                                            was_moving = false;
                                            if let Err(err) = client.stop().await {
                                                client.state.push_log(format!("Stop failed: {err}"));
                                            }
                                            client.state.push_log(
                                                "Map target: click/WASD to place X, Enter to walk, Esc/right-click cancel.",
                                            );
                                        }
                                    }
                                    InputAction::ConfirmMapTarget => {
                                        let goal_x = map_target.cursor_x;
                                        let goal_y = map_target.cursor_y;
                                        map_target.deactivate();
                                        movement_state.reset();
                                        was_moving = false;
                                        match AutoNavigator::plan(&client.state, goal_x, goal_y) {
                                            Some(nav) => {
                                                auto_nav = Some(nav);
                                                client.state.push_log(format!(
                                                    "Walking to ({goal_x:.1}, {goal_y:.1})…"
                                                ));
                                            }
                                            None => {
                                                client.state.push_log(
                                                    "No path to that map target.",
                                                );
                                            }
                                        }
                                    }
                                    InputAction::CancelMapTarget => {
                                        map_target.deactivate();
                                        movement_state.reset();
                                        was_moving = false;
                                        if let Err(err) = client.stop().await {
                                            client.state.push_log(format!("Stop failed: {err}"));
                                        }
                                    }
                                    InputAction::MapTargetNudge { dx, dy } => {
                                        map_target.nudge(
                                            dx,
                                            dy,
                                            client.state.world_width_m,
                                            client.state.world_height_m,
                                        );
                                    }
                                    InputAction::CancelAutoNav | InputAction::StopMovement => {
                                        auto_nav = None;
                                        movement_state.reset();
                                        was_moving = false;
                                        if let Err(err) = client.stop().await {
                                            client.state.push_log(format!("Stop failed: {err}"));
                                        }
                                    }
                                    InputAction::CastHealTouch => {
                                        let target = client
                                            .state
                                            .target_for_slot(2)
                                            .unwrap_or(client.state.entity_id);
                                        if let Err(err) =
                                            client.cast_ability("heal_touch", Some(target)).await
                                        {
                                            client.state.push_log(format!("Heal: {err}"));
                                        }
                                    }
                                    InputAction::CastPoisonDart => {
                                        let target = client.state.target_for_slot(1);
                                        if let Err(err) =
                                            client.cast_ability("poison_dart", target).await
                                        {
                                            client.state.push_log(format!("Poison: {err}"));
                                        }
                                    }
                                    InputAction::ToggleBlock => {
                                        block_active = !block_active;
                                        if let Err(err) = client.set_block(block_active).await {
                                            client.state.push_log(format!("Block: {err}"));
                                            block_active = false;
                                        }
                                    }
                                    InputAction::ToggleStats => {
                                        client.toggle_stats();
                                    }
                                    InputAction::ToggleInventory => {
                                        client.toggle_inventory_menu();
                                    }
                                    InputAction::ToggleHelp => {
                                        show_help = !show_help;
                                        if show_help {
                                            client.close_overlays();
                                            chat_mode = false;
                                            chat_buffer.clear();
                                        }
                                    }
                                    InputAction::CycleHudView => {
                                        hud_view_mode = hud_view_mode.cycle();
                                        client.state.push_log(format!(
                                            "HUD view: {} (.)",
                                            hud_view_mode.label()
                                        ));
                                    }
                                    InputAction::StartChat { whisper } => {
                                        if whisper && !client.state.inventory.contains_key("whisper_stone") {
                                            client.state.push_log("Need whisper_stone (harvest whisper shrine)");
                                        } else {
                                            chat_mode = true;
                                            chat_whisper = whisper;
                                            chat_buffer.clear();
                                        }
                                    }
                                    InputAction::SubmitChat | InputAction::CancelChat => {}
                                    InputAction::RotationEditorListUp
                                    | InputAction::RotationEditorListDown
                                    | InputAction::RotationEditorEdit
                                    | InputAction::RotationEditorNew
                                    | InputAction::RotationEditorDelete
                                    | InputAction::RotationEditorBack
                                    | InputAction::RotationEditorAddAbility
                                    | InputAction::RotationEditorRemoveAbility
                                    | InputAction::RotationEditorMoveAbilityUp
                                    | InputAction::RotationEditorMoveAbilityDown
                                    | InputAction::RotationEditorAbilityUp
                                    | InputAction::RotationEditorAbilityDown
                                    | InputAction::RotationEditorPickerUp
                                    | InputAction::RotationEditorPickerDown
                                    | InputAction::RotationEditorPickAbility
                                    | InputAction::RotationEditorRename
                                    | InputAction::RotationEditorConfirmLabel
                                    | InputAction::RotationEditorLabelBackspace
                                    | InputAction::RotationEditorLabelChar(_)
                                    | InputAction::RotationEditorSave => {
                                        handle_rotation_editor_action(&mut client, input_action).await;
                                    }
                                    InputAction::None => {
                                        // Immediate intent sync on key press/partial release
                                        // (don't wait for the 100ms movement tick).
                                        if !chat_mode
                                            && !map_target.active
                                            && !client.state.show_craft_menu
                                            && !client.state.show_inventory_menu
                                            && !client.state.show_loadout_menu
                                            && !client.state.show_rotation_editor
                                            && client.state.connected
                                            && client.state.is_alive()
                                            && auto_nav.is_none()
                                        {
                                            let (forward, strafe) =
                                                movement_state.current().components();
                                            let vertical = movement_state.vertical_axis();
                                            let sprint = movement_state.sprinting();
                                            let moving = forward.abs() > f32::EPSILON
                                                || strafe.abs() > f32::EPSILON
                                                || vertical.abs() > f32::EPSILON;
                                            if moving {
                                                if let Err(err) = client
                                                    .move_by(forward, strafe, vertical, sprint)
                                                    .await
                                                {
                                                    client
                                                        .state
                                                        .push_log(format!("Move failed: {err}"));
                                                }
                                                was_moving = true;
                                            } else if was_moving {
                                                if let Err(err) = client.stop().await {
                                                    client
                                                        .state
                                                        .push_log(format!("Stop failed: {err}"));
                                                }
                                                was_moving = false;
                                            }
                                        } else if auto_nav.is_some()
                                            && movement_state.current()
                                                != flatland_tui::MovementInput::Stop
                                        {
                                            auto_nav = None;
                                            client.state.push_log("Auto-walk cancelled.");
                                        }
                                    }
                                }
                            }
                            _ => {}
                            }
                        }
                    }

                    if !client.state.connected {
                        paint_world(
                            &mut ratatui,
                            &client,
                            &movement_state,
                            &map_target,
                            &auto_nav,
                            mouse_hover,
                            &chat_line,
                            show_help,
                            hud_view_mode,
                        )?;
                        break 'session PlayExit::Disconnected;
                    }

                    paint_world(
                        &mut ratatui,
                        &client,
                        &movement_state,
                        &map_target,
                        &auto_nav,
                        mouse_hover,
                        &chat_line,
                        show_help,
                        hud_view_mode,
                    )?;
                }
            };

            match session_exit {
                PlayExit::Disconnected if auto_reconnect => {
                    if let Some(reason) = &client.state.disconnect_reason {
                        info!(%reason, "server disconnected — reconnecting");
                    } else {
                        info!("server disconnected — reconnecting");
                    }
                    client.disconnect();
                    was_moving = false;
                    chat_mode = false;
                    chat_buffer.clear();
                    movement_state.reset();
                    client = wait_for_server(&opts, &mut ratatui).await?;
                    client.close_overlays();
                    paint_world(
                        &mut ratatui,
                        &client,
                        &movement_state,
                        &map_target,
                        &auto_nav,
                        mouse_hover,
                        "",
                        show_help,
                        hud_view_mode,
                    )?;
                    info!(entity_id = client.entity_id(), "reconnected");
                    continue 'outer;
                }
                PlayExit::Disconnected => {
                    disconnect_reason = client.state.disconnect_reason.clone();
                    client.disconnect();
                    break 'outer session_exit;
                }
                PlayExit::UserQuit => {
                    client.disconnect();
                    break 'outer session_exit;
                }
                PlayExit::Interrupt => break 'outer session_exit,
            }
        };

        let _ = ratatui.show_cursor();
        // Explicit restore before Drop — ensures kitty keyboard flags clear even if
        // ratatui/backend teardown races with Guard.
        let _ = crate::terminal::restore_terminal();
        (exit, disconnect_reason)
    };

    let _ = crate::terminal::restore_terminal();

    match exit {
        PlayExit::UserQuit => info!("quit"),
        PlayExit::Disconnected => {
            if let Some(reason) = &disconnect_reason {
                eprintln!("Disconnected: {reason}");
            } else {
                eprintln!("Disconnected from server");
            }
        }
        PlayExit::Interrupt => eprintln!("Interrupted"),
    }

    Ok(())
}

pub fn auto_reconnect_enabled(no_reconnect: bool) -> bool {
    if no_reconnect {
        return false;
    }
    match std::env::var("FLATLAND_AUTO_RECONNECT").as_deref() {
        Ok("0" | "false" | "FALSE" | "no" | "NO") => false,
        _ => true,
    }
}

async fn connect_client(opts: &ConnectOptions) -> anyhow::Result<GameClient<RemoteSession>> {
    let character_id = opts.character_id;
    let session = connect(opts.clone()).await?;
    let mut client = GameClient::new(session);
    client.state.character_id = character_id;
    client.wait_until_ready().await?;
    Ok(client)
}

fn play_hud<'a>(
    movement_state: &'a MovementState,
    client: &GameClient<RemoteSession>,
    map_target: &MapTargetState,
    auto_nav: &Option<AutoNavigator>,
    mouse_hover: Option<(f32, f32)>,
    view_mode: HudViewMode,
) -> PlayHud<'a> {
    let movement_label = if client.state.connected {
        movement_state.current().label()
    } else {
        "offline"
    };
    PlayHud {
        movement: movement_label,
        sprint_mode: movement_state.sprint_mode(),
        map_target: map_target
            .active
            .then_some((map_target.cursor_x, map_target.cursor_y)),
        auto_nav_goal: auto_nav.as_ref().map(|n| (n.goal_x, n.goal_y)),
        mouse_hover,
        view_mode,
    }
}

fn paint_world(
    ratatui: &mut Terminal<CrosstermBackend<io::Stdout>>,
    client: &GameClient<RemoteSession>,
    movement_state: &MovementState,
    map_target: &MapTargetState,
    auto_nav: &Option<AutoNavigator>,
    mouse_hover: Option<(f32, f32)>,
    chat_line: &str,
    show_help: bool,
    view_mode: HudViewMode,
) -> anyhow::Result<()> {
    let hud = play_hud(
        movement_state,
        client,
        map_target,
        auto_nav,
        mouse_hover,
        view_mode,
    );
    ratatui.draw(|frame| draw(frame, &client.state, &hud, chat_line, show_help))?;
    Ok(())
}

async fn wait_for_server(
    opts: &ConnectOptions,
    ratatui: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> anyhow::Result<GameClient<RemoteSession>> {
    let mut backoff = RECONNECT_INITIAL;
    let mut attempt = 0u32;
    let mut status = String::from("Waiting for game server…");

    loop {
        attempt += 1;
        draw_reconnect_screen(ratatui, &status)?;

        match connect_client(opts).await {
            Ok(client) => return Ok(client),
            Err(err) => {
                status = format!(
                    "Reconnecting to {} as {} (attempt {attempt})\n{err}\nretry in {:.1}s — Ctrl+q to quit",
                    opts.server,
                    opts.name,
                    backoff.as_secs_f32()
                );
            }
        }

        let sleep = tokio::time::sleep(backoff);
        tokio::pin!(sleep);
        loop {
            tokio::select! {
                _ = &mut sleep => break,
                _ = frame_tick() => {
                    draw_reconnect_screen(ratatui, &status)?;
                }
                maybe_key = read_key_event() => {
                    if let Some(Event::Key(key)) = maybe_key {
                        if key.kind == KeyEventKind::Press
                            && key.modifiers.contains(KeyModifiers::CONTROL)
                            && matches!(key.code, KeyCode::Char('q') | KeyCode::Char('c'))
                        {
                            anyhow::bail!("quit while waiting for server");
                        }
                    }
                }
                _ = tokio::signal::ctrl_c() => {
                    anyhow::bail!("interrupted while waiting for server");
                }
            }
        }

        backoff = (backoff * 2).min(RECONNECT_MAX);
    }
}

async fn frame_tick() {
    tokio::time::sleep(Duration::from_millis(33)).await;
}

async fn read_key_event() -> Option<Event> {
    if event::poll(Duration::from_millis(50)).unwrap_or(false) {
        event::read().ok()
    } else {
        None
    }
}

async fn handle_rotation_editor_action(
    client: &mut GameClient<RemoteSession>,
    action: InputAction,
) {
    match action {
        InputAction::RotationEditorListUp => {
            if client.state.rotation_editor.list_index > 0 {
                client.state.rotation_editor.list_index -= 1;
            }
        }
        InputAction::RotationEditorListDown => {
            let max = client.state.rotation_presets.len();
            if max > 0 {
                client.state.rotation_editor.list_index =
                    (client.state.rotation_editor.list_index + 1).min(max - 1);
            }
        }
        InputAction::RotationEditorEdit => {
            let idx = client.state.rotation_editor.list_index;
            if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
                client.state.rotation_editor.draft = Some(preset);
                client.state.rotation_editor.ability_index = 0;
                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
            }
        }
        InputAction::RotationEditorNew => {
            let preset = next_custom_preset(&client.state.rotation_presets);
            client.state.rotation_editor.list_index = client.state.rotation_presets.len();
            client.state.rotation_editor.draft = Some(preset);
            client.state.rotation_editor.ability_index = 0;
            client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
        }
        InputAction::RotationEditorDelete => {
            let idx = client.state.rotation_editor.list_index;
            let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
            if let Some(id) = preset_id {
                if preset_deletable(&id) {
                    if let Err(err) = client.delete_rotation_preset(&id).await {
                        client.state.push_log(format!("Delete: {err}"));
                    } else {
                        let max = client.state.rotation_presets.len();
                        client.state.rotation_editor.list_index = if max == 0 {
                            0
                        } else {
                            client.state.rotation_editor.list_index.min(max - 1)
                        };
                    }
                } else {
                    client.state.push_log("Cannot delete built-in preset");
                }
            }
        }
        InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
            RotationEditorMode::EditLabel => {
                client.state.rotation_editor.label_buffer.clear();
                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
            }
            RotationEditorMode::PickAbility => {
                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
            }
            RotationEditorMode::EditSequence => {
                client.state.rotation_editor.draft = None;
                client.state.rotation_editor.mode = RotationEditorMode::List;
            }
            RotationEditorMode::List => {}
        },
        InputAction::RotationEditorAddAbility => {
            client.state.rotation_editor.picker_index = 0;
            client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
        }
        InputAction::RotationEditorRemoveAbility => {
            let idx = client.state.rotation_editor.ability_index;
            if let Some(draft) = &mut client.state.rotation_editor.draft {
                if idx < draft.abilities.len() {
                    draft.abilities.remove(idx);
                    if client.state.rotation_editor.ability_index >= draft.abilities.len()
                        && client.state.rotation_editor.ability_index > 0
                    {
                        client.state.rotation_editor.ability_index -= 1;
                    }
                }
            }
        }
        InputAction::RotationEditorMoveAbilityUp => {
            let i = client.state.rotation_editor.ability_index;
            if let Some(draft) = &mut client.state.rotation_editor.draft {
                if i > 0 && i < draft.abilities.len() {
                    draft.abilities.swap(i, i - 1);
                    client.state.rotation_editor.ability_index -= 1;
                }
            }
        }
        InputAction::RotationEditorMoveAbilityDown => {
            let i = client.state.rotation_editor.ability_index;
            if let Some(draft) = &mut client.state.rotation_editor.draft {
                if i + 1 < draft.abilities.len() {
                    draft.abilities.swap(i, i + 1);
                    client.state.rotation_editor.ability_index += 1;
                }
            }
        }
        InputAction::RotationEditorAbilityUp => {
            if client.state.rotation_editor.ability_index > 0 {
                client.state.rotation_editor.ability_index -= 1;
            }
        }
        InputAction::RotationEditorAbilityDown => {
            if let Some(draft) = &client.state.rotation_editor.draft {
                if !draft.abilities.is_empty() {
                    client.state.rotation_editor.ability_index = (client.state.rotation_editor.ability_index
                        + 1)
                        .min(draft.abilities.len() - 1);
                }
            }
        }
        InputAction::RotationEditorPickerUp => {
            if client.state.rotation_editor.picker_index > 0 {
                client.state.rotation_editor.picker_index -= 1;
            }
        }
        InputAction::RotationEditorPickerDown => {
            if !EDITOR_ABILITIES.is_empty() {
                client.state.rotation_editor.picker_index = (client.state.rotation_editor.picker_index
                    + 1)
                    .min(EDITOR_ABILITIES.len() - 1);
            }
        }
        InputAction::RotationEditorPickAbility => {
            let pick = client.state.rotation_editor.picker_index;
            if let Some(ability) = EDITOR_ABILITIES.get(pick) {
                if let Some(draft) = &mut client.state.rotation_editor.draft {
                    draft.abilities.push((*ability).to_string());
                    client.state.rotation_editor.ability_index =
                        draft.abilities.len().saturating_sub(1);
                }
                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
            }
        }
        InputAction::RotationEditorRename => {
            if let Some(draft) = &client.state.rotation_editor.draft {
                client.state.rotation_editor.label_buffer = draft.label.clone();
                client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
            }
        }
        InputAction::RotationEditorConfirmLabel => {
            let label = client.state.rotation_editor.label_buffer.trim().to_string();
            if label.is_empty() {
                client.state.push_log("Label cannot be empty");
            } else if let Some(draft) = &mut client.state.rotation_editor.draft {
                draft.label = label;
                client.state.rotation_editor.label_buffer.clear();
                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
            }
        }
        InputAction::RotationEditorLabelBackspace => {
            client.state.rotation_editor.label_buffer.pop();
        }
        InputAction::RotationEditorLabelChar(c) => {
            if client.state.rotation_editor.label_buffer.len() < 32 {
                client.state.rotation_editor.label_buffer.push(c);
            }
        }
        InputAction::RotationEditorSave => {
            let draft = match client.state.rotation_editor.draft.clone() {
                Some(d) => d,
                None => return,
            };
            if draft.abilities.is_empty() {
                client.state.push_log("Rotation needs at least one ability");
                return;
            }
            let saved_id = draft.id.clone();
            if let Err(err) = client.upsert_rotation_preset(draft).await {
                client.state.push_log(format!("Save: {err}"));
            } else {
                client.state.rotation_editor.mode = RotationEditorMode::List;
                client.state.rotation_editor.draft = None;
                if let Some(i) = client
                    .state
                    .rotation_presets
                    .iter()
                    .position(|p| p.id == saved_id)
                {
                    client.state.rotation_editor.list_index = i;
                }
            }
        }
        _ => {}
    }
}

fn draw_reconnect_screen(
    ratatui: &mut Terminal<CrosstermBackend<io::Stdout>>,
    status: &str,
) -> anyhow::Result<()> {
    ratatui.draw(|frame| {
        let area = frame.area();
        frame.render_widget(Clear, area);
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(3), Constraint::Length(3)])
            .split(area);

        let body = Paragraph::new(status.lines().map(Line::from).collect::<Vec<_>>())
            .alignment(Alignment::Center)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title(" Flatland3 — reconnecting "),
            );
        frame.render_widget(body, chunks[0]);

        let hint = Paragraph::new(Line::from(vec![
            Span::styled("Ctrl+Q", Style::default().add_modifier(Modifier::BOLD)),
            Span::raw(" quit  "),
            Span::styled(
                "Ctrl+C",
                Style::default().add_modifier(Modifier::BOLD),
            ),
            Span::raw(" interrupt"),
        ]))
        .alignment(Alignment::Center);
        frame.render_widget(hint, chunks[1]);
    })?;
    Ok(())
}
