//! Visual ASCII commit graph output.

use serde::{Deserialize, Serialize};

use crate::commit::types::*;
use crate::{GitDomain, GitResult};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitGraphLine {
    pub oid: CommitOid,
    pub graph_chars: String,
    pub refs: String,
    pub short_message: String,
    /// Column index (0-based) where the commit dot is rendered.
    /// Used by @gitgraph/react to assign lane color.
    pub lane_index: usize,
    /// Full commit metadata (author, timestamp, parents, etc.)
    pub meta: CommitMeta,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitGraph {
    pub lines: Vec<CommitGraphLine>,
    pub max_parents: usize,
}

#[derive(Default)]
struct GraphState {
    column_to_commit: Vec<Option<CommitOid>>,
    column_prev_commit: Vec<Option<CommitOid>>,
    active_columns: usize,
}

pub struct CommitGraphOptions {
    pub rev: Option<String>,
    pub limit: usize,
    pub first_parent_only: bool,
    pub show_refs: bool,
}

impl Default for CommitGraphOptions {
    fn default() -> Self {
        Self {
            rev: None,
            limit: 0,
            first_parent_only: false,
            show_refs: true,
        }
    }
}

impl CommitGraphOptions {
    pub fn rev(mut self, rev: &str) -> Self {
        self.rev = Some(rev.to_string());
        self
    }

    pub fn limit(mut self, n: usize) -> Self {
        self.limit = n;
        self
    }

    pub fn first_parent_only(mut self) -> Self {
        self.first_parent_only = true;
        self
    }

    pub fn no_refs(mut self) -> Self {
        self.show_refs = false;
        self
    }
}

impl GitDomain {
    pub fn commit_graph(&self, opts: CommitGraphOptions) -> GitResult<CommitGraph> {
        let commits = self.commit_walk(crate::commit::traverse::CommitWalkOptions {
            rev: opts.rev.map(String::from),
            sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()),
            limit: opts.limit,
            first_parent_only: opts.first_parent_only,
        })?;

        let mut state = GraphState::default();
        let mut lines = Vec::with_capacity(commits.len());
        let mut max_parents = 0;

        for commit in commits {
            max_parents = max_parents.max(commit.parent_ids.len());
            let line = self.build_graph_line(&commit, &mut state, opts.show_refs)?;
            lines.push(line);
        }

        Ok(CommitGraph { lines, max_parents })
    }

    pub fn commit_graph_simple(&self, rev: Option<&str>, limit: usize) -> GitResult<CommitGraph> {
        let opts = CommitGraphOptions::default().limit(limit);
        let opts = match rev {
            Some(r) => opts.rev(r),
            None => opts,
        };
        self.commit_graph(opts)
    }

    fn build_graph_line(
        &self,
        commit: &CommitMeta,
        state: &mut GraphState,
        show_refs: bool,
    ) -> GitResult<CommitGraphLine> {
        let oid = commit.oid.clone();
        let short_message = commit.summary.clone();

        let refs = if show_refs {
            self.get_commit_refs_string(&oid)?
        } else {
            String::new()
        };

        let (graph_chars, lane_index) = self.render_graph_chars(commit, state);

        Ok(CommitGraphLine {
            oid,
            graph_chars,
            refs,
            short_message,
            lane_index,
            meta: commit.clone(),
        })
    }

    fn get_commit_refs_string(&self, oid: &CommitOid) -> GitResult<String> {
        let refs = self.commit_refs(oid)?;
        let parts: Vec<String> = refs
            .iter()
            .map(|r| {
                if r.is_tag {
                    r.name.trim_start_matches("refs/tags/").to_string()
                } else {
                    r.name.trim_start_matches("refs/heads/").to_string()
                }
            })
            .collect();
        Ok(parts.join(", "))
    }

    fn render_graph_chars(&self, commit: &CommitMeta, state: &mut GraphState) -> (String, usize) {
        let current_oid = &commit.oid;
        let num_parents = commit.parent_ids.len();

        let mut result = String::new();
        let active: Vec<bool> = state
            .column_to_commit
            .iter()
            .map(|col_oid| col_oid.as_ref().map_or(false, |col| col == current_oid))
            .collect();

        let current_col = if let Some(pos) = active.iter().position(|a| *a) {
            pos
        } else {
            if state.active_columns == 0 {
                state.active_columns = 1;
            }
            let col = state.column_to_commit.len();
            state.column_to_commit.push(Some(current_oid.clone()));
            state.column_prev_commit.push(None);
            state.active_columns += 1;
            col
        };

        for i in 0..state.column_to_commit.len() {
            if i == current_col {
                result.push_str("* ");
            } else if state.column_to_commit[i].is_some() {
                result.push_str("| ");
            } else {
                result.push_str("  ");
            }
        }

        if num_parents > 1 {
            let available = state
                .column_to_commit
                .len()
                .saturating_sub(state.active_columns);
            if available > 0 {
                result.push_str(&format!("/{}", " ".repeat(available * 2 - 1)));
            }
        }

        for col in 0..state.column_to_commit.len() {
            if state.column_to_commit[col].as_ref() == Some(current_oid) {
                state.column_to_commit[col] = None;
                state.active_columns = state.active_columns.saturating_sub(1);
            }
        }

        for (i, parent) in commit.parent_ids.iter().enumerate() {
            if i == 0 && state.active_columns == 0 {
                state.column_to_commit[0] = Some(parent.clone());
                state.column_prev_commit[0] = Some(current_oid.clone());
                state.active_columns = 1;
            } else {
                if let Some(idx) = state.column_to_commit.iter().position(|c| c.is_none()) {
                    state.column_to_commit[idx] = Some(parent.clone());
                    state.column_prev_commit[idx] = Some(current_oid.clone());
                    state.active_columns += 1;
                } else {
                    state.column_to_commit.push(Some(parent.clone()));
                    state.column_prev_commit.push(Some(current_oid.clone()));
                    state.active_columns += 1;
                }
            }
        }

        (result, current_col)
    }
}
