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

//! Merge request detail screen view.
//!
//! Composes metadata header, tab bar (Overview / Files / Discussions),
//! and tab content. Supports progressive hydration: metadata + file
//! changes render immediately while discussions load async.

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::safety::{UrlPolicy, sanitize_for_terminal};
use crate::state::mr_detail::{FileChangeType, MrDetailState, MrMetadata, MrTab};
use crate::view::common::cross_ref::{CrossRefColors, render_cross_refs};
use crate::view::common::discussion_tree::{DiscussionTreeColors, render_discussion_tree};

// ---------------------------------------------------------------------------
// Colors (Flexoki palette)
// ---------------------------------------------------------------------------

const TEXT: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3);
const TEXT_MUTED: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80);
const ACCENT: PackedRgba = PackedRgba::rgb(0xDA, 0x70, 0x2C);
const GREEN: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39);
const RED: PackedRgba = PackedRgba::rgb(0xAF, 0x3A, 0x29);
const CYAN: PackedRgba = PackedRgba::rgb(0x3A, 0xA9, 0x9F);
const YELLOW: PackedRgba = PackedRgba::rgb(0xD0, 0xA2, 0x15);
const BORDER: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80);
const SELECTED_FG: PackedRgba = PackedRgba::rgb(0x10, 0x0F, 0x0F);
const SELECTED_BG: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3);

// ---------------------------------------------------------------------------
// Color constructors
// ---------------------------------------------------------------------------

fn discussion_colors() -> DiscussionTreeColors {
    DiscussionTreeColors {
        author_fg: CYAN,
        timestamp_fg: TEXT_MUTED,
        body_fg: TEXT,
        system_fg: TEXT_MUTED,
        diff_path_fg: GREEN,
        resolved_fg: TEXT_MUTED,
        guide_fg: BORDER,
        selected_fg: SELECTED_FG,
        selected_bg: SELECTED_BG,
        expand_fg: ACCENT,
    }
}

fn cross_ref_colors() -> CrossRefColors {
    CrossRefColors {
        kind_fg: ACCENT,
        label_fg: TEXT,
        muted_fg: TEXT_MUTED,
        selected_fg: SELECTED_FG,
        selected_bg: SELECTED_BG,
    }
}

// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------

/// Render the full MR detail screen.
///
/// Layout:
/// ```text
/// Row 0:     !10 Fix auth flow                    (title bar)
/// Row 1:     opened | alice | fix-auth -> main    (metadata row)
/// Row 2:     [Overview] [Files (3)] [Discussions] (tab bar)
/// Row 3:     ────────────────────────────────────  (separator)
/// Row 4..N:  Tab-specific content
/// ```
pub fn render_mr_detail(
    frame: &mut Frame<'_>,
    state: &MrDetailState,
    area: Rect,
    clock: &dyn Clock,
) {
    if area.height < 4 || area.width < 10 {
        return;
    }

    let Some(ref meta) = state.metadata else {
        return;
    };

    let max_x = area.x.saturating_add(area.width);
    let mut y = area.y;

    // --- Title bar ---
    y = render_title_bar(frame, meta, area.x, y, max_x);

    // --- Metadata row ---
    y = render_metadata_row(frame, meta, area.x, y, max_x);

    // --- Tab bar ---
    y = render_tab_bar(frame, state, area.x, y, max_x);

    // --- Separator ---
    y = render_separator(frame, area.x, y, area.width);

    let bottom = area.y.saturating_add(area.height);
    if y >= bottom {
        return;
    }

    let content_area = Rect::new(area.x, y, area.width, bottom.saturating_sub(y));

    match state.active_tab {
        MrTab::Overview => render_overview_tab(frame, state, meta, content_area, clock),
        MrTab::Files => render_files_tab(frame, state, content_area),
        MrTab::Discussions => render_discussions_tab(frame, state, content_area, clock),
    }
}

// ---------------------------------------------------------------------------
// Sub-renderers
// ---------------------------------------------------------------------------

/// Render `!10 Fix auth flow` (or `!10 [Draft] Fix auth flow`).
fn render_title_bar(frame: &mut Frame<'_>, meta: &MrMetadata, x: u16, y: u16, max_x: u16) -> u16 {
    let iid_text = format!("!{} ", meta.iid);
    let iid_style = Cell {
        fg: ACCENT,
        ..Cell::default()
    };
    let mut cx = frame.print_text_clipped(x, y, &iid_text, iid_style, max_x);

    if meta.draft {
        let draft_style = Cell {
            fg: YELLOW,
            ..Cell::default()
        };
        cx = frame.print_text_clipped(cx, y, "[Draft] ", draft_style, max_x);
    }

    let title_style = Cell {
        fg: TEXT,
        ..Cell::default()
    };
    let safe_title = sanitize_for_terminal(&meta.title, UrlPolicy::Strip);
    let _ = frame.print_text_clipped(cx, y, &safe_title, title_style, max_x);

    y + 1
}

/// Render `opened | alice | fix-auth -> main | mergeable`.
fn render_metadata_row(
    frame: &mut Frame<'_>,
    meta: &MrMetadata,
    x: u16,
    y: u16,
    max_x: u16,
) -> u16 {
    let state_fg = match meta.state.as_str() {
        "opened" => GREEN,
        "merged" => CYAN,
        "closed" => RED,
        _ => TEXT_MUTED,
    };
    let state_style = Cell {
        fg: state_fg,
        ..Cell::default()
    };
    let muted = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    let author_style = Cell {
        fg: CYAN,
        ..Cell::default()
    };

    let mut cx = frame.print_text_clipped(x, y, &meta.state, state_style, max_x);
    cx = frame.print_text_clipped(cx, y, " | ", muted, max_x);
    cx = frame.print_text_clipped(cx, y, &meta.author, author_style, max_x);
    cx = frame.print_text_clipped(cx, y, " | ", muted, max_x);

    let branch_text = format!("{} -> {}", meta.source_branch, meta.target_branch);
    cx = frame.print_text_clipped(cx, y, &branch_text, muted, max_x);

    if !meta.merge_status.is_empty() {
        cx = frame.print_text_clipped(cx, y, " | ", muted, max_x);
        let status_fg = if meta.merge_status == "mergeable" {
            GREEN
        } else {
            YELLOW
        };
        let status_style = Cell {
            fg: status_fg,
            ..Cell::default()
        };
        let _ = frame.print_text_clipped(cx, y, &meta.merge_status, status_style, max_x);
    }

    y + 1
}

/// Render tab bar: `[Overview] [Files (3)] [Discussions (2)]`.
fn render_tab_bar(frame: &mut Frame<'_>, state: &MrDetailState, x: u16, y: u16, max_x: u16) -> u16 {
    // Use metadata counts before async data loads to avoid showing 0.
    let disc_count = if state.discussions_loaded {
        state.discussions.len()
    } else {
        state.metadata.as_ref().map_or(0, |m| m.discussion_count)
    };

    let tabs = [
        (MrTab::Overview, "Overview".to_string()),
        (
            MrTab::Files,
            format!("Files ({})", state.file_changes.len()),
        ),
        (MrTab::Discussions, format!("Discussions ({disc_count})")),
    ];

    let mut cx = x;
    for (tab, label) in &tabs {
        if *tab == state.active_tab {
            let style = Cell {
                fg: SELECTED_FG,
                bg: SELECTED_BG,
                ..Cell::default()
            };
            let text = format!(" {label} ");
            cx = frame.print_text_clipped(cx, y, &text, style, max_x);
        } else {
            let style = Cell {
                fg: TEXT_MUTED,
                ..Cell::default()
            };
            let text = format!(" {label} ");
            cx = frame.print_text_clipped(cx, y, &text, style, max_x);
        }
        // Tab separator.
        let sep = Cell {
            fg: BORDER,
            ..Cell::default()
        };
        cx = frame.print_text_clipped(cx, y, " ", sep, max_x);
    }

    y + 1
}

/// Render horizontal separator.
fn render_separator(frame: &mut Frame<'_>, x: u16, y: u16, width: u16) -> u16 {
    let sep_style = Cell {
        fg: BORDER,
        ..Cell::default()
    };
    let line: String = "\u{2500}".repeat(width as usize);
    let _ = frame.print_text_clipped(x, y, &line, sep_style, x.saturating_add(width));
    y + 1
}

// ---------------------------------------------------------------------------
// Tab content renderers
// ---------------------------------------------------------------------------

/// Overview tab: description + cross-references.
fn render_overview_tab(
    frame: &mut Frame<'_>,
    state: &MrDetailState,
    meta: &MrMetadata,
    area: Rect,
    _clock: &dyn Clock,
) {
    let max_x = area.x.saturating_add(area.width);
    let mut y = area.y;
    let bottom = area.y.saturating_add(area.height);

    // --- Description ---
    let safe_desc = sanitize_for_terminal(&meta.description, UrlPolicy::Strip);
    let lines: Vec<&str> = safe_desc.lines().collect();
    let text_style = Cell {
        fg: TEXT,
        ..Cell::default()
    };

    for line in lines
        .iter()
        .skip(state.description_scroll)
        .take((bottom.saturating_sub(y)) as usize)
    {
        let _ = frame.print_text_clipped(area.x, y, line, text_style, max_x);
        y += 1;
    }

    if y >= bottom {
        return;
    }

    // --- Separator ---
    y = render_separator(frame, area.x, y, area.width);
    if y >= bottom {
        return;
    }

    // --- Cross-references ---
    if !state.cross_refs.is_empty() {
        let header_style = Cell {
            fg: ACCENT,
            ..Cell::default()
        };
        let header = format!("Cross References ({})", state.cross_refs.len());
        let _ = frame.print_text_clipped(area.x, y, &header, header_style, max_x);
        y += 1;

        if y < bottom {
            let refs_area = Rect::new(area.x, y, area.width, bottom.saturating_sub(y));
            let _ = render_cross_refs(
                frame,
                &state.cross_refs,
                &state.cross_ref_state,
                refs_area,
                &cross_ref_colors(),
            );
        }
    }
}

/// Files tab: list of changed files with change type indicators.
fn render_files_tab(frame: &mut Frame<'_>, state: &MrDetailState, area: Rect) {
    let max_x = area.x.saturating_add(area.width);
    let mut y = area.y;
    let bottom = area.y.saturating_add(area.height);

    if state.file_changes.is_empty() {
        let style = Cell {
            fg: TEXT_MUTED,
            ..Cell::default()
        };
        let _ = frame.print_text_clipped(area.x + 1, y, "No file changes", style, max_x);
        return;
    }

    for (i, fc) in state
        .file_changes
        .iter()
        .skip(state.file_scroll)
        .take((bottom.saturating_sub(y)) as usize)
        .enumerate()
    {
        let is_selected = i + state.file_scroll == state.file_selected;

        let (fg, bg) = if is_selected {
            (SELECTED_FG, SELECTED_BG)
        } else {
            (TEXT, PackedRgba::TRANSPARENT)
        };

        if is_selected {
            let sel_cell = Cell {
                fg,
                bg,
                ..Cell::default()
            };
            frame.draw_rect_filled(Rect::new(area.x, y, area.width, 1), sel_cell);
        }

        // Change type icon.
        let icon_fg = match fc.change_type {
            FileChangeType::Added => GREEN,
            FileChangeType::Deleted => RED,
            FileChangeType::Modified => YELLOW,
            FileChangeType::Renamed => CYAN,
        };
        let icon_style = Cell {
            fg: if is_selected { fg } else { icon_fg },
            bg,
            ..Cell::default()
        };
        let mut cx = frame.print_text_clipped(area.x, y, fc.change_type.icon(), icon_style, max_x);
        cx = frame.print_text_clipped(cx, y, " ", icon_style, max_x);

        // File path.
        let path_style = Cell {
            fg,
            bg,
            ..Cell::default()
        };
        let display_path = if fc.change_type == FileChangeType::Renamed {
            if let Some(ref old) = fc.old_path {
                format!("{old} -> {}", fc.new_path)
            } else {
                fc.new_path.clone()
            }
        } else {
            fc.new_path.clone()
        };
        let _ = frame.print_text_clipped(cx, y, &display_path, path_style, max_x);

        y += 1;
    }
}

/// Discussions tab: all discussions rendered via the tree widget.
fn render_discussions_tab(
    frame: &mut Frame<'_>,
    state: &MrDetailState,
    area: Rect,
    clock: &dyn Clock,
) {
    let max_x = area.x.saturating_add(area.width);

    if !state.discussions_loaded {
        let style = Cell {
            fg: TEXT_MUTED,
            ..Cell::default()
        };
        let _ =
            frame.print_text_clipped(area.x + 1, area.y, "Loading discussions...", style, max_x);
        return;
    }

    if state.discussions.is_empty() {
        let style = Cell {
            fg: TEXT_MUTED,
            ..Cell::default()
        };
        let _ = frame.print_text_clipped(area.x + 1, area.y, "No discussions", style, max_x);
        return;
    }

    let _ = render_discussion_tree(
        frame,
        &state.discussions,
        &state.tree_state,
        area,
        &discussion_colors(),
        clock,
    );
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::clock::FakeClock;
    use crate::message::EntityKey;
    use crate::state::mr_detail::{FileChange, FileChangeType, MrDetailData, MrMetadata, MrTab};
    use crate::view::common::cross_ref::{CrossRef, CrossRefKind};
    use crate::view::common::discussion_tree::{DiscussionNode, NoteNode};
    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_mr_metadata() -> MrMetadata {
        MrMetadata {
            iid: 10,
            project_path: "group/project".into(),
            title: "Fix authentication flow".into(),
            description: "This MR fixes the login bug.\nSee issue #42.".into(),
            state: "opened".into(),
            draft: false,
            author: "alice".into(),
            assignees: vec!["bob".into()],
            reviewers: vec!["carol".into()],
            labels: vec!["backend".into()],
            source_branch: "fix-auth".into(),
            target_branch: "main".into(),
            merge_status: "mergeable".into(),
            created_at: 1_700_000_000_000,
            updated_at: 1_700_000_060_000,
            merged_at: None,
            web_url: "https://gitlab.com/group/project/-/merge_requests/10".into(),
            discussion_count: 1,
            file_change_count: 2,
        }
    }

    fn sample_mr_state() -> MrDetailState {
        let mut state = MrDetailState::default();
        state.load_new(EntityKey::mr(1, 10));
        state.apply_metadata(MrDetailData {
            metadata: sample_mr_metadata(),
            cross_refs: vec![CrossRef {
                kind: CrossRefKind::ClosingMr,
                entity_key: EntityKey::issue(1, 42),
                label: "Auth bug".into(),
                navigable: true,
            }],
            file_changes: vec![
                FileChange {
                    old_path: None,
                    new_path: "src/auth.rs".into(),
                    change_type: FileChangeType::Modified,
                },
                FileChange {
                    old_path: None,
                    new_path: "src/lib.rs".into(),
                    change_type: FileChangeType::Added,
                },
            ],
        });
        state
    }

    #[test]
    fn test_render_mr_detail_no_metadata() {
        with_frame!(80, 24, |frame| {
            let state = MrDetailState::default();
            let clock = FakeClock::from_ms(1_700_000_000_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_overview_tab() {
        with_frame!(80, 24, |frame| {
            let state = sample_mr_state();
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_files_tab() {
        with_frame!(80, 24, |frame| {
            let mut state = sample_mr_state();
            state.active_tab = MrTab::Files;
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_discussions_tab_loading() {
        with_frame!(80, 24, |frame| {
            let mut state = sample_mr_state();
            state.active_tab = MrTab::Discussions;
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_discussions_tab_with_data() {
        with_frame!(80, 30, |frame| {
            let mut state = sample_mr_state();
            state.active_tab = MrTab::Discussions;
            state.apply_discussions(vec![DiscussionNode {
                discussion_id: "d1".into(),
                notes: vec![NoteNode {
                    author: "alice".into(),
                    body: "Looks good".into(),
                    created_at: 1_700_000_020_000,
                    is_system: false,
                    is_diff_note: true,
                    diff_file_path: Some("src/auth.rs".into()),
                    diff_new_line: Some(42),
                }],
                resolvable: true,
                resolved: false,
            }]);
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 30), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_draft() {
        with_frame!(80, 24, |frame| {
            let mut state = sample_mr_state();
            state.metadata.as_mut().unwrap().draft = true;
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_tiny_area() {
        with_frame!(5, 3, |frame| {
            let state = sample_mr_state();
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 5, 3), &clock);
        });
    }

    #[test]
    fn test_render_mr_detail_narrow_terminal() {
        with_frame!(30, 10, |frame| {
            let state = sample_mr_state();
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 30, 10), &clock);
        });
    }

    #[test]
    fn test_render_files_empty() {
        with_frame!(80, 24, |frame| {
            let mut state = MrDetailState::default();
            state.load_new(EntityKey::mr(1, 10));
            state.apply_metadata(MrDetailData {
                metadata: sample_mr_metadata(),
                cross_refs: vec![],
                file_changes: vec![],
            });
            state.active_tab = MrTab::Files;
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

    #[test]
    fn test_render_files_with_rename() {
        with_frame!(80, 24, |frame| {
            let mut state = MrDetailState::default();
            state.load_new(EntityKey::mr(1, 10));
            state.apply_metadata(MrDetailData {
                metadata: sample_mr_metadata(),
                cross_refs: vec![],
                file_changes: vec![FileChange {
                    old_path: Some("src/old.rs".into()),
                    new_path: "src/new.rs".into(),
                    change_type: FileChangeType::Renamed,
                }],
            });
            state.active_tab = MrTab::Files;
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_mr_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }
}
