// Back - An extremely simple git issue tracking system. Inspired by tvix's
// panettone
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This file is part of Back.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

use std::path::Path;

use gix::{bstr::ByteSlice, refs::Target, Commit, Id, ObjectId, Repository};

use crate::error;

use super::issue::{
    entity::{Entity, RawEntity},
    CollapsedIssue, RawCollapsedIssue,
};

#[derive(Debug)]
pub struct Dag {
    entities: Vec<Entity>,
}

impl Dag {
    pub fn collapse(self) -> CollapsedIssue {
        let raw_collapsed_issue = self.entities.into_iter().rev().fold(
            RawCollapsedIssue::default(),
            |mut collapsed_issue, entity| {
                collapsed_issue.append_entity(entity);
                collapsed_issue
            },
        );

        CollapsedIssue::from(raw_collapsed_issue)
    }

    /// Construct a DAG from the root child upwards.
    pub fn construct(repo: &Repository, id: ObjectId) -> Self {
        let mut entities = vec![];

        let base_commit = repo
            .find_object(id)
            .expect("The object with this id should exist.")
            .try_into_commit()
            .expect("The git-bug's data model enforces this.");

        entities.push(Self::commit_to_operations(repo, &base_commit));

        let mut current_commit = base_commit;
        while let Some(parent_id) = Self::try_get_parent(repo, &current_commit) {
            entities.push(Self::commit_to_operations(repo, &parent_id));
            current_commit = parent_id;
        }

        Self {
            entities: {
                entities
                    .into_iter()
                    .map(|(raw_entity, id)| Entity::from_raw(repo, raw_entity, id))
                    .collect()
            },
        }
    }

    fn commit_to_operations<'b>(repo: &Repository, id: &Commit<'b>) -> (RawEntity, Id<'b>) {
        let tree_obj = repo
            .find_object(
                id.tree_id()
                    .expect("All of git-bug's commits should have trees attached to them'"),
            )
            .expect("The object with this id should exist.")
            .try_into_tree()
            .expect("git-bug's data model enforces this.");

        let ops_ref = tree_obj
            .find_entry("ops")
            .expect("All of git-bug's trees should contain a 'ops' json file");

        let issue_data = repo
            .find_object(ops_ref.object_id())
            .expect("The object with this id should exist.")
            .try_into_blob()
            .expect("The git-bug's data model enforces this.")
            .data
            .clone();

        let operations = serde_json::from_str(
            issue_data
                .to_str()
                .expect("git-bug's ensures, that this is valid json."),
        )
        .expect("The returned json should be valid");

        (operations, id.id())
    }

    fn try_get_parent<'a>(repo: &'a Repository, base_commit: &Commit<'a>) -> Option<Commit<'a>> {
        let count = base_commit.parent_ids().count();

        match count {
            0 => None,
            1 => {
                let parent = base_commit.parent_ids().last().expect("One does exist");

                let parent_id = parent.object().expect("The object exists").id;
                Some(
                    repo.find_object(parent_id)
                        .expect("This is a valid id")
                        .try_into_commit()
                        .expect("This should be a commit"),
                )
            }
            other => {
                unreachable!(
                    "Each commit, used by git-bug should only have one parent, but found: {other}"
                );
            }
        }
    }
}

/// Check whether `git-bug` has been initialized in this repository
pub fn is_git_bug(repo: &Repository) -> error::Result<bool> {
    Ok(repo
        .refs
        .iter()?
        .prefixed(Path::new("refs/bugs/"))
        .map_err(|err| error::Error::RepoRefsPrefixed { error: err })?
        .count()
        > 0)
}

pub fn issues_from_repository(repo: &Repository) -> error::Result<Vec<Dag>> {
    let dags = repo
        .refs
        .iter()?
        .prefixed(Path::new("refs/bugs/"))
        .map_err(|err| error::Error::RepoRefsPrefixed { error: err })?
        .map(|val| {
            let reference = val.expect("All `git-bug` references in 'refs/bugs' should be objects");

            if let Target::Object(id) = reference.target {
                Dag::construct(repo, id)
            } else {
                unreachable!("All 'refs/bugs/' should contain a clear target.");
            }
        })
        .collect::<Vec<Dag>>();

    Ok(dags)
}