//! Flat, JSON-friendly output types for git tools.
//!
//! These types convert from internal `git` crate types to clean, flat structures
//! suitable for JSON serialization (function call responses).

use base64::Engine;
use chrono::TimeZone;
use git::commit::types::{CommitMeta, CommitReflogEntry};
use git::diff::types::{DiffDelta, DiffStats};
use git::tree::types::TreeEntry;
use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Parameter structs used by all tool modules
// ---------------------------------------------------------------------------

#[derive(serde::Deserialize)]
pub struct RevQuery { pub rev: String }

#[derive(serde::Deserialize)]
pub struct SearchCommits { pub query: String, #[serde(default = "dl")] pub limit: u32 }
fn dl() -> u32 { 20 }

#[derive(serde::Deserialize)]
pub struct GraphParams { #[serde(default)] pub rev: Option<String>, #[serde(default = "dl")] pub limit: u32 }

#[derive(serde::Deserialize)]
pub struct ReflogParams { #[serde(default)] pub ref_name: Option<String>, #[serde(default = "reflog_def")] pub limit: u32 }
fn reflog_def() -> u32 { 50 }

#[derive(serde::Deserialize)]
pub struct SingleBranch { pub name: String }

#[derive(serde::Deserialize)]
pub struct BranchesMerged { pub branch: String, #[serde(default)] pub into: Option<String> }

#[derive(serde::Deserialize)]
pub struct BranchDiffP { pub local: String, #[serde(default)] pub remote: Option<String> }

#[derive(serde::Deserialize)]
pub struct DiffP { #[serde(default)] pub base: Option<String>, #[serde(default)] pub head: Option<String>, #[serde(default)] pub paths: Option<Vec<String>> }

#[derive(serde::Deserialize)]
pub struct DiffStatsP { pub base: String, pub head: String }

#[derive(serde::Deserialize)]
pub struct BlameP { pub path: String, #[serde(default)] pub rev: Option<String>, #[serde(default)] pub from_line: Option<u32>, #[serde(default)] pub to_line: Option<u32> }

#[derive(serde::Deserialize)]
pub struct FileContentP { pub path: String, #[serde(default)] pub rev: Option<String> }

#[derive(serde::Deserialize)]
pub struct TreeLsP { #[serde(default)] pub path: Option<String>, #[serde(default)] pub rev: Option<String> }

#[derive(serde::Deserialize)]
pub struct FileHistoryP { pub path: String, #[serde(default = "dl")] pub limit: u32 }

#[derive(serde::Deserialize)]
pub struct TagListP { #[serde(default)] pub pattern: Option<String> }

#[derive(serde::Deserialize)]
pub struct SingleTagP { pub name: String }

#[derive(serde::Deserialize)]
pub struct GitLogP { #[serde(default)] pub rev: Option<String>, #[serde(default = "dl")] pub limit: u32, #[serde(default)] pub skip: u32 }

// ---------------------------------------------------------------------------
// Commit types
// ---------------------------------------------------------------------------

/// Flat commit information for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitInfo {
    pub oid: String,
    pub short_oid: String,
    pub message: String,
    pub summary: String,
    pub author_name: String,
    pub author_email: String,
    pub author_time: String,
    pub committer_name: String,
    pub committer_email: String,
    pub parent_oids: Vec<String>,
    pub tree_oid: String,
}

impl CommitInfo {
    pub fn from_meta(meta: &CommitMeta) -> Self {
        let ts = meta.author.time_secs;
        let offset = meta.author.offset_minutes;
        let author_time = format_rfc3339(ts, offset);

        Self {
            oid: meta.oid.to_string(),
            short_oid: meta.oid.to_string().get(..7).unwrap_or(&meta.oid.to_string()).to_string(),
            message: meta.message.clone(),
            summary: meta.summary.clone(),
            author_name: meta.author.name.clone(),
            author_email: meta.author.email.clone(),
            author_time,
            committer_name: meta.committer.name.clone(),
            committer_email: meta.committer.email.clone(),
            parent_oids: meta.parent_ids.iter().map(|p| p.to_string()).collect(),
            tree_oid: meta.tree_id.to_string(),
        }
    }
}

/// Commit reflog entry for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflogEntryInfo {
    pub oid_new: String,
    pub oid_old: String,
    pub committer_name: String,
    pub committer_email: String,
    pub time: String,
    pub message: Option<String>,
    pub ref_name: String,
}

impl ReflogEntryInfo {
    pub fn from_entry(entry: &CommitReflogEntry) -> Self {
        let ts = entry.time_secs;
        let time = format_rfc3339(ts, 0);
        Self {
            oid_new: entry.oid_new.to_string(),
            oid_old: entry.oid_old.to_string(),
            committer_name: entry.committer_name.clone(),
            committer_email: entry.committer_email.clone(),
            time,
            message: entry.message.clone(),
            ref_name: entry.ref_name.clone(),
        }
    }
}

// ---------------------------------------------------------------------------
// Branch types
// ---------------------------------------------------------------------------

/// Flat branch info for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchInfoOut {
    pub name: String,
    pub oid: String,
    pub short_oid: String,
    pub is_head: bool,
    pub is_remote: bool,
    pub is_current: bool,
    pub upstream: Option<String>,
}

impl From<&git::branch::types::BranchInfo> for BranchInfoOut {
    fn from(b: &git::branch::types::BranchInfo) -> Self {
        let oid = b.oid.to_string();
        Self {
            name: b.name.clone(),
            oid: oid.clone(),
            short_oid: oid.get(..7).unwrap_or(&oid).to_string(),
            is_head: b.is_head,
            is_remote: b.is_remote,
            is_current: b.is_current,
            upstream: b.upstream.clone(),
        }
    }
}

/// Branch diff (ahead/behind) for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchDiffOut {
    pub ahead: usize,
    pub behind: usize,
    pub diverged: bool,
}

impl From<&git::branch::types::BranchDiff> for BranchDiffOut {
    fn from(d: &git::branch::types::BranchDiff) -> Self {
        Self {
            ahead: d.ahead,
            behind: d.behind,
            diverged: d.diverged,
        }
    }
}

// ---------------------------------------------------------------------------
// Diff types
// ---------------------------------------------------------------------------

/// Diff statistics for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffStatsOut {
    pub files_changed: u32,
    pub insertions: u32,
    pub deletions: u32,
}

impl From<&DiffStats> for DiffStatsOut {
    fn from(s: &DiffStats) -> Self {
        Self {
            files_changed: s.files_changed as u32,
            insertions: s.insertions as u32,
            deletions: s.deletions as u32,
        }
    }
}

/// A single diff file change for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffFileOut {
    pub path: Option<String>,
    pub status: String,
    pub is_binary: bool,
    pub size: u64,
}

impl DiffFileOut {
    pub fn from_delta(delta: &DiffDelta) -> Self {
        Self {
            path: delta.new_file.path.clone(),
            status: format!("{:?}", delta.status),
            is_binary: delta.new_file.is_binary,
            size: delta.new_file.size,
        }
    }
}

/// Diff summary (files + stats) for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffOut {
    pub stats: DiffStatsOut,
    pub files: Vec<DiffFileOut>,
}

impl DiffOut {
    pub fn from_result(result: &git::diff::types::DiffResult) -> Self {
        let stats = DiffStatsOut::from(&result.stats);
        let files = result.deltas.iter().map(DiffFileOut::from_delta).collect();
        Self { stats, files }
    }
}

/// Blame hunk for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlameHunkOut {
    pub commit_oid: String,
    pub short_oid: String,
    pub final_start_line: u32,
    pub final_lines: u32,
    pub orig_start_line: u32,
    pub orig_path: Option<String>,
    pub boundary: bool,
}

impl From<&git::commit::types::CommitBlameHunk> for BlameHunkOut {
    fn from(h: &git::commit::types::CommitBlameHunk) -> Self {
        let oid = h.commit_oid.to_string();
        Self {
            commit_oid: oid.clone(),
            short_oid: oid.get(..7).unwrap_or(&oid).to_string(),
            final_start_line: h.final_start_line,
            final_lines: h.final_lines,
            orig_start_line: h.orig_start_line,
            orig_path: h.orig_path.clone(),
            boundary: h.boundary,
        }
    }
}

// ---------------------------------------------------------------------------
// Tree types
// ---------------------------------------------------------------------------

/// Directory entry for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeLsEntry {
    pub name: String,
    pub oid: String,
    pub kind: String,
    pub is_binary: bool,
}

impl From<&TreeEntry> for TreeLsEntry {
    fn from(entry: &TreeEntry) -> Self {
        Self {
            name: entry.name.clone(),
            oid: entry.oid.to_string(),
            kind: entry.kind.clone(),
            is_binary: entry.is_binary,
        }
    }
}

/// File content for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContentOut {
    pub path: String,
    pub oid: String,
    pub size: u64,
    pub content: String,
    pub is_binary: bool,
}

impl FileContentOut {
    pub fn from_blob(
        path: String,
        oid: &git::commit::types::CommitOid,
        content: &[u8],
        is_binary: bool,
    ) -> Self {
        let (display_content, size) = if is_binary {
            (
                base64::engine::general_purpose::STANDARD.encode(content),
                content.len() as u64,
            )
        } else {
            (
                String::from_utf8_lossy(content).to_string(),
                content.len() as u64,
            )
        };

        Self {
            path,
            oid: oid.to_string(),
            size,
            content: display_content,
            is_binary,
        }
    }
}

// ---------------------------------------------------------------------------
// Tag types
// ---------------------------------------------------------------------------

/// Tag info for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagInfoOut {
    pub name: String,
    pub oid: String,
    pub target: String,
    pub is_annotated: bool,
    pub message: Option<String>,
    pub tagger_name: Option<String>,
    pub tagger_email: Option<String>,
}

impl From<&git::tags::types::TagInfo> for TagInfoOut {
    fn from(t: &git::tags::types::TagInfo) -> Self {
        Self {
            name: t.name.clone(),
            oid: t.oid.to_string(),
            target: t.target.to_string(),
            is_annotated: t.is_annotated,
            message: t.message.clone(),
            tagger_name: t.tagger.clone(),
            tagger_email: t.tagger_email.clone(),
        }
    }
}

// ---------------------------------------------------------------------------
// Graph types
// ---------------------------------------------------------------------------

/// Commit graph line for tool responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphLineOut {
    pub oid: String,
    pub short_oid: String,
    pub refs: String,
    pub short_message: String,
    pub lane_index: usize,
    pub author_name: String,
    pub author_email: String,
    pub author_time: String,
    pub parent_oids: Vec<String>,
}

impl From<&git::commit::graph::CommitGraphLine> for GraphLineOut {
    fn from(line: &git::commit::graph::CommitGraphLine) -> Self {
        let oid = line.oid.to_string();
        let ts = line.meta.author.time_secs;
        let offset = line.meta.author.offset_minutes;
        Self {
            oid: oid.clone(),
            short_oid: oid.get(..7).unwrap_or(&oid).to_string(),
            refs: line.refs.clone(),
            short_message: line.short_message.clone(),
            lane_index: line.lane_index,
            author_name: line.meta.author.name.clone(),
            author_email: line.meta.author.email.clone(),
            author_time: format_rfc3339(ts, offset),
            parent_oids: line.meta.parent_ids.iter().map(|p| p.to_string()).collect(),
        }
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn format_rfc3339(time_secs: i64, offset_minutes: i32) -> String {
    let secs = time_secs + (offset_minutes as i64 * 60);
    chrono::Utc
        .timestamp_opt(secs, 0)
        .single()
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| format!("{}", time_secs))
}
