#![allow(dead_code)]

//! Trace view — file → MR → issue chain drill-down.
//!
//! Layout:
//! ```text
//! +-----------------------------------+
//! | Path: [src/main.rs_]  [R] [D]     |  <- path input + toggles
//! | Renames: old.rs -> new.rs         |  <- shown when renames followed
//! | 3 trace chains                    |  <- summary
//! +-----------------------------------+
//! | > M !42 Fix auth @alice modified  |  <- collapsed chain (selected)
//! |   O !39 Refactor @bob renamed     |  <- collapsed chain
//! |   M !35 Init @carol added         |  <- expanded chain header
//! |     #12 Bug: login broken (close) |    <- linked issue
//! |     @dave: "This path needs..."   |    <- discussion snippet
//! +-----------------------------------+
//! | Enter:expand  r:renames  d:disc   |  <- hint bar
//! +-----------------------------------+
//! ```

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

use crate::state::trace::TraceState;
use crate::text_width::cursor_cell_offset;
use lore::core::trace::TraceResult;

use super::common::truncate_str;
use super::{ACCENT, BG_SURFACE, TEXT, TEXT_MUTED};

// ---------------------------------------------------------------------------
// Colors (Flexoki palette — extras not in parent module)
// ---------------------------------------------------------------------------

const GREEN: PackedRgba = PackedRgba::rgb(0x87, 0x9A, 0x39); // green
const CYAN: PackedRgba = PackedRgba::rgb(0x3A, 0xA9, 0x9F); // cyan
const YELLOW: PackedRgba = PackedRgba::rgb(0xD0, 0xA2, 0x15); // yellow
const RED: PackedRgba = PackedRgba::rgb(0xAF, 0x3A, 0x29); // red
const PURPLE: PackedRgba = PackedRgba::rgb(0x8B, 0x7E, 0xC8); // purple
const SELECTION_BG: PackedRgba = PackedRgba::rgb(0x34, 0x34, 0x31); // bg-3

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

/// Render the Trace screen.
pub fn render_trace(frame: &mut Frame<'_>, state: &TraceState, area: ftui::core::geometry::Rect) {
    if area.width < 10 || area.height < 3 {
        return;
    }

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

    // --- Path input ---
    render_path_input(frame, state, x, y, width);
    y += 1;

    if area.height < 5 {
        return;
    }

    // --- Toggle indicators ---
    render_toggle_indicators(frame, state, x, y, width);
    y += 1;

    // --- Loading ---
    if state.loading {
        render_loading(frame, x, y, max_x);
        return;
    }

    let Some(result) = &state.result else {
        render_empty_state(frame, x, y, max_x);
        return;
    };

    // --- Rename chain ---
    if result.renames_followed && result.resolved_paths.len() > 1 {
        render_rename_chain(frame, &result.resolved_paths, x, y, max_x);
        y += 1;
    }

    // --- Summary ---
    render_summary(frame, result, x, y, max_x);
    y += 1;

    if result.trace_chains.is_empty() {
        render_no_results(frame, x, y, max_x);
        return;
    }

    // Reserve hint bar.
    let hint_y = area.bottom().saturating_sub(1);
    let list_height = hint_y.saturating_sub(y) as usize;

    if list_height == 0 {
        return;
    }

    // --- Chain list ---
    render_chain_list(frame, result, state, x, y, width, list_height);

    // --- Hint bar ---
    render_hint_bar(frame, x, hint_y, max_x);
}

// ---------------------------------------------------------------------------
// Components
// ---------------------------------------------------------------------------

fn render_path_input(frame: &mut Frame<'_>, state: &TraceState, x: u16, y: u16, width: u16) {
    let max_x = x + width;
    let label = "Path: ";
    let label_style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    let after_label = frame.print_text_clipped(x, y, label, label_style, max_x);

    let input_style = Cell {
        fg: if state.path_focused { TEXT } else { TEXT_MUTED },
        ..Cell::default()
    };
    let display_text = if state.path_input.is_empty() && !state.path_focused {
        "type a file path..."
    } else {
        &state.path_input
    };
    frame.print_text_clipped(after_label, y, display_text, input_style, max_x);

    // Cursor.
    if state.path_focused {
        let cursor_x = after_label + cursor_cell_offset(&state.path_input, state.path_cursor);
        if cursor_x < max_x {
            let cursor_cell = Cell {
                fg: PackedRgba::rgb(0x10, 0x0F, 0x0F),
                bg: TEXT,
                ..Cell::default()
            };
            let ch = state
                .path_input
                .get(state.path_cursor..)
                .and_then(|s| s.chars().next())
                .unwrap_or(' ');
            frame.print_text_clipped(cursor_x, y, &ch.to_string(), cursor_cell, max_x);
        }
    }
}

fn render_toggle_indicators(frame: &mut Frame<'_>, state: &TraceState, x: u16, y: u16, width: u16) {
    let max_x = x + width;

    let on_style = Cell {
        fg: GREEN,
        ..Cell::default()
    };
    let off_style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };

    let renames_tag = if state.follow_renames {
        "[renames:on]"
    } else {
        "[renames:off]"
    };
    let disc_tag = if state.include_discussions {
        "[disc:on]"
    } else {
        "[disc:off]"
    };

    let renames_style = if state.follow_renames {
        on_style
    } else {
        off_style
    };
    let disc_style = if state.include_discussions {
        on_style
    } else {
        off_style
    };

    let after_r = frame.print_text_clipped(x + 1, y, renames_tag, renames_style, max_x);
    frame.print_text_clipped(after_r + 1, y, disc_tag, disc_style, max_x);
}

fn render_rename_chain(frame: &mut Frame<'_>, paths: &[String], x: u16, y: u16, max_x: u16) {
    let label_style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    let chain_style = Cell {
        fg: CYAN,
        ..Cell::default()
    };

    let after_label = frame.print_text_clipped(x + 1, y, "Renames: ", label_style, max_x);

    // For long chains, show first 2 + "..." + last.
    let chain_str = if paths.len() > 5 {
        let first_two = paths[..2].join(" -> ");
        let last = &paths[paths.len() - 1];
        format!("{first_two} -> ... ({} more) -> {last}", paths.len() - 3)
    } else {
        paths.join(" -> ")
    };
    frame.print_text_clipped(after_label, y, &chain_str, chain_style, max_x);
}

fn render_summary(frame: &mut Frame<'_>, result: &TraceResult, x: u16, y: u16, max_x: u16) {
    let summary = format!(
        "{} trace chain{}",
        result.total_chains,
        if result.total_chains == 1 { "" } else { "s" },
    );

    let style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    frame.print_text_clipped(x + 1, y, &summary, style, max_x);
}

fn render_chain_list(
    frame: &mut Frame<'_>,
    result: &TraceResult,
    state: &TraceState,
    x: u16,
    start_y: u16,
    width: u16,
    height: usize,
) {
    let max_x = x + width;
    let mut row = 0;

    for (chain_idx, chain) in result.trace_chains.iter().enumerate() {
        if row >= height {
            break;
        }

        let y = start_y + row as u16;
        let selected = chain_idx == state.selected_chain_index;
        let expanded = state.expanded_chains.contains(&chain_idx);

        // Selection background.
        if selected {
            let bg_cell = Cell {
                bg: SELECTION_BG,
                ..Cell::default()
            };
            for col in x..max_x {
                frame.buffer.set(col, y, bg_cell);
            }
        }

        let sel_bg = if selected { SELECTION_BG } else { BG_SURFACE };

        // Expand indicator.
        let expand_icon = if expanded { "v " } else { "> " };
        let prefix = if selected { expand_icon } else { "  " };
        let prefix_style = Cell {
            fg: ACCENT,
            bg: sel_bg,
            ..Cell::default()
        };
        let after_prefix = frame.print_text_clipped(x, y, prefix, prefix_style, max_x);

        // State icon.
        let (icon, icon_color) = match chain.mr_state.as_str() {
            "merged" => ("M", PURPLE),
            "opened" => ("O", GREEN),
            "closed" => ("C", RED),
            _ => ("?", TEXT_MUTED),
        };
        let icon_style = Cell {
            fg: icon_color,
            bg: sel_bg,
            ..Cell::default()
        };
        let after_icon = frame.print_text_clipped(after_prefix, y, icon, icon_style, max_x);

        // !iid
        let iid_str = format!(" !{}", chain.mr_iid);
        let ref_style = Cell {
            fg: ACCENT,
            bg: sel_bg,
            ..Cell::default()
        };
        let after_iid = frame.print_text_clipped(after_icon, y, &iid_str, ref_style, max_x);

        // Title.
        let title = truncate_str(&chain.mr_title, 30);
        let title_style = Cell {
            fg: TEXT,
            bg: sel_bg,
            ..Cell::default()
        };
        let after_title = frame.print_text_clipped(after_iid + 1, y, &title, title_style, max_x);

        // @author + change_type
        let meta = format!(
            "@{} {}",
            truncate_str(&chain.mr_author, 12),
            chain.change_type
        );
        let meta_style = Cell {
            fg: TEXT_MUTED,
            bg: sel_bg,
            ..Cell::default()
        };
        frame.print_text_clipped(after_title + 1, y, &meta, meta_style, max_x);

        row += 1;

        // Expanded content: linked issues + discussions.
        if expanded {
            // Issues.
            for issue in &chain.issues {
                if row >= height {
                    break;
                }
                let iy = start_y + row as u16;

                let issue_icon = match issue.state.as_str() {
                    "opened" => "O",
                    "closed" => "C",
                    _ => "?",
                };
                let issue_icon_color = match issue.state.as_str() {
                    "opened" => GREEN,
                    "closed" => RED,
                    _ => TEXT_MUTED,
                };

                let indent_style = Cell {
                    fg: TEXT_MUTED,
                    ..Cell::default()
                };
                let after_indent = frame.print_text_clipped(
                    x + 4,
                    iy,
                    issue_icon,
                    Cell {
                        fg: issue_icon_color,
                        ..Cell::default()
                    },
                    max_x,
                );

                let issue_ref = format!(" #{} ", issue.iid);
                let issue_ref_style = Cell {
                    fg: YELLOW,
                    ..Cell::default()
                };
                let after_ref =
                    frame.print_text_clipped(after_indent, iy, &issue_ref, issue_ref_style, max_x);

                let issue_title = truncate_str(&issue.title, 40);
                let _ = indent_style; // suppress unused
                frame.print_text_clipped(
                    after_ref,
                    iy,
                    &issue_title,
                    Cell {
                        fg: TEXT,
                        ..Cell::default()
                    },
                    max_x,
                );

                row += 1;
            }

            // Discussions.
            for disc in &chain.discussions {
                if row >= height {
                    break;
                }
                let dy = start_y + row as u16;

                let author = format!("@{}: ", truncate_str(&disc.author_username, 12));
                let author_style = Cell {
                    fg: CYAN,
                    ..Cell::default()
                };
                let after_author =
                    frame.print_text_clipped(x + 4, dy, &author, author_style, max_x);

                let snippet = truncate_str(&disc.body, 60);
                let snippet_style = Cell {
                    fg: TEXT_MUTED,
                    ..Cell::default()
                };
                frame.print_text_clipped(after_author, dy, &snippet, snippet_style, max_x);

                row += 1;
            }
        }
    }
}

fn render_loading(frame: &mut Frame<'_>, x: u16, y: u16, max_x: u16) {
    let style = Cell {
        fg: ACCENT,
        ..Cell::default()
    };
    frame.print_text_clipped(x + 1, y, "Tracing file provenance...", style, max_x);
}

fn render_empty_state(frame: &mut Frame<'_>, x: u16, y: u16, max_x: u16) {
    let style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    frame.print_text_clipped(
        x + 1,
        y,
        "Enter a file path and press Enter to trace.",
        style,
        max_x,
    );
}

fn render_no_results(frame: &mut Frame<'_>, x: u16, y: u16, max_x: u16) {
    let style = Cell {
        fg: TEXT_MUTED,
        ..Cell::default()
    };
    frame.print_text_clipped(x + 1, y, "No trace chains found.", style, max_x);
    frame.print_text_clipped(
        x + 1,
        y + 1,
        "Hint: Run 'lore sync' to fetch MR file changes.",
        style,
        max_x,
    );
}

fn render_hint_bar(frame: &mut Frame<'_>, x: u16, y: u16, max_x: u16) {
    let style = Cell {
        fg: TEXT_MUTED,
        bg: BG_SURFACE,
        ..Cell::default()
    };

    for col in x..max_x {
        frame.buffer.set(col, y, style);
    }

    let hints = "/:path  Enter:expand  r:renames  d:discussions  q:back";
    frame.print_text_clipped(x + 1, y, hints, style, max_x);
}

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

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::*;
    use crate::state::trace::TraceState;
    use ftui::render::grapheme_pool::GraphemePool;
    use lore::core::trace::{TraceChain, TraceResult};

    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 test_area(w: u16, h: u16) -> ftui::core::geometry::Rect {
        ftui::core::geometry::Rect {
            x: 0,
            y: 0,
            width: w,
            height: h,
        }
    }

    fn sample_chain(iid: i64, title: &str, state: &str) -> TraceChain {
        TraceChain {
            mr_iid: iid,
            mr_title: title.into(),
            mr_state: state.into(),
            mr_author: "alice".into(),
            change_type: "modified".into(),
            merged_at_iso: None,
            updated_at_iso: "2024-01-01".into(),
            web_url: None,
            issues: vec![],
            discussions: vec![],
        }
    }

    #[test]
    fn test_render_empty_no_panic() {
        with_frame!(80, 24, |frame| {
            let state = TraceState::default();
            render_trace(&mut frame, &state, test_area(80, 24));
        });
    }

    #[test]
    fn test_render_tiny_terminal_noop() {
        with_frame!(5, 2, |frame| {
            let state = TraceState::default();
            render_trace(&mut frame, &state, test_area(5, 2));
        });
    }

    #[test]
    fn test_render_loading() {
        with_frame!(80, 24, |frame| {
            let state = TraceState {
                loading: true,
                ..TraceState::default()
            };
            render_trace(&mut frame, &state, test_area(80, 24));
        });
    }

    #[test]
    fn test_render_with_chains() {
        with_frame!(100, 30, |frame| {
            let state = TraceState {
                result: Some(TraceResult {
                    path: "src/main.rs".into(),
                    resolved_paths: vec!["src/main.rs".into()],
                    renames_followed: false,
                    trace_chains: vec![
                        sample_chain(42, "Fix auth flow", "merged"),
                        sample_chain(39, "Refactor modules", "opened"),
                    ],
                    total_chains: 2,
                }),
                ..TraceState::default()
            };
            render_trace(&mut frame, &state, test_area(100, 30));
        });
    }

    #[test]
    fn test_render_expanded_chain() {
        with_frame!(100, 30, |frame| {
            let state = TraceState {
                expanded_chains: HashSet::from([0]),
                result: Some(TraceResult {
                    path: "src/main.rs".into(),
                    resolved_paths: vec!["src/main.rs".into()],
                    renames_followed: false,
                    trace_chains: vec![TraceChain {
                        mr_iid: 42,
                        mr_title: "Fix auth".into(),
                        mr_state: "merged".into(),
                        mr_author: "alice".into(),
                        change_type: "modified".into(),
                        merged_at_iso: None,
                        updated_at_iso: "2024-01-01".into(),
                        web_url: None,
                        issues: vec![lore::core::trace::TraceIssue {
                            iid: 12,
                            title: "Login broken".into(),
                            state: "closed".into(),
                            reference_type: "closes".into(),
                            web_url: None,
                        }],
                        discussions: vec![lore::core::trace::TraceDiscussion {
                            discussion_id: "abc".into(),
                            mr_iid: 42,
                            author_username: "bob".into(),
                            body: "This path needs review".into(),
                            path: "src/main.rs".into(),
                            created_at_iso: "2024-01-01".into(),
                        }],
                    }],
                    total_chains: 1,
                }),
                ..TraceState::default()
            };
            render_trace(&mut frame, &state, test_area(100, 30));
        });
    }

    #[test]
    fn test_render_with_rename_chain() {
        with_frame!(80, 24, |frame| {
            let state = TraceState {
                result: Some(TraceResult {
                    path: "src/old.rs".into(),
                    resolved_paths: vec!["src/old.rs".into(), "src/new.rs".into()],
                    renames_followed: true,
                    trace_chains: vec![],
                    total_chains: 0,
                }),
                ..TraceState::default()
            };
            render_trace(&mut frame, &state, test_area(80, 24));
        });
    }

    #[test]
    fn test_truncate_str() {
        assert_eq!(truncate_str("hello", 10), "hello");
        assert_eq!(truncate_str("hello world", 5), "hell…");
        assert_eq!(truncate_str("", 5), "");
    }
}
