use ratatui::{
    buffer::Buffer,
    layout::{Constraint, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph, Tabs, Widget, Wrap},
};

use indexmap::IndexMap;

use super::theme::Theme;
use crate::model::{CiSystem, Job, RuleResult, VarOrigin};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailTab {
    Config,
    Script,
    Variables,
    Debug,
}

impl DetailTab {
    pub fn titles() -> Vec<&'static str> {
        vec!["Config", "Script", "Variables", "Debug"]
    }

    pub fn index(self) -> usize {
        match self {
            DetailTab::Config => 0,
            DetailTab::Script => 1,
            DetailTab::Variables => 2,
            DetailTab::Debug => 3,
        }
    }

    pub fn next(self) -> Self {
        match self {
            DetailTab::Config => DetailTab::Script,
            DetailTab::Script => DetailTab::Variables,
            DetailTab::Variables => DetailTab::Debug,
            DetailTab::Debug => DetailTab::Config,
        }
    }

    pub fn prev(self) -> Self {
        match self {
            DetailTab::Config => DetailTab::Debug,
            DetailTab::Script => DetailTab::Config,
            DetailTab::Variables => DetailTab::Script,
            DetailTab::Debug => DetailTab::Variables,
        }
    }
}

pub struct JobDetailView<'a> {
    pub job: &'a Job,
    pub edited_vars: Option<&'a IndexMap<String, String>>,
    pub tab: DetailTab,
    pub scroll: u16,
    pub theme: &'a Theme,
    pub ci_system: CiSystem,
}

impl<'a> Widget for JobDetailView<'a> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(1)]).split(area);

        // Header with tabs
        render_header(
            self.job,
            self.tab,
            chunks[0],
            buf,
            self.theme,
            self.ci_system,
        );

        // Content
        let content_area = chunks[1];
        let block = Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(self.theme.border_unfocused));
        let inner = block.inner(content_area);
        block.render(content_area, buf);

        match self.tab {
            DetailTab::Config => render_config(self.job, inner, buf, self.scroll, self.theme),
            DetailTab::Script => render_script(
                self.job,
                self.edited_vars,
                inner,
                buf,
                self.scroll,
                self.theme,
                self.ci_system,
            ),
            DetailTab::Variables => render_variables(
                self.job,
                self.edited_vars,
                inner,
                buf,
                self.scroll,
                self.theme,
            ),
            DetailTab::Debug => render_debug(
                self.job,
                inner,
                buf,
                self.scroll,
                self.theme,
                self.ci_system,
            ),
        }
    }
}

fn render_header(
    job: &Job,
    active_tab: DetailTab,
    area: Rect,
    buf: &mut Buffer,
    theme: &Theme,
    ci_system: CiSystem,
) {
    let chunks = Layout::vertical([Constraint::Length(1), Constraint::Length(2)]).split(area);

    // Job info line
    let mut info_spans = vec![
        Span::styled(
            &job.name,
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw("  "),
        Span::styled(
            format!("stage: {}", job.stage),
            Style::default().fg(theme.success),
        ),
    ];
    if let Some(ref image) = job.image {
        let label = match ci_system {
            CiSystem::GitHub => "container",
            CiSystem::GitLab => "image",
        };
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled(
            format!("{}: {}", label, image),
            Style::default().fg(theme.info),
        ));
    }
    if !job.tags.is_empty() {
        let label = match ci_system {
            CiSystem::GitHub => "runs-on",
            CiSystem::GitLab => "tags",
        };
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled(
            format!("{}: {}", label, job.tags.join(", ")),
            Style::default().fg(theme.warning),
        ));
    }
    if ci_system == CiSystem::GitLab && job.is_never() {
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled(
            "[disabled by rules]",
            Style::default().fg(theme.error),
        ));
    } else if ci_system == CiSystem::GitLab && job.is_manual() {
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled("[manual]", Style::default().fg(theme.warning)));
    }
    if job.is_unresolved() {
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled(
            format!("[unresolved: {}]", job.unresolved_extends.join(", ")),
            Style::default().fg(theme.warning),
        ));
    }
    if !job.needs.is_empty() {
        info_spans.push(Span::raw("  "));
        info_spans.push(Span::styled(
            format!("needs: {}", job.need_names().join(", ")),
            Style::default().fg(theme.text_dim),
        ));
    }

    let info_line = Line::from(info_spans);
    Paragraph::new(info_line).render(chunks[0], buf);

    // Tabs
    let tabs = Tabs::new(DetailTab::titles().into_iter().map(String::from))
        .select(active_tab.index())
        .style(Style::default().fg(theme.text_dim))
        .highlight_style(
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        )
        .divider("│");
    tabs.render(chunks[1], buf);
}

fn render_config(job: &Job, area: Rect, buf: &mut Buffer, scroll: u16, theme: &Theme) {
    let text = &job.resolved_yaml;
    let lines: Vec<Line> = text
        .lines()
        .map(|line| {
            let style = if line.contains(':') && !line.starts_with(' ') {
                Style::default().fg(theme.accent)
            } else if line.trim_start().starts_with('-') {
                Style::default().fg(theme.success)
            } else {
                Style::default().fg(theme.text)
            };
            Line::from(Span::styled(line.to_string(), style))
        })
        .collect();

    Paragraph::new(lines)
        .scroll((scroll, 0))
        .wrap(Wrap { trim: false })
        .render(area, buf);
}

fn render_script(
    job: &Job,
    edited_vars: Option<&IndexMap<String, String>>,
    area: Rect,
    buf: &mut Buffer,
    scroll: u16,
    theme: &Theme,
    ci_system: CiSystem,
) {
    let empty = IndexMap::new();
    let script_text =
        job.build_local_script_with_extra_vars(ci_system, edited_vars.unwrap_or(&empty));
    let lines: Vec<Line> = script_text
        .lines()
        .map(|line| {
            let style = if line.starts_with('#') {
                Style::default().fg(theme.text_dim)
            } else if line.starts_with("export ") {
                Style::default().fg(theme.warning)
            } else if line.starts_with("#!/") {
                Style::default().fg(theme.info)
            } else {
                Style::default().fg(theme.text)
            };
            Line::from(Span::styled(line.to_string(), style))
        })
        .collect();

    Paragraph::new(lines)
        .scroll((scroll, 0))
        .wrap(Wrap { trim: false })
        .render(area, buf);
}

fn render_variables(
    job: &Job,
    edited_vars: Option<&IndexMap<String, String>>,
    area: Rect,
    buf: &mut Buffer,
    scroll: u16,
    theme: &Theme,
) {
    let mut merged = job.variables.clone();
    if let Some(overrides) = edited_vars {
        for (k, v) in overrides {
            merged.insert(k.clone(), v.clone());
        }
    }
    let mut sorted_vars: Vec<_> = merged.iter().collect();
    sorted_vars.sort_by_key(|(k, _)| k.as_str());
    let is_edited = |k: &str| edited_vars.is_some_and(|ev| ev.contains_key(k));
    let lines: Vec<Line> = sorted_vars
        .into_iter()
        .map(|(k, v)| {
            let key_color = if is_edited(k) {
                theme.warning
            } else {
                theme.accent
            };
            Line::from(vec![
                Span::styled(k.as_str(), Style::default().fg(key_color)),
                Span::raw(" = "),
                Span::styled(v.as_str(), Style::default().fg(theme.text)),
            ])
        })
        .collect();

    Paragraph::new(lines)
        .scroll((scroll, 0))
        .wrap(Wrap { trim: false })
        .render(area, buf);
}

fn render_debug(
    job: &Job,
    area: Rect,
    buf: &mut Buffer,
    scroll: u16,
    theme: &Theme,
    ci_system: CiSystem,
) {
    let debug = &job.debug_info;
    let mut lines: Vec<Line> = Vec::new();

    // ── Defined in ──
    let mut header_spans = vec![Span::styled(
        "Defined in  ",
        Style::default()
            .fg(theme.accent)
            .add_modifier(Modifier::BOLD),
    )];
    if let Some(loc) = &debug.source {
        header_spans.push(Span::styled(
            format!("{}:{}", loc.file, loc.line),
            Style::default().fg(theme.info),
        ));
    } else {
        header_spans.push(Span::styled(
            "(unknown)",
            Style::default().fg(theme.text_dim),
        ));
    }
    lines.push(Line::from(header_spans));
    lines.push(Line::from(""));

    // ── Rules evaluation ──
    if !job.rules_summary.is_empty() {
        lines.push(Line::from(Span::styled(
            "Rules evaluation",
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        )));
        for (i, rule) in job.rules_summary.iter().enumerate() {
            let (tag, tag_color) = match rule.result {
                RuleResult::Matched => ("(matched)", theme.success),
                RuleResult::NotReached => ("(not reached)", theme.text_dim),
                RuleResult::IfFalse => ("(if: false)", theme.text_dim),
                RuleResult::IfIndeterminate => ("(if: unknown var)", theme.warning),
                RuleResult::ChangesFalse => ("(changes: no match)", theme.text_dim),
                RuleResult::ChangesIndeterminate => ("(changes: skipped)", theme.warning),
                RuleResult::ExistsSkipped => ("(exists: skipped)", theme.warning),
            };
            let text_color = match rule.result {
                RuleResult::Matched => theme.text,
                RuleResult::NotReached => theme.text_dim,
                _ => theme.text,
            };
            // First line: rule number + tag
            lines.push(Line::from(vec![
                Span::styled(
                    format!("  {}. ", i + 1),
                    Style::default().fg(theme.text_dim),
                ),
                Span::styled(tag, Style::default().fg(tag_color)),
            ]));
            // Full YAML of the rule entry, indented
            for yaml_line in rule.text.lines() {
                lines.push(Line::from(Span::styled(
                    format!("     {}", yaml_line),
                    Style::default().fg(text_color),
                )));
            }
        }
        lines.push(Line::from(""));
    }

    // ── Extends chain (GitLab only) ──
    if ci_system == CiSystem::GitLab {
        lines.push(Line::from(Span::styled(
            "Extends chain",
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        )));
        if debug.extends_chain.is_empty() {
            lines.push(Line::from(Span::styled(
                "  (none)",
                Style::default().fg(theme.text_dim),
            )));
        } else {
            for (i, (parent, source)) in debug.extends_chain.iter().enumerate() {
                let arrow = if i == 0 { "  " } else { "    -> " };
                let mut spans = vec![Span::styled(
                    format!("{}{}", arrow, parent),
                    Style::default().fg(theme.info),
                )];
                if let Some(loc) = source {
                    spans.push(Span::styled(
                        format!("  ({}:{})", loc.file, loc.line),
                        Style::default().fg(theme.text_dim),
                    ));
                }
                lines.push(Line::from(spans));
            }
        }
        lines.push(Line::from(""));
    }

    // ── Key provenance (GitLab only) ──
    if ci_system == CiSystem::GitLab && !debug.key_provenance.is_empty() {
        lines.push(Line::from(Span::styled(
            "Key provenance",
            Style::default()
                .fg(theme.accent)
                .add_modifier(Modifier::BOLD),
        )));

        // Find max key length for alignment
        let max_key_len = debug
            .key_provenance
            .keys()
            .map(|k| k.len())
            .max()
            .unwrap_or(0);

        for (key, (source_name, source_loc)) in &debug.key_provenance {
            let is_self = source_name == &job.name;
            let is_default = source_name == "default:";
            let source_color = if is_self {
                theme.text
            } else if is_default {
                theme.warning
            } else {
                theme.info
            };
            let display_name = if is_self {
                "(this job)".to_string()
            } else {
                source_name.clone()
            };

            let mut spans = vec![
                Span::styled(
                    format!("  {:width$}", key, width = max_key_len),
                    Style::default().fg(theme.text),
                ),
                Span::styled(" <- ", Style::default().fg(theme.text_dim)),
                Span::styled(display_name, Style::default().fg(source_color)),
            ];
            if let Some(loc) = source_loc
                && !is_self
            {
                spans.push(Span::styled(
                    format!("  ({}:{})", loc.file, loc.line),
                    Style::default().fg(theme.text_dim),
                ));
            }
            lines.push(Line::from(spans));
        }
        lines.push(Line::from(""));
    }

    // ── Variable origins ──
    lines.push(Line::from(Span::styled(
        "Variable origins",
        Style::default()
            .fg(theme.accent)
            .add_modifier(Modifier::BOLD),
    )));

    let origin_order = [
        (VarOrigin::Override, "Override (-e)"),
        (
            VarOrigin::GlobalOverriddenByJob,
            "Global (overridden by job)",
        ),
        (VarOrigin::Job, "Job"),
        (VarOrigin::Rule, "Rule"),
        (VarOrigin::Matrix, "Matrix"),
        (VarOrigin::Global, "Global"),
        (VarOrigin::Predefined, "Predefined"),
    ];

    for (origin, label) in &origin_order {
        let vars: Vec<&String> = debug
            .variable_origins
            .iter()
            .filter(|(_, o)| *o == origin)
            .map(|(k, _)| k)
            .collect();

        if vars.is_empty() {
            continue;
        }

        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("  {}", label),
            Style::default()
                .fg(theme.success)
                .add_modifier(Modifier::BOLD),
        )));

        for var_name in &vars {
            let value = job
                .variables
                .get(var_name.as_str())
                .map(|v| v.as_str())
                .unwrap_or("");
            let color = match origin {
                VarOrigin::Override => theme.warning,
                VarOrigin::GlobalOverriddenByJob => theme.warning,
                VarOrigin::Job => theme.info,
                VarOrigin::Rule => theme.info,
                VarOrigin::Matrix => theme.accent,
                VarOrigin::Global => theme.text,
                VarOrigin::Predefined => theme.text_dim,
            };
            lines.push(Line::from(vec![
                Span::styled(format!("    {}", var_name), Style::default().fg(color)),
                Span::raw(" = "),
                Span::styled(value, Style::default().fg(theme.text_dim)),
            ]));
        }
    }

    Paragraph::new(lines)
        .scroll((scroll, 0))
        .wrap(Wrap { trim: false })
        .render(area, buf);
}
