use crate::{
    Client, Error, Result,
    domain::{
        CreateIssue, EditIssue, IssueDetails, IssueDraft, IssueEditorData, IssueQuery, Page,
        RepositoryId,
    },
    models, positive,
};
use gitea_openapi::apis;

impl Client {
    pub async fn issues(
        &self,
        repository: &RepositoryId,
        query: &IssueQuery,
    ) -> Result<Page<models::Issue>> {
        if !matches!(query.state.as_str(), "open" | "closed" | "all") {
            return Err(Error::InvalidInput(
                "issue state must be open, closed, or all".into(),
            ));
        }
        if !matches!(query.kind.as_str(), "issues" | "pulls" | "all") {
            return Err(Error::InvalidInput(
                "issue kind must be issues, pulls, or all".into(),
            ));
        }
        if query.page < 1 || query.limit < 1 {
            return Err(Error::InvalidInput(
                "issue page and limit must be positive".into(),
            ));
        }
        apis::issue_api::issue_list_issues(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            Some(&query.state),
            query.labels.as_deref(),
            query.keyword.as_deref(),
            Some(&query.kind),
            query.milestones.as_deref(),
            query.from.clone(),
            query.until.clone(),
            query.author.as_deref(),
            query.assignee.as_deref(),
            query.mentions.as_deref(),
            Some(query.page),
            Some(query.limit),
        )
        .await
        .map(|items| Page::from_items(items, query.limit))
        .map_err(Error::generated)
    }

    pub async fn issue(&self, repository: &RepositoryId, number: i64) -> Result<models::Issue> {
        positive(number, "issue number")?;
        apis::issue_api::issue_get_issue(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn issue_details(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<IssueDetails> {
        let (issue, comments, viewer) = tokio::join!(
            self.issue(repository, number),
            self.issue_comments(repository, number),
            self.current_user(),
        );
        Ok(IssueDetails {
            issue: issue?,
            comments: comments?,
            viewer_id: viewer?.id,
            has_more: false,
        })
    }

    pub async fn issue_editor(
        &self,
        repository: &RepositoryId,
        number: Option<i64>,
    ) -> Result<IssueEditorData> {
        let issue = async {
            match number {
                Some(number) => self.issue(repository, number).await.map(Some),
                None => Ok(None),
            }
        };
        let (issue, labels, milestones) =
            tokio::try_join!(issue, self.labels(repository), self.milestones(repository),)?;
        Ok(IssueEditorData {
            issue,
            labels,
            milestones,
        })
    }

    pub async fn labels(&self, repository: &RepositoryId) -> Result<Vec<models::Label>> {
        let configuration = self.configuration();
        let mut labels = Vec::new();
        for page in 1.. {
            let batch = apis::issue_api::issue_list_labels(
                &configuration,
                &repository.owner,
                &repository.repository,
                Some(page),
                Some(100),
            )
            .await
            .map_err(Error::generated)?;
            let done = batch.len() < 100;
            labels.extend(batch);
            if done {
                break;
            }
        }
        Ok(labels)
    }

    pub async fn resolve_label_ids(
        &self,
        repository: &RepositoryId,
        names: &[String],
    ) -> Result<Vec<i64>> {
        if names.is_empty() {
            return Ok(Vec::new());
        }
        let labels = self.labels(repository).await?;
        names
            .iter()
            .map(|name| {
                labels
                    .iter()
                    .find(|label| label.name.as_deref() == Some(name))
                    .and_then(|label| label.id)
                    .ok_or_else(|| Error::InvalidInput(format!("unknown label {name:?}")))
            })
            .collect()
    }

    pub async fn create_issue(
        &self,
        repository: &RepositoryId,
        mut input: CreateIssue,
    ) -> Result<models::Issue> {
        if input.option.title.trim().is_empty() {
            return Err(Error::InvalidInput("issue title must not be empty".into()));
        }
        if input.option.milestone.is_some() && input.milestone_name.is_some() {
            return Err(Error::InvalidInput(
                "use either milestone or milestone_name, not both".into(),
            ));
        }
        if let Some(name) = input.milestone_name.as_deref() {
            input.option.milestone = Some(self.resolve_milestone_id(repository, name).await?);
        }
        if !input.label_names.is_empty() {
            input.option.labels.get_or_insert_default().extend(
                self.resolve_label_ids(repository, &input.label_names)
                    .await?,
            );
        }
        apis::issue_api::issue_create_issue(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            Some(input.option),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn create_issue_draft(
        &self,
        repository: &RepositoryId,
        draft: IssueDraft,
    ) -> Result<models::Issue> {
        self.create_issue(
            repository,
            CreateIssue {
                option: create_issue_option(draft),
                label_names: Vec::new(),
                milestone_name: None,
            },
        )
        .await
    }

    pub async fn edit_issues(
        &self,
        repository: &RepositoryId,
        numbers: &[i64],
        mut input: EditIssue,
    ) -> Result<Vec<models::Issue>> {
        if numbers.is_empty() {
            return Err(Error::InvalidInput(
                "at least one issue number is required".into(),
            ));
        }
        if input.option.milestone.is_some() && input.milestone_name.is_some() {
            return Err(Error::InvalidInput(
                "use either milestone or milestone_name, not both".into(),
            ));
        }
        if input.option.assignees.is_some() && !input.add_assignees.is_empty() {
            return Err(Error::InvalidInput(
                "use either assignees or add_assignees, not both".into(),
            ));
        }
        if input.replace_labels.is_some()
            && (!input.add_labels.is_empty() || !input.remove_labels.is_empty())
        {
            return Err(Error::InvalidInput(
                "use either replace_labels or add_labels/remove_labels, not both".into(),
            ));
        }
        if let Some(name) = input.milestone_name.as_deref() {
            input.option.milestone = Some(self.resolve_milestone_id(repository, name).await?);
        }
        let remove = self
            .resolve_label_ids(repository, &input.remove_labels)
            .await?;
        let add = self
            .resolve_label_ids(repository, &input.add_labels)
            .await?;
        let mut issues = Vec::with_capacity(numbers.len());
        for &number in numbers {
            positive(number, "issue number")?;
            issues.push(
                self.apply_issue_edit(repository, number, &input, &remove, &add)
                    .await?,
            );
        }
        Ok(issues)
    }

    async fn apply_issue_edit(
        &self,
        repository: &RepositoryId,
        number: i64,
        input: &EditIssue,
        remove: &[i64],
        add: &[i64],
    ) -> Result<models::Issue> {
        let configuration = self.configuration();
        let mut edit = input.option.clone();
        if !input.add_assignees.is_empty() {
            let mut assignees = self
                .issue(repository, number)
                .await?
                .assignees
                .as_deref()
                .unwrap_or_default()
                .iter()
                .filter_map(|user| user.login.clone())
                .collect::<Vec<_>>();
            for assignee in &input.add_assignees {
                if !assignees.contains(assignee) {
                    assignees.push(assignee.clone());
                }
            }
            edit.assignees = Some(assignees);
        }
        if edit != models::EditIssueOption::default() {
            apis::issue_api::issue_edit_issue(
                &configuration,
                &repository.owner,
                &repository.repository,
                number,
                Some(edit),
            )
            .await
            .map_err(Error::generated)?;
        }
        if let Some(labels) = &input.replace_labels {
            apis::issue_api::issue_replace_labels(
                &configuration,
                &repository.owner,
                &repository.repository,
                number,
                Some(label_option(labels)),
            )
            .await
            .map_err(Error::generated)?;
        } else {
            for &id in remove {
                apis::issue_api::issue_remove_label(
                    &configuration,
                    &repository.owner,
                    &repository.repository,
                    number,
                    id,
                )
                .await
                .map_err(Error::generated)?;
            }
            if !add.is_empty() {
                apis::issue_api::issue_add_label(
                    &configuration,
                    &repository.owner,
                    &repository.repository,
                    number,
                    Some(label_option(add)),
                )
                .await
                .map_err(Error::generated)?;
            }
        }
        self.issue(repository, number).await
    }

    pub async fn edit_issue_draft(
        &self,
        repository: &RepositoryId,
        number: i64,
        draft: IssueDraft,
    ) -> Result<models::Issue> {
        self.edit_issues(repository, &[number], edit_issue_draft(draft))
            .await
            .map(|mut issues| issues.remove(0))
    }

    pub async fn set_issue_state(
        &self,
        repository: &RepositoryId,
        numbers: &[i64],
        state: &str,
    ) -> Result<Vec<models::Issue>> {
        if !matches!(state, "open" | "closed") {
            return Err(Error::InvalidInput(
                "issue state must be open or closed".into(),
            ));
        }
        self.edit_issues(
            repository,
            numbers,
            EditIssue {
                option: models::EditIssueOption {
                    state: Some(state.into()),
                    ..Default::default()
                },
                replace_labels: None,
                add_labels: Vec::new(),
                remove_labels: Vec::new(),
                add_assignees: Vec::new(),
                milestone_name: None,
            },
        )
        .await
    }

    pub async fn delete_issue(&self, repository: &RepositoryId, number: i64) -> Result<()> {
        positive(number, "issue number")?;
        apis::issue_api::issue_delete(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn issue_comments(
        &self,
        repository: &RepositoryId,
        number: i64,
    ) -> Result<Vec<models::Comment>> {
        positive(number, "issue number")?;
        apis::issue_api::issue_get_comments(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            None,
            None,
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn create_issue_comment(
        &self,
        repository: &RepositoryId,
        number: i64,
        body: String,
    ) -> Result<models::Comment> {
        positive(number, "issue number")?;
        if body.trim().is_empty() {
            return Err(Error::InvalidInput("comment must not be empty".into()));
        }
        apis::issue_api::issue_create_comment(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            number,
            Some(models::CreateIssueCommentOption::new(body)),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn save_issue_comment(
        &self,
        repository: &RepositoryId,
        number: i64,
        comment_id: Option<i64>,
        body: String,
    ) -> Result<models::Comment> {
        let Some(id) = comment_id else {
            return self.create_issue_comment(repository, number, body).await;
        };
        positive(id, "comment id")?;
        if body.trim().is_empty() {
            return Err(Error::InvalidInput("comment must not be empty".into()));
        }
        let configuration = self.configuration();
        let (comment, viewer) = tokio::join!(
            apis::issue_api::issue_get_comment(
                &configuration,
                &repository.owner,
                &repository.repository,
                id,
            ),
            self.current_user(),
        );
        let comment = comment.map_err(Error::generated)?;
        let viewer = viewer?;
        if !comment_can_edit(&comment, viewer.id) || !comment_belongs_to_issue(&comment, number) {
            return Err(Error::Forbidden(
                "You can only edit your own comments.".into(),
            ));
        }
        apis::issue_api::issue_edit_comment(
            &configuration,
            &repository.owner,
            &repository.repository,
            id,
            Some(models::EditIssueCommentOption::new(body)),
        )
        .await
        .map_err(Error::generated)
    }
}

pub fn comment_can_edit(comment: &models::Comment, viewer_id: Option<i64>) -> bool {
    matches!(
        (
            comment.id,
            comment.user.as_ref().and_then(|user| user.id),
            viewer_id,
        ),
        (Some(_), Some(author), Some(viewer)) if author == viewer
    )
}

mod helpers;
use helpers::*;

#[cfg(test)]
mod tests;
