use std::collections::BTreeSet;

use crate::{
    Client, Error, Result,
    domain::{DEFAULT_PAGE_SIZE, Page, PullDetails, RepositoryId},
    models, positive, validate_page,
};
use gitea_openapi::apis;

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PullFileSource {
    Branch(String),
    Commit(String),
    Request,
}

pub fn pull_state(pull: &models::PullRequest) -> &str {
    if pull.merged.unwrap_or(false) {
        "merged"
    } else if pull.draft.unwrap_or(false) {
        "draft"
    } else {
        pull.state.as_deref().unwrap_or("unknown")
    }
}

pub fn pull_file_source(pull: &models::PullRequest) -> PullFileSource {
    if pull.state.as_deref() == Some("open") {
        PullFileSource::Branch(
            pull.head
                .as_ref()
                .and_then(|head| head.r#ref.clone().or(head.label.clone()))
                .unwrap_or_else(|| "head branch".into()),
        )
    } else if let Some(sha) = pull.merge_commit_sha.as_deref() {
        PullFileSource::Commit(sha.chars().take(8).collect())
    } else {
        PullFileSource::Request
    }
}

impl Client {
    pub async fn repository_pulls(
        &self,
        repository: &RepositoryId,
        state: &str,
        page: i32,
        limit: i32,
    ) -> Result<Page<models::PullRequest>> {
        validate_page(page, limit)?;
        apis::repository_api::repo_list_pull_requests(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            None,
            Some(state),
            None,
            None,
            None,
            None,
            Some(page),
            Some(limit),
        )
        .await
        .map(|items| Page::from_items(items, limit))
        .map_err(Error::generated)
    }

    pub async fn search_pulls(
        &self,
        state: &str,
        milestone: Option<&str>,
        keyword: Option<&str>,
        page: i32,
        limit: i32,
    ) -> Result<Page<models::Issue>> {
        validate_page(page, limit)?;
        let owner = self
            .current_user()
            .await?
            .login
            .ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
        apis::issue_api::issue_search_issues(
            &self.configuration(),
            Some(state),
            None,
            milestone,
            keyword,
            None,
            Some("pulls"),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(&owner),
            None,
            Some(page),
            Some(limit),
        )
        .await
        .map(|items| Page::from_items(items, limit))
        .map_err(Error::generated)
    }

    pub async fn pull_milestones(&self) -> Result<Vec<String>> {
        let (open, closed) = tokio::try_join!(
            self.all_searched_pulls("open"),
            self.all_searched_pulls("closed"),
        )?;
        Ok(open
            .into_iter()
            .chain(closed)
            .filter_map(|pull| pull.milestone?.title)
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect())
    }

    async fn all_searched_pulls(&self, state: &str) -> Result<Vec<models::Issue>> {
        let mut pulls = Vec::new();
        for page in 1.. {
            let batch = self
                .search_pulls(state, None, None, page, DEFAULT_PAGE_SIZE)
                .await?;
            pulls.extend(batch.items);
            if !batch.has_more {
                break;
            }
        }
        Ok(pulls)
    }

    pub async fn pull(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<models::PullRequest> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_get_pull_request(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn pull_details(
        &self,
        repository: &RepositoryId,
        number: i64,
        page: i32,
    ) -> Result<PullDetails> {
        let pull = self.pull(repository, number);
        let comments = async {
            if page == 1 {
                self.issue_comments(repository, number).await
            } else {
                Ok(Vec::new())
            }
        };
        let files = self.pull_files_page(repository, number, page, DEFAULT_PAGE_SIZE);
        let (pull, comments, files) = tokio::try_join!(pull, comments, files)?;
        Ok(PullDetails {
            pull,
            comments,
            has_more: files.has_more,
            files: files.items,
        })
    }

    pub async fn create_pull(
        &self,
        repository: &RepositoryId,
        option: models::CreatePullRequestOption,
    ) -> Result<models::PullRequest> {
        if [
            option.title.as_deref(),
            option.head.as_deref(),
            option.base.as_deref(),
        ]
        .into_iter()
        .any(|value| value.unwrap_or_default().trim().is_empty())
        {
            return Err(Error::InvalidInput(
                "pull request title, head, and base must not be empty".into(),
            ));
        }
        apis::repository_api::repo_create_pull_request(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            Some(option),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn edit_pull(
        &self,
        repository: &RepositoryId,
        number: i64,
        option: models::EditPullRequestOption,
    ) -> Result<models::PullRequest> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_edit_pull_request(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            Some(option),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn merge_pull(
        &self,
        repository: &RepositoryId,
        number: i64,
        option: models::MergePullRequestOption,
    ) -> Result<()> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_merge_pull_request(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            Some(option),
        )
        .await
        .map(|_| ())
        .map_err(Error::generated)
    }

    pub async fn pull_commits(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<Vec<models::Commit>> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_get_pull_request_commits(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            None,
            None,
            None,
            None,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn pull_files_page(
        &self,
        repository: &RepositoryId,
        number: i64,
        page: i32,
        limit: i32,
    ) -> Result<Page<models::ChangedFile>> {
        positive(number, "pull request number")?;
        validate_page(page, limit)?;
        apis::repository_api::repo_get_pull_request_files(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            None,
            None,
            Some(page),
            Some(limit),
        )
        .await
        .map(|items| Page::from_items(items, limit))
        .map_err(Error::generated)
    }

    pub async fn pull_files(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<Vec<models::ChangedFile>> {
        self.pull_files_page(repository, number, 1, DEFAULT_PAGE_SIZE)
            .await
            .map(|page| page.items)
    }

    pub async fn pull_reviews(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<Vec<models::PullReview>> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_list_pull_reviews(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            None,
            None,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn pull_diff(&self, repository: &RepositoryId, number: i64) -> Result<String> {
        positive(number, "pull request number")?;
        apis::repository_api::repo_download_pull_diff_or_patch(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            "diff",
            Some(false),
        )
        .await
        .map_err(Error::generated)
    }
}
