#![allow(dead_code)] // Phase 3: consumed by view/mod.rs screen dispatch

//! Timeline screen view — chronological event stream with color-coded types.
//!
//! Layout:
//! ```text
//! +─── Timeline ──────────────────────────────+
//! | 3h ago  #42  Created: Fix login bug       |
//! | 2h ago  #42  State changed to closed      |
//! | 1h ago  !99  Label added: backend         |
//! | 30m ago !99  Merged                        |
//! +───────────────────────────────────────────+
//! | j/k: nav  Enter: open  q: back           |
//! +───────────────────────────────────────────+
//! ```

use ftui::core::geometry::Rect;
use ftui::render::cell::{Cell, PackedRgba};
use ftui::render::drawing::Draw;
use ftui::render::frame::Frame;

use crate::clock::Clock;
use crate::message::TimelineEventKind;
use crate::state::timeline::TimelineState;
use crate::view::common::discussion_tree::format_relative_time;

use super::{ACCENT, BG_SURFACE, BORDER, TEXT, TEXT_MUTED};

// ---------------------------------------------------------------------------
// Colors for event kinds (Flexoki palette)
// ---------------------------------------------------------------------------

const GREEN: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39); // Created
const YELLOW: PackedRgba = PackedRgba::rgb(0xD0, 0xA2, 0x15); // StateChanged
const RED: PackedRgba = PackedRgba::rgb(0xAF, 0x3A, 0x29); // Closed (via StateChanged)
const PURPLE: PackedRgba = PackedRgba::rgb(0x8B, 0x7E, 0xC8); // Merged
const CYAN: PackedRgba = PackedRgba::rgb(0x3A, 0xA9, 0x9F); // Label
const SELECTED_FG: PackedRgba = PackedRgba::rgb(0x10, 0x0F, 0x0F); // bg (dark)

/// Map event kind to its display color.
fn event_color(kind: TimelineEventKind, detail: Option<&str>) -> PackedRgba {
    match kind {
        TimelineEventKind::Created => GREEN,
        TimelineEventKind::StateChanged => {
            if detail == Some("closed") {
                RED
            } else {
                YELLOW
            }
        }
        TimelineEventKind::LabelAdded | TimelineEventKind::LabelRemoved => CYAN,
        TimelineEventKind::MilestoneSet | TimelineEventKind::MilestoneRemoved => ACCENT,
        TimelineEventKind::Merged => PURPLE,
    }
}

// ---------------------------------------------------------------------------
// render_timeline
// ---------------------------------------------------------------------------

/// Render the timeline screen.
///
/// Composes: scope header (row 0), separator (row 1),
/// event list (fill), and a hint bar at the bottom.
pub fn render_timeline(
    frame: &mut Frame<'_>,
    state: &TimelineState,
    area: Rect,
    clock: &dyn Clock,
) {
    if area.height < 4 || area.width < 20 {
        return;
    }

    let mut y = area.y;
    let max_x = area.right();

    // -- Scope header --
    let scope_label = match &state.scope {
        crate::state::timeline::TimelineScope::All => "All events".to_string(),
        crate::state::timeline::TimelineScope::Entity(key) => {
            let sigil = match key.kind {
                crate::message::EntityKind::Issue => "#",
                crate::message::EntityKind::MergeRequest => "!",
            };
            format!("Entity {sigil}{}", key.iid)
        }
        crate::state::timeline::TimelineScope::Author(name) => format!("Author: {name}"),
    };

    let header = format!("Timeline: {scope_label}");
    let header_cell = Cell {
        fg: ACCENT,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    frame.print_text_clipped(area.x, y, &header, header_cell, max_x);
    y += 1;

    // -- Separator --
    if y >= area.bottom() {
        return;
    }
    let sep_cell = Cell {
        fg: BORDER,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let sep_line = "─".repeat(area.width as usize);
    frame.print_text_clipped(area.x, y, &sep_line, sep_cell, max_x);
    y += 1;

    // -- Event list --
    let bottom_hint_row = area.bottom().saturating_sub(1);
    let list_height = bottom_hint_row.saturating_sub(y) as usize;

    if list_height == 0 {
        return;
    }

    if state.events.is_empty() {
        render_empty_state(frame, state, area.x + 1, y, max_x);
    } else {
        render_event_list(frame, state, area.x, y, area.width, list_height, clock);
    }

    // -- Hint bar --
    if bottom_hint_row < area.bottom() {
        render_hint_bar(frame, area.x, bottom_hint_row, max_x);
    }
}

// ---------------------------------------------------------------------------
// Empty state
// ---------------------------------------------------------------------------

fn render_empty_state(frame: &mut Frame<'_>, state: &TimelineState, x: u16, y: u16, max_x: u16) {
    let msg = if state.loading {
        "Loading timeline..."
    } else {
        "No timeline events found"
    };
    let cell = Cell {
        fg: TEXT_MUTED,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    frame.print_text_clipped(x, y, msg, cell, max_x);
}

// ---------------------------------------------------------------------------
// Event list
// ---------------------------------------------------------------------------

/// Render the scrollable list of timeline events.
fn render_event_list(
    frame: &mut Frame<'_>,
    state: &TimelineState,
    x: u16,
    start_y: u16,
    width: u16,
    list_height: usize,
    clock: &dyn Clock,
) {
    let max_x = x + width;

    // Scroll so selected item is always visible.
    let scroll_offset = if state.selected_index >= list_height {
        state.selected_index - list_height + 1
    } else {
        0
    };

    let selected_cell = Cell {
        fg: SELECTED_FG,
        bg: ACCENT,
        ..Cell::default()
    };

    for (i, event) in state
        .events
        .iter()
        .skip(scroll_offset)
        .enumerate()
        .take(list_height)
    {
        let y = start_y + i as u16;
        let is_selected = i + scroll_offset == state.selected_index;

        let kind_color = event_color(event.event_kind, event.detail.as_deref());

        // Fill row background for selected item.
        if is_selected {
            for col in x..max_x {
                frame.buffer.set(col, y, selected_cell);
            }
        }

        let mut cx = x + 1;

        // Timestamp gutter (right-aligned in ~10 chars).
        let time_str = format_relative_time(event.timestamp_ms, clock);
        let time_width = 10u16;
        let time_x = cx + time_width.saturating_sub(time_str.len() as u16);
        let time_cell = if is_selected {
            selected_cell
        } else {
            Cell {
                fg: TEXT_MUTED,
                bg: BG_SURFACE,
                ..Cell::default()
            }
        };
        frame.print_text_clipped(time_x, y, &time_str, time_cell, cx + time_width);
        cx += time_width + 1;

        // Entity prefix: #42 or !99
        let prefix = match event.entity_key.kind {
            crate::message::EntityKind::Issue => "#",
            crate::message::EntityKind::MergeRequest => "!",
        };
        let entity_str = format!("{prefix}{}", event.entity_key.iid);
        let entity_cell = if is_selected {
            selected_cell
        } else {
            Cell {
                fg: kind_color,
                bg: BG_SURFACE,
                ..Cell::default()
            }
        };
        let after_entity = frame.print_text_clipped(cx, y, &entity_str, entity_cell, max_x);
        cx = after_entity + 1;

        // Event kind badge.
        let badge = event.event_kind.label();
        let badge_cell = if is_selected {
            selected_cell
        } else {
            Cell {
                fg: kind_color,
                bg: BG_SURFACE,
                ..Cell::default()
            }
        };
        let after_badge = frame.print_text_clipped(cx, y, badge, badge_cell, max_x);
        cx = after_badge + 1;

        // Summary text.
        let summary_cell = if is_selected {
            selected_cell
        } else {
            Cell {
                fg: TEXT,
                bg: BG_SURFACE,
                ..Cell::default()
            }
        };
        frame.print_text_clipped(cx, y, &event.summary, summary_cell, max_x);

        // Actor (right-aligned) if there's room.
        if let Some(ref actor) = event.actor {
            let actor_str = format!(" {actor} ");
            let actor_width = actor_str.len() as u16;
            let actor_x = max_x.saturating_sub(actor_width);
            if actor_x > cx + 5 {
                let actor_cell = if is_selected {
                    selected_cell
                } else {
                    Cell {
                        fg: TEXT_MUTED,
                        bg: BG_SURFACE,
                        ..Cell::default()
                    }
                };
                frame.print_text_clipped(actor_x, y, &actor_str, actor_cell, max_x);
            }
        }
    }

    // Scroll indicator (overlaid on last visible row when events overflow).
    if state.events.len() > list_height && list_height > 0 {
        let indicator = format!(
            " {}/{} ",
            (scroll_offset + list_height).min(state.events.len()),
            state.events.len()
        );
        let ind_x = max_x.saturating_sub(indicator.len() as u16);
        let ind_y = start_y + list_height.saturating_sub(1) as u16;
        let ind_cell = Cell {
            fg: TEXT_MUTED,
            bg: BG_SURFACE,
            ..Cell::default()
        };
        frame.print_text_clipped(ind_x, ind_y, &indicator, ind_cell, max_x);
    }
}

// ---------------------------------------------------------------------------
// Hint bar
// ---------------------------------------------------------------------------

fn render_hint_bar(frame: &mut Frame<'_>, x: u16, y: u16, max_x: u16) {
    let hint_cell = Cell {
        fg: TEXT_MUTED,
        bg: BG_SURFACE,
        ..Cell::default()
    };
    let hints = "j/k: nav  Enter: open  q: back";
    frame.print_text_clipped(x + 1, y, hints, hint_cell, max_x);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::clock::FakeClock;
    use crate::message::{EntityKey, TimelineEvent, TimelineEventKind};
    use crate::state::timeline::TimelineState;
    use ftui::render::grapheme_pool::GraphemePool;

    macro_rules! with_frame {
        ($width:expr, $height:expr, |$frame:ident| $body:block) => {{
            let mut pool = GraphemePool::new();
            let mut $frame = Frame::new($width, $height, &mut pool);
            $body
        }};
    }

    fn sample_event(timestamp_ms: i64, iid: i64, kind: TimelineEventKind) -> TimelineEvent {
        TimelineEvent {
            timestamp_ms,
            entity_key: EntityKey::issue(1, iid),
            event_kind: kind,
            summary: format!("Event for #{iid}"),
            detail: None,
            actor: Some("alice".into()),
            project_path: "group/project".into(),
        }
    }

    fn test_clock() -> FakeClock {
        FakeClock::from_ms(1_700_000_100_000)
    }

    #[test]
    fn test_render_timeline_empty_no_panic() {
        with_frame!(80, 24, |frame| {
            let state = TimelineState::default();
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_timeline_with_events_no_panic() {
        with_frame!(100, 30, |frame| {
            let state = TimelineState {
                events: vec![
                    sample_event(1_700_000_000_000, 1, TimelineEventKind::Created),
                    sample_event(1_700_000_050_000, 2, TimelineEventKind::StateChanged),
                    sample_event(1_700_000_080_000, 3, TimelineEventKind::Merged),
                ],
                ..TimelineState::default()
            };
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 100, 30), &clock);
        });
    }

    #[test]
    fn test_render_timeline_with_selection_no_panic() {
        with_frame!(80, 24, |frame| {
            let state = TimelineState {
                events: vec![
                    sample_event(1_700_000_000_000, 1, TimelineEventKind::Created),
                    sample_event(1_700_000_050_000, 2, TimelineEventKind::LabelAdded),
                ],
                selected_index: 1,
                ..TimelineState::default()
            };
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_timeline_tiny_terminal_noop() {
        with_frame!(15, 3, |frame| {
            let state = TimelineState::default();
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 15, 3), &clock);
        });
    }

    #[test]
    fn test_render_timeline_loading_state() {
        with_frame!(80, 24, |frame| {
            let state = TimelineState {
                loading: true,
                ..TimelineState::default()
            };
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_timeline_scrollable_events() {
        with_frame!(80, 10, |frame| {
            let state = TimelineState {
                events: (0..20)
                    .map(|i| {
                        sample_event(
                            1_700_000_000_000 + i * 10_000,
                            i + 1,
                            TimelineEventKind::Created,
                        )
                    })
                    .collect(),
                selected_index: 15,
                ..TimelineState::default()
            };
            let clock = test_clock();
            render_timeline(&mut frame, &state, Rect::new(0, 0, 80, 10), &clock);
        });
    }

    #[test]
    fn test_event_color_created_is_green() {
        assert_eq!(event_color(TimelineEventKind::Created, None), GREEN);
    }

    #[test]
    fn test_event_color_closed_is_red() {
        assert_eq!(
            event_color(TimelineEventKind::StateChanged, Some("closed")),
            RED
        );
    }

    #[test]
    fn test_event_color_merged_is_purple() {
        assert_eq!(event_color(TimelineEventKind::Merged, None), PURPLE);
    }
}
