use std::time::Duration;

use juniper::graphql_object;
use serde::{Deserialize, Serialize};

use crate::{
    cmd::{
        commit::{
            CommitMeta, CommitSignature,
            commit_summary::CommitSummary,
            commit_walker::{CommitWalkParams, CommitWalkSort},
        },
        oid::ObjectId,
    },
    graphql::{
        GraphqlContext,
        cache_helper::{
            IMMUTABLE_TTL, MUTABLE_TTL, cache_key, cached_json,
            mutable_cache_key, repo_revision,
        },
    },
};

const COMMIT_TTL: Duration = IMMUTABLE_TTL;

#[derive(Clone, Serialize, Deserialize)]
pub struct CommitGql {
    pub oid: String,
    pub message: String,
    pub summary: String,
    pub author: CommitSignatureGql,
    pub committer: CommitSignatureGql,
    pub tree_id: String,
    pub parent_ids: Vec<String>,
    pub encoding: Option<String>,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct CommitSignatureGql {
    pub name: String,
    pub email: String,
    pub time_secs: f64,
    pub offset_minutes: i32,
}

#[graphql_object(context = GraphqlContext)]
impl CommitSignatureGql {
    fn name(&self) -> &str {
        &self.name
    }
    fn email(&self) -> &str {
        &self.email
    }
    fn time_secs(&self) -> f64 {
        self.time_secs
    }
    fn offset_minutes(&self) -> i32 {
        self.offset_minutes
    }
}

#[graphql_object(context = GraphqlContext)]
impl CommitGql {
    fn oid(&self) -> &str {
        &self.oid
    }
    fn message(&self) -> &str {
        &self.message
    }
    fn summary(&self) -> &str {
        &self.summary
    }
    fn author(&self) -> &CommitSignatureGql {
        &self.author
    }
    fn committer(&self) -> &CommitSignatureGql {
        &self.committer
    }
    fn tree_id(&self) -> &str {
        &self.tree_id
    }
    fn parent_ids(&self) -> &[String] {
        &self.parent_ids
    }
    fn encoding(&self) -> Option<&str> {
        self.encoding.as_deref()
    }
}

impl From<CommitSignature> for CommitSignatureGql {
    fn from(s: CommitSignature) -> Self {
        CommitSignatureGql {
            name: s.name,
            email: s.email,
            time_secs: s.time_secs as f64,
            offset_minutes: s.offset_minutes,
        }
    }
}

impl From<CommitMeta> for CommitGql {
    fn from(m: CommitMeta) -> Self {
        CommitGql {
            oid: m.oid.0,
            message: m.message,
            summary: m.summary,
            author: CommitSignatureGql::from(m.author),
            committer: CommitSignatureGql::from(m.committer),
            tree_id: m.tree_id.0,
            parent_ids: m.parent_ids.iter().map(|p| p.0.clone()).collect(),
            encoding: m.encoding,
        }
    }
}

pub async fn resolve_commit(
    ctx: &GraphqlContext,
    oid: String,
) -> anyhow::Result<CommitGql> {
    let key = cache_key("query:git:commit", &[&oid]);
    cached_json(&ctx.cache, &key, COMMIT_TTL, || {
        let repo = ctx.repo.clone();
        let oid_obj = ObjectId::new(&oid);
        async move {
            let meta =
                tokio::task::spawn_blocking(move || repo.commit_info(oid_obj))
                    .await?
                    .map_err(|e| anyhow::anyhow!(e))?;
            Ok(CommitGql::from(meta))
        }
    })
    .await
}

pub async fn resolve_commit_history(
    ctx: &GraphqlContext,
    limit: Option<i32>,
    skip: Option<i32>,
    sort: Option<String>,
) -> anyhow::Result<Vec<CommitGql>> {
    let revision = repo_revision(ctx).await;
    let sort_str = sort.as_deref().unwrap_or("time");
    let key_parts = format!(
        "{}:{}:{}:{}",
        limit.map(|l| l.to_string()).unwrap_or_default(),
        skip.map(|s| s.to_string()).unwrap_or_default(),
        sort_str,
        revision,
    );
    let key = mutable_cache_key(
        ctx,
        "query:git:commit_history",
        &[&key_parts],
        &revision,
    );
    cached_json(&ctx.cache, &key, MUTABLE_TTL, || {
        let repo = ctx.repo.clone();
        let walk_sort = match sort_str {
            "topological" => CommitWalkSort::Topological,
            "time" => CommitWalkSort::Time,
            "reverse" => CommitWalkSort::Reverse,
            _ => CommitWalkSort::Time,
        };
        let params = CommitWalkParams {
            limit: limit.map(|l| l as usize),
            skip: skip.unwrap_or(0) as usize,
            sort: walk_sort,
            ..Default::default()
        };
        async move {
            let commits = tokio::task::spawn_blocking(move || {
                repo.commit_history(params)
            })
            .await?
            .map_err(|e| anyhow::anyhow!(e))?;
            Ok(commits.into_iter().map(CommitGql::from).collect())
        }
    })
    .await
}

pub async fn resolve_commit_summary(
    ctx: &GraphqlContext,
) -> anyhow::Result<CommitSummaryGql> {
    let revision = repo_revision(ctx).await;
    let key =
        mutable_cache_key(ctx, "query:git:commit_summary", &[], &revision);
    cached_json(&ctx.cache, &key, MUTABLE_TTL, || {
        let repo = ctx.repo.clone();
        async move {
            let summary =
                tokio::task::spawn_blocking(move || repo.commit_summary())
                    .await?
                    .map_err(|e| anyhow::anyhow!(e))?;
            Ok(CommitSummaryGql::from(summary))
        }
    })
    .await
}

#[derive(Clone, Serialize, Deserialize)]
pub struct CommitSummaryGql {
    pub head: Option<CommitGql>,
    pub count: i32,
}

#[graphql_object(context = GraphqlContext)]
impl CommitSummaryGql {
    fn head(&self) -> Option<&CommitGql> {
        self.head.as_ref()
    }
    fn count(&self) -> i32 {
        self.count
    }
}

impl From<CommitSummary> for CommitSummaryGql {
    fn from(s: CommitSummary) -> Self {
        CommitSummaryGql {
            head: s.head.map(CommitGql::from),
            count: s.count as i32,
        }
    }
}
