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

//! Issue detail screen view.
//!
//! Composes metadata header, description, discussion tree, and
//! cross-references into a scrollable detail layout. Supports
//! progressive hydration: metadata renders immediately while
//! discussions load async in Phase 2.

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::issue_detail::{DetailSection, IssueDetailState, IssueMetadata};
use crate::view::common::cross_ref::{CrossRefColors, render_cross_refs};
use crate::view::common::discussion_tree::{DiscussionTreeColors, render_discussion_tree};

// ---------------------------------------------------------------------------
// Colors (Flexoki palette — will use injected Theme in a later phase)
// ---------------------------------------------------------------------------

const TEXT: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3); // tx
const TEXT_MUTED: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80); // tx-2
const ACCENT: PackedRgba = PackedRgba::rgb(0xDA, 0x70, 0x2C); // orange
const GREEN: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39); // green
const RED: PackedRgba = PackedRgba::rgb(0xAF, 0x3A, 0x29); // red
const CYAN: PackedRgba = PackedRgba::rgb(0x3A, 0xA9, 0x9F); // cyan
const BG_SURFACE: PackedRgba = PackedRgba::rgb(0x28, 0x28, 0x24); // bg-2
const BORDER: PackedRgba = PackedRgba::rgb(0x87, 0x87, 0x80); // tx-2
const SELECTED_FG: PackedRgba = PackedRgba::rgb(0x10, 0x0F, 0x0F); // bg
const SELECTED_BG: PackedRgba = PackedRgba::rgb(0xCE, 0xCD, 0xC3); // tx

// ---------------------------------------------------------------------------
// 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 issue detail screen.
///
/// Layout:
/// ```text
/// Row 0:     #42 Fix authentication flow        (title bar)
/// Row 1:     opened | alice | backend, security  (metadata row)
/// Row 2:     Milestone: v1.0 | Due: 2026-03-01   (optional)
/// Row 3:     ─────────────────────────────────── (separator)
/// Row 4..N:  Description text...                  (scrollable)
///            ─────────────────────────────────── (separator)
///            Discussions (3)                       (section header)
///            ▶ alice: Fixed the login flow...      (collapsed)
///            ▼ bob: I think we should also...      (expanded)
///              bob: body line 1...
///            ─────────────────────────────────── (separator)
///            Cross References                     (section header)
///            [MR] !10 Fix authentication MR
/// ```
pub fn render_issue_detail(
    frame: &mut Frame<'_>,
    state: &IssueDetailState,
    area: Rect,
    clock: &dyn Clock,
) {
    if area.height < 3 || area.width < 10 {
        return;
    }

    let Some(ref meta) = state.metadata else {
        // No metadata yet — the loading spinner handles this.
        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);

    // --- Optional milestone / due date row ---
    if meta.milestone.is_some() || meta.due_date.is_some() {
        y = render_milestone_row(frame, meta, 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;
    }

    // Remaining space is split between description, discussions, and cross-refs.
    let remaining = bottom.saturating_sub(y);

    // Compute section heights based on content.
    let desc_lines = count_description_lines(meta, area.width);
    let disc_count = state.discussions.len();
    let xref_count = state.cross_refs.len();

    let (desc_h, disc_h, xref_h) = allocate_sections(remaining, desc_lines, disc_count, xref_count);

    // --- Description section ---
    if desc_h > 0 {
        let desc_area = Rect::new(area.x, y, area.width, desc_h);
        let is_focused = state.active_section == DetailSection::Description;
        render_description(frame, meta, state.description_scroll, desc_area, is_focused);
        y += desc_h;
    }

    // --- Separator before discussions ---
    if (disc_h > 0 || xref_h > 0) && y < bottom {
        y = render_separator(frame, area.x, y, area.width);
    }

    // --- Discussions section ---
    if disc_h > 0 && y < bottom {
        let header_h = 1;
        let is_focused = state.active_section == DetailSection::Discussions;

        // Section header.
        render_section_header(
            frame,
            &format!("Discussions ({})", state.discussions.len()),
            area.x,
            y,
            max_x,
            is_focused,
        );
        y += header_h;

        if !state.discussions_loaded {
            // Still loading.
            let style = Cell {
                fg: TEXT_MUTED,
                ..Cell::default()
            };
            let _ = frame.print_text_clipped(area.x + 1, y, "Loading discussions...", style, max_x);
            y += 1;
        } else if state.discussions.is_empty() {
            let style = Cell {
                fg: TEXT_MUTED,
                ..Cell::default()
            };
            let _ = frame.print_text_clipped(area.x + 1, y, "No discussions", style, max_x);
            y += 1;
        } else {
            let tree_height = disc_h.saturating_sub(header_h);
            if tree_height > 0 {
                let tree_area = Rect::new(area.x, y, area.width, tree_height);
                let rendered = render_discussion_tree(
                    frame,
                    &state.discussions,
                    &state.tree_state,
                    tree_area,
                    &discussion_colors(),
                    clock,
                );
                y += rendered;
            }
        }
    }

    // --- Separator before cross-refs ---
    if xref_h > 0 && y < bottom {
        y = render_separator(frame, area.x, y, area.width);
    }

    // --- Cross-references section ---
    if xref_h > 0 && y < bottom {
        let is_focused = state.active_section == DetailSection::CrossRefs;

        render_section_header(
            frame,
            &format!("Cross References ({})", state.cross_refs.len()),
            area.x,
            y,
            max_x,
            is_focused,
        );
        y += 1;

        if state.cross_refs.is_empty() {
            let style = Cell {
                fg: TEXT_MUTED,
                ..Cell::default()
            };
            let _ = frame.print_text_clipped(area.x + 1, y, "No cross-references", style, max_x);
        } else {
            let refs_height = xref_h.saturating_sub(1); // minus header
            if refs_height > 0 {
                let refs_area = Rect::new(area.x, y, area.width, refs_height);
                let _ = render_cross_refs(
                    frame,
                    &state.cross_refs,
                    &state.cross_ref_state,
                    refs_area,
                    &cross_ref_colors(),
                );
            }
        }
    }
}

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

/// Render the issue title bar: `#42 Fix authentication flow`
fn render_title_bar(
    frame: &mut Frame<'_>,
    meta: &IssueMetadata,
    x: u16,
    y: u16,
    max_x: u16,
) -> u16 {
    let iid_text = format!("#{} ", meta.iid);
    let iid_style = Cell {
        fg: ACCENT,
        ..Cell::default()
    };
    let title_style = Cell {
        fg: TEXT,
        ..Cell::default()
    };

    let cx = frame.print_text_clipped(x, y, &iid_text, iid_style, max_x);
    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 the metadata row: `opened | alice | backend, security`
fn render_metadata_row(
    frame: &mut Frame<'_>,
    meta: &IssueMetadata,
    x: u16,
    y: u16,
    max_x: u16,
) -> u16 {
    let state_fg = match meta.state.as_str() {
        "opened" => GREEN,
        "closed" => RED,
        _ => TEXT_MUTED,
    };
    let state_style = Cell {
        fg: state_fg,
        ..Cell::default()
    };
    let muted_style = 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_style, max_x);
    cx = frame.print_text_clipped(cx, y, &meta.author, author_style, max_x);

    if !meta.labels.is_empty() {
        cx = frame.print_text_clipped(cx, y, " | ", muted_style, max_x);
        let labels_text = meta.labels.join(", ");
        cx = frame.print_text_clipped(cx, y, &labels_text, muted_style, max_x);
    }

    if !meta.assignees.is_empty() {
        cx = frame.print_text_clipped(cx, y, " | ", muted_style, max_x);
        let assignees_text = format!("-> {}", meta.assignees.join(", "));
        let _ = frame.print_text_clipped(cx, y, &assignees_text, muted_style, max_x);
    }

    y + 1
}

/// Render optional milestone / due date row.
fn render_milestone_row(
    frame: &mut Frame<'_>,
    meta: &IssueMetadata,
    x: u16,
    y: u16,
    max_x: u16,
) -> u16 {
    let muted = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    let mut cx = x;

    if let Some(ref ms) = meta.milestone {
        cx = frame.print_text_clipped(cx, y, "Milestone: ", muted, max_x);
        let val_style = Cell {
            fg: TEXT,
            ..Cell::default()
        };
        cx = frame.print_text_clipped(cx, y, ms, val_style, max_x);
    }

    if let Some(ref due) = meta.due_date {
        if cx > x {
            cx = frame.print_text_clipped(cx, y, " | ", muted, max_x);
        }
        cx = frame.print_text_clipped(cx, y, "Due: ", muted, max_x);
        let val_style = Cell {
            fg: TEXT,
            ..Cell::default()
        };
        let _ = frame.print_text_clipped(cx, y, due, val_style, max_x);
    }

    y + 1
}

/// Render a horizontal separator line.
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
}

/// Render a section header with focus indicator.
fn render_section_header(
    frame: &mut Frame<'_>,
    label: &str,
    x: u16,
    y: u16,
    max_x: u16,
    is_focused: bool,
) {
    if is_focused {
        let style = Cell {
            fg: SELECTED_FG,
            bg: SELECTED_BG,
            ..Cell::default()
        };
        // Fill the row with selected background.
        frame.draw_rect_filled(Rect::new(x, y, max_x.saturating_sub(x), 1), style);
        let _ = frame.print_text_clipped(x, y, label, style, max_x);
    } else {
        let style = Cell {
            fg: ACCENT,
            ..Cell::default()
        };
        let _ = frame.print_text_clipped(x, y, label, style, max_x);
    }
}

/// Render the description section.
fn render_description(
    frame: &mut Frame<'_>,
    meta: &IssueMetadata,
    scroll: usize,
    area: Rect,
    _is_focused: bool,
) {
    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()
    };
    let max_x = area.x.saturating_add(area.width);

    for (i, line) in lines
        .iter()
        .skip(scroll)
        .take(area.height as usize)
        .enumerate()
    {
        let y = area.y + i as u16;
        let _ = frame.print_text_clipped(area.x, y, line, text_style, max_x);
    }
}

/// Count the number of visible description lines for layout allocation.
fn count_description_lines(meta: &IssueMetadata, _width: u16) -> usize {
    if meta.description.is_empty() {
        return 0;
    }
    // Rough estimate: count newlines. Proper word-wrap would need unicode width.
    meta.description.lines().count().max(1)
}

/// Allocate vertical space between description, discussions, and cross-refs.
///
/// Priority: description gets min(content, 40%), discussions get most of the
/// remaining space, cross-refs get a fixed portion at the bottom.
fn allocate_sections(
    available: u16,
    desc_lines: usize,
    _disc_count: usize,
    xref_count: usize,
) -> (u16, u16, u16) {
    if available == 0 {
        return (0, 0, 0);
    }

    let total = available as usize;

    // Cross-refs: 1 header + count, max 25% of space.
    let xref_need = if xref_count > 0 {
        (1 + xref_count).min(total / 4)
    } else {
        0
    };

    let after_xref = total.saturating_sub(xref_need);

    // Description: up to 40% of remaining, but at least the content lines.
    let desc_max = after_xref * 2 / 5;
    let desc_alloc = desc_lines.min(desc_max).min(after_xref);

    // Discussions: everything else.
    let disc_alloc = after_xref.saturating_sub(desc_alloc);

    (desc_alloc as u16, disc_alloc as u16, xref_need as u16)
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::clock::FakeClock;
    use crate::message::EntityKey;
    use crate::state::issue_detail::{IssueDetailData, IssueMetadata};
    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_metadata() -> IssueMetadata {
        IssueMetadata {
            iid: 42,
            project_path: "group/project".into(),
            title: "Fix authentication flow".into(),
            description: "The login page has a bug.\nSteps to reproduce:\n1. Go to /login\n2. Enter credentials\n3. Click submit".into(),
            state: "opened".into(),
            author: "alice".into(),
            assignees: vec!["bob".into()],
            labels: vec!["backend".into(), "security".into()],
            milestone: Some("v1.0".into()),
            due_date: Some("2026-03-01".into()),
            created_at: 1_700_000_000_000,
            updated_at: 1_700_000_060_000,
            web_url: "https://gitlab.com/group/project/-/issues/42".into(),
            discussion_count: 2,
        }
    }

    fn sample_state_with_metadata() -> IssueDetailState {
        let mut state = IssueDetailState::default();
        state.load_new(EntityKey::issue(1, 42));
        state.apply_metadata(IssueDetailData {
            metadata: sample_metadata(),
            cross_refs: vec![CrossRef {
                kind: CrossRefKind::ClosingMr,
                entity_key: EntityKey::mr(1, 10),
                label: "Fix auth MR".into(),
                navigable: true,
            }],
        });
        state
    }

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

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

    #[test]
    fn test_render_issue_detail_tiny_area() {
        with_frame!(5, 2, |frame| {
            let state = sample_state_with_metadata();
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_issue_detail(&mut frame, &state, Rect::new(0, 0, 5, 2), &clock);
            // Should bail early, no panic.
        });
    }

    #[test]
    fn test_render_issue_detail_with_discussions() {
        with_frame!(80, 40, |frame| {
            let mut state = sample_state_with_metadata();
            state.apply_discussions(vec![DiscussionNode {
                discussion_id: "d1".into(),
                notes: vec![NoteNode {
                    author: "alice".into(),
                    body: "I found the bug".into(),
                    created_at: 1_700_000_000_000,
                    is_system: false,
                    is_diff_note: false,
                    diff_file_path: None,
                    diff_new_line: None,
                }],
                resolvable: false,
                resolved: false,
            }]);
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_issue_detail(&mut frame, &state, Rect::new(0, 0, 80, 40), &clock);
        });
    }

    #[test]
    fn test_render_issue_detail_discussions_loading() {
        with_frame!(80, 24, |frame| {
            let state = sample_state_with_metadata();
            // discussions_loaded is false by default after load_new.
            assert!(!state.discussions_loaded);
            let clock = FakeClock::from_ms(1_700_000_060_000);
            render_issue_detail(&mut frame, &state, Rect::new(0, 0, 80, 24), &clock);
        });
    }

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

    #[test]
    fn test_allocate_sections_empty() {
        assert_eq!(allocate_sections(0, 5, 3, 2), (0, 0, 0));
    }

    #[test]
    fn test_allocate_sections_balanced() {
        let (d, disc, x) = allocate_sections(20, 5, 3, 2);
        assert!(d > 0);
        assert!(disc > 0);
        assert!(x > 0);
        assert_eq!(d + disc + x, 20);
    }

    #[test]
    fn test_allocate_sections_no_xrefs() {
        let (d, disc, x) = allocate_sections(20, 5, 3, 0);
        assert_eq!(x, 0);
        assert_eq!(d + disc, 20);
    }

    #[test]
    fn test_allocate_sections_no_discussions() {
        let (d, disc, x) = allocate_sections(20, 5, 0, 2);
        assert!(d > 0);
        assert_eq!(d + disc + x, 20);
    }

    #[test]
    fn test_count_description_lines() {
        let meta = sample_metadata();
        let lines = count_description_lines(&meta, 80);
        assert_eq!(lines, 5); // 5 lines in the sample description
    }

    #[test]
    fn test_count_description_lines_empty() {
        let mut meta = sample_metadata();
        meta.description = String::new();
        assert_eq!(count_description_lines(&meta, 80), 0);
    }
}
