use gotcha_gitea::models;

#[derive(Debug, Eq, PartialEq)]
pub enum Target {
    Repository {
        owner: String,
        repository: String,
    },
    Commit {
        owner: String,
        repository: String,
        sha: String,
    },
    Issue {
        owner: String,
        repository: String,
        number: i64,
    },
    Pull {
        owner: String,
        repository: String,
        number: i64,
    },
}

pub fn target(activity: &models::Activity) -> Option<Target> {
    use models::activity::OpType;

    let (owner, repository) = activity
        .repo
        .as_ref()?
        .full_name
        .as_deref()?
        .split_once('/')?;
    let pair = || (owner.to_string(), repository.to_string());
    match activity.op_type? {
        OpType::CreateRepo => {
            let (owner, repository) = pair();
            Some(Target::Repository { owner, repository })
        }
        OpType::CommitRepo | OpType::MirrorSyncPush => {
            let (owner, repository) = pair();
            Some(Target::Commit {
                owner,
                repository,
                sha: commit_sha(activity.content.as_deref()?)?,
            })
        }
        OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue => {
            let (owner, repository) = pair();
            Some(Target::Issue {
                owner,
                repository,
                number: issue_number(activity)?,
            })
        }
        OpType::CreatePullRequest
        | OpType::MergePullRequest
        | OpType::AutoMergePullRequest
        | OpType::ClosePullRequest
        | OpType::ReopenPullRequest
        | OpType::CommentPull
        | OpType::ApprovePullRequest
        | OpType::RejectPullRequest
        | OpType::PullReviewDismissed
        | OpType::PullRequestReadyForReview => {
            let (owner, repository) = pair();
            Some(Target::Pull {
                owner,
                repository,
                number: issue_number(activity)?,
            })
        }
        _ => None,
    }
}

fn issue_number(activity: &models::Activity) -> Option<i64> {
    activity
        .comment
        .as_ref()
        .and_then(|comment| {
            comment
                .pull_request_url
                .as_deref()
                .or(comment.issue_url.as_deref())
        })
        .and_then(url_number)
        .or_else(|| {
            activity
                .content
                .as_deref()?
                .split(|character: char| !character.is_ascii_digit())
                .find(|part| !part.is_empty())?
                .parse()
                .ok()
        })
}

fn url_number(url: &str) -> Option<i64> {
    url.trim_end_matches('/').rsplit('/').next()?.parse().ok()
}

fn commit_sha(content: &str) -> Option<String> {
    let payload: serde_json::Value = serde_json::from_str(content).ok()?;
    payload.get("HeadCommit").and_then(find_sha).or_else(|| {
        payload
            .get("Commits")?
            .as_array()?
            .last()
            .and_then(find_sha)
    })
}

fn find_sha(value: &serde_json::Value) -> Option<String> {
    value.as_object()?.iter().find_map(|(key, value)| {
        (key.to_ascii_lowercase().contains("sha"))
            .then(|| value.as_str())
            .flatten()
            .filter(|sha| sha.len() >= 7)
            .map(str::to_string)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use models::activity::OpType;

    fn activity(op_type: OpType, content: &str) -> models::Activity {
        models::Activity {
            op_type: Some(op_type),
            content: Some(content.into()),
            repo: Some(Box::new(models::Repository {
                full_name: Some("octo/demo".into()),
                ..Default::default()
            })),
            ..Default::default()
        }
    }

    #[test]
    fn decodes_linkable_activity_targets() {
        assert_eq!(
            target(&activity(OpType::CreateRepo, "")),
            Some(Target::Repository {
                owner: "octo".into(),
                repository: "demo".into(),
            })
        );
        assert_eq!(
            target(&activity(OpType::CreatePullRequest, "42|feature")),
            Some(Target::Pull {
                owner: "octo".into(),
                repository: "demo".into(),
                number: 42,
            })
        );
        assert_eq!(
            target(&activity(
                OpType::CommitRepo,
                r#"{"HeadCommit":{"Sha1":"0123456789abcdef"}}"#,
            )),
            Some(Target::Commit {
                owner: "octo".into(),
                repository: "demo".into(),
                sha: "0123456789abcdef".into(),
            })
        );
    }
}
