use serde::Serialize;

use crate::{
    Client, Error, Method, Result,
    domain::{DEFAULT_PAGE_SIZE, IssueQuery, MilestoneDetails, MilestoneDraft, Page, RepositoryId},
    models,
};
use gitea_openapi::apis;

impl Client {
    pub async fn milestones_page(
        &self,
        repository: &RepositoryId,
        page: i32,
        limit: i32,
    ) -> Result<Page<models::Milestone>> {
        if page < 1 || limit < 1 {
            return Err(Error::InvalidInput(
                "milestone page and limit must be positive".into(),
            ));
        }
        apis::issue_api::issue_get_milestones_list(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            Some("all"),
            None,
            Some(page),
            Some(limit),
        )
        .await
        .map(|items| Page::from_items(items, limit))
        .map_err(Error::generated)
    }

    pub async fn milestones(&self, repository: &RepositoryId) -> Result<Vec<models::Milestone>> {
        let mut milestones = Vec::new();
        for page in 1.. {
            let batch = self.milestones_page(repository, page, 100).await?;
            milestones.extend(batch.items);
            if !batch.has_more {
                break;
            }
        }
        Ok(milestones)
    }

    pub async fn resolve_milestone_id(&self, repository: &RepositoryId, name: &str) -> Result<i64> {
        self.milestones(repository)
            .await?
            .into_iter()
            .find(|milestone| milestone.title.as_deref() == Some(name))
            .and_then(|milestone| milestone.id)
            .ok_or_else(|| Error::InvalidInput(format!("unknown milestone {name:?}")))
    }

    pub async fn milestone(&self, repository: &RepositoryId, id: i64) -> Result<models::Milestone> {
        if id < 1 {
            return Err(Error::InvalidInput(
                "milestone id must be a positive integer".into(),
            ));
        }
        apis::issue_api::issue_get_milestone(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            &id.to_string(),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn milestone_details(
        &self,
        repository: &RepositoryId,
        id: i64,
        page: i32,
    ) -> Result<MilestoneDetails> {
        let milestone = self.milestone(repository, id).await?;
        let base = IssueQuery {
            state: "all".into(),
            milestones: milestone.title.clone(),
            page,
            limit: DEFAULT_PAGE_SIZE,
            ..Default::default()
        };
        let pulls_query = IssueQuery {
            kind: "pulls".into(),
            ..base.clone()
        };
        let issues = self.issues(repository, &base);
        let pulls = self.issues(repository, &pulls_query);
        let (issues, pulls) = tokio::try_join!(issues, pulls)?;
        let has_more = issues.has_more || pulls.has_more;
        let mut pulls = pulls.items;
        for pull in &mut pulls {
            pull.repository.get_or_insert_with(|| {
                Box::new(models::RepositoryMeta {
                    name: Some(repository.repository.clone()),
                    owner: Some(repository.owner.clone()),
                    ..Default::default()
                })
            });
        }
        Ok(MilestoneDetails {
            milestone,
            has_more,
            issues: issues.items,
            pulls,
        })
    }

    pub async fn create_milestone(
        &self,
        repository: &RepositoryId,
        option: models::CreateMilestoneOption,
    ) -> Result<models::Milestone> {
        if option
            .title
            .as_deref()
            .unwrap_or_default()
            .trim()
            .is_empty()
        {
            return Err(Error::InvalidInput(
                "milestone title must not be empty".into(),
            ));
        }
        apis::issue_api::issue_create_milestone(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            Some(option),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn edit_milestone(
        &self,
        repository: &RepositoryId,
        id: &str,
        option: models::EditMilestoneOption,
    ) -> Result<models::Milestone> {
        apis::issue_api::issue_edit_milestone(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            id,
            Some(option),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn save_milestone(
        &self,
        repository: &RepositoryId,
        id: Option<i64>,
        draft: MilestoneDraft,
    ) -> Result<models::Milestone> {
        if draft.title.trim().is_empty() {
            return Err(Error::InvalidInput(
                "milestone title must not be empty".into(),
            ));
        }
        if !matches!(draft.state.as_str(), "open" | "closed") {
            return Err(Error::InvalidInput(
                "milestone state must be open or closed".into(),
            ));
        }
        let endpoint = match id {
            Some(id) if id > 0 => format!(
                "repos/{}/{}/milestones/{id}",
                apis::urlencode(&repository.owner),
                apis::urlencode(&repository.repository)
            ),
            Some(_) => {
                return Err(Error::InvalidInput(
                    "milestone id must be a positive integer".into(),
                ));
            }
            None => format!(
                "repos/{}/{}/milestones",
                apis::urlencode(&repository.owner),
                apis::urlencode(&repository.repository)
            ),
        };
        let request = self
            .request(
                if id.is_some() {
                    Method::PATCH
                } else {
                    Method::POST
                },
                &endpoint,
            )?
            .json(&MilestoneRequest {
                title: draft.title,
                description: draft.description,
                due_on: draft.due_on,
                state: draft.state,
            });
        self.execute(request)
            .await?
            .json()
            .await
            .map_err(Into::into)
    }

    pub async fn set_milestone_closed(
        &self,
        repository: &RepositoryId,
        id: i64,
        closed: bool,
    ) -> Result<models::Milestone> {
        if id < 1 {
            return Err(Error::InvalidInput(
                "milestone id must be a positive integer".into(),
            ));
        }
        self.edit_milestone(
            repository,
            &id.to_string(),
            models::EditMilestoneOption {
                state: Some(if closed { "closed" } else { "open" }.into()),
                ..Default::default()
            },
        )
        .await
    }

    pub async fn delete_milestone(&self, repository: &RepositoryId, id: &str) -> Result<()> {
        let id = id
            .parse()
            .map_err(|_| Error::InvalidInput("milestone id must be a positive integer".into()))?;
        let milestone = self.milestone(repository, id).await?;
        let title = milestone
            .title
            .ok_or_else(|| Error::Generated("The milestone has no title.".into()))?;
        if !self
            .issues(repository, &assigned_items_query(title))
            .await?
            .items
            .is_empty()
        {
            return Err(Error::InvalidInput(
                "Remove all assigned issues and pull requests before deleting this milestone."
                    .into(),
            ));
        }
        apis::issue_api::issue_delete_milestone(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            &id.to_string(),
        )
        .await
        .map_err(Error::generated)
    }
}

fn assigned_items_query(title: String) -> IssueQuery {
    IssueQuery {
        state: "all".into(),
        kind: "all".into(),
        milestones: Some(title),
        limit: 1,
        ..Default::default()
    }
}

#[derive(Serialize)]
struct MilestoneRequest {
    title: String,
    description: String,
    due_on: Option<String>,
    state: String,
}

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

    #[test]
    fn milestone_request_preserves_due_date_clear() {
        let value = serde_json::to_value(MilestoneRequest {
            title: "Release".into(),
            description: String::new(),
            due_on: None,
            state: "closed".into(),
        })
        .unwrap();
        assert!(value.get("due_on").unwrap().is_null());
        assert_eq!(value["state"], "closed");
    }

    #[test]
    fn assigned_item_check_includes_issues_and_pull_requests() {
        let query = assigned_items_query("Release".into());
        assert_eq!(query.state, "all");
        assert_eq!(query.kind, "all");
        assert_eq!(query.milestones.as_deref(), Some("Release"));
        assert_eq!(query.limit, 1);
    }
}
