use std::collections::VecDeque;

use crate::{
    Client, Error, Result,
    domain::{DEFAULT_PAGE_SIZE, HomeData, Page},
    models,
};
use gitea_openapi::apis;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ActivityFilter {
    #[default]
    All,
    Issues,
    PullRequests,
}

impl ActivityFilter {
    fn matches(self, activity: &models::Activity) -> bool {
        use models::activity::OpType;

        match self {
            Self::All => true,
            Self::Issues => matches!(
                activity.op_type,
                Some(
                    OpType::CreateIssue
                        | OpType::CloseIssue
                        | OpType::ReopenIssue
                        | OpType::CommentIssue
                )
            ),
            Self::PullRequests => matches!(
                activity.op_type,
                Some(
                    OpType::CreatePullRequest
                        | OpType::MergePullRequest
                        | OpType::AutoMergePullRequest
                        | OpType::ClosePullRequest
                        | OpType::ReopenPullRequest
                        | OpType::CommentPull
                        | OpType::ApprovePullRequest
                        | OpType::RejectPullRequest
                        | OpType::PullReviewDismissed
                        | OpType::PullRequestReadyForReview
                )
            ),
        }
    }
}

#[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,
    },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommitActivity {
    pub count: usize,
    pub commits: Vec<ActivityCommit>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ActivityCommit {
    pub sha: String,
    pub message: String,
    pub author: String,
    pub timestamp: String,
}

pub struct ServerActivityPager {
    configuration: apis::configuration::Configuration,
    feeds: Vec<UserActivityFeed>,
}

struct UserActivityFeed {
    login: String,
    activities: VecDeque<models::Activity>,
    next_page: i32,
    complete: bool,
}

impl Client {
    pub async fn activities(
        &self,
        page: i32,
        filter: ActivityFilter,
    ) -> Result<Page<models::Activity>> {
        if page < 1 {
            return Err(Error::InvalidInput("page must be positive".into()));
        }
        let configuration = self.configuration();
        let login = self
            .current_user()
            .await?
            .login
            .ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
        filtered_activity_page(&configuration, &login, page, filter).await
    }

    pub async fn home(&self, page: i32, filter: ActivityFilter) -> Result<HomeData> {
        if page < 1 {
            return Err(Error::InvalidInput("page must be positive".into()));
        }
        let configuration = self.configuration();
        let login = self
            .current_user()
            .await?
            .login
            .ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
        let (activities, heatmap) = tokio::join!(
            filtered_activity_page(&configuration, &login, page, filter),
            apis::user_api::user_get_heatmap_data(&configuration, &login),
        );
        let activities = activities?;
        Ok(HomeData {
            activities: activities.items,
            heatmap: heatmap.map_err(Error::generated)?,
            next_page: activities.has_more.then_some(page + 1),
        })
    }

    pub async fn server_activity_pager(&self) -> Result<ServerActivityPager> {
        let configuration = self.configuration();
        let users = activity_users(&configuration).await?;
        Ok(ServerActivityPager {
            configuration,
            feeds: users
                .into_iter()
                .map(|login| UserActivityFeed {
                    login,
                    activities: VecDeque::new(),
                    next_page: 1,
                    complete: false,
                })
                .collect(),
        })
    }
}

impl ServerActivityPager {
    pub async fn next_page(&mut self) -> Result<Page<models::Activity>> {
        let mut items = Vec::with_capacity(DEFAULT_PAGE_SIZE as usize);
        while items.len() < DEFAULT_PAGE_SIZE as usize {
            for feed in &mut self.feeds {
                feed.fill(&self.configuration).await?;
            }
            let Some(feed) = self.feeds.iter_mut().max_by(|left, right| {
                activity_order(left.activities.front(), right.activities.front())
            }) else {
                break;
            };
            let Some(activity) = feed.activities.pop_front() else {
                break;
            };
            items.push(activity);
        }
        let has_more = self
            .feeds
            .iter()
            .any(|feed| !feed.complete || !feed.activities.is_empty());
        Ok(Page { items, has_more })
    }
}

impl UserActivityFeed {
    async fn fill(&mut self, configuration: &apis::configuration::Configuration) -> Result<()> {
        if self.complete || !self.activities.is_empty() {
            return Ok(());
        }
        let mut activities = activity_page(configuration, &self.login, self.next_page).await?;
        self.complete = activities.len() < DEFAULT_PAGE_SIZE as usize;
        self.next_page = self
            .next_page
            .checked_add(1)
            .ok_or_else(|| Error::InvalidInput("activity feed has too many pages".into()))?;
        activities.sort_unstable_by(|left, right| activity_order(Some(right), Some(left)));
        self.activities = activities.into();
        Ok(())
    }
}

fn activity_order(
    left: Option<&models::Activity>,
    right: Option<&models::Activity>,
) -> std::cmp::Ordering {
    left.map(|activity| (&activity.created, &activity.id))
        .cmp(&right.map(|activity| (&activity.created, &activity.id)))
}

async fn activity_users(configuration: &apis::configuration::Configuration) -> Result<Vec<String>> {
    let mut users = Vec::new();
    let mut page = 1;
    loop {
        let response = apis::user_api::user_search(
            configuration,
            None,
            None,
            Some(page),
            Some(DEFAULT_PAGE_SIZE),
        )
        .await
        .map_err(Error::generated)?;
        if response.ok == Some(false) {
            return Err(Error::Generated("The server user search failed.".into()));
        }
        let batch = response.data.unwrap_or_default();
        let complete = batch.len() < DEFAULT_PAGE_SIZE as usize;
        users.extend(batch.into_iter().filter_map(|user| user.login));
        if complete {
            return Ok(users);
        }
        page = page
            .checked_add(1)
            .ok_or_else(|| Error::InvalidInput("user search has too many pages".into()))?;
    }
}

async fn filtered_activity_page(
    configuration: &apis::configuration::Configuration,
    login: &str,
    mut page: i32,
    filter: ActivityFilter,
) -> Result<Page<models::Activity>> {
    let mut activities = activity_page(configuration, login, page).await?;

    loop {
        let has_more = activities.len() == DEFAULT_PAGE_SIZE as usize;
        let filtered: Vec<_> = activities
            .into_iter()
            .filter(|activity| filter.matches(activity))
            .collect();
        if filter == ActivityFilter::All || !filtered.is_empty() || !has_more {
            return Ok(Page {
                items: filtered,
                has_more,
            });
        }
        page += 1;
        activities = activity_page(configuration, login, page).await?;
    }
}

async fn activity_page(
    configuration: &apis::configuration::Configuration,
    login: &str,
    page: i32,
) -> Result<Vec<models::Activity>> {
    apis::user_api::user_list_activity_feeds(
        configuration,
        login,
        Some(true),
        None,
        Some(page),
        Some(DEFAULT_PAGE_SIZE),
    )
    .await
    .map_err(Error::generated)
}

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,
    }
}

pub fn commit_activity(activity: &models::Activity) -> Option<CommitActivity> {
    let payload: serde_json::Value = serde_json::from_str(activity.content.as_deref()?).ok()?;
    let mut commits = field(&payload, "Commits")
        .and_then(serde_json::Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(activity_commit)
        .collect::<Vec<_>>();
    if commits.is_empty()
        && let Some(commit) = field(&payload, "HeadCommit").and_then(activity_commit)
    {
        commits.push(commit);
    }
    (!commits.is_empty()).then(|| CommitActivity {
        count: field(&payload, "Len")
            .and_then(serde_json::Value::as_u64)
            .and_then(|count| usize::try_from(count).ok())
            .unwrap_or(commits.len()),
        commits,
    })
}

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()?;
    field(&payload, "HeadCommit")
        .and_then(find_sha)
        .or_else(|| {
            field(&payload, "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)
    })
}

fn activity_commit(value: &serde_json::Value) -> Option<ActivityCommit> {
    let sha = find_sha(value).unwrap_or_default();
    let message = string_field(value, "Message")
        .unwrap_or_default()
        .to_string();
    (!sha.is_empty() || !message.is_empty()).then(|| ActivityCommit {
        sha,
        message,
        author: string_field(value, "AuthorName")
            .or_else(|| string_field(value, "CommitterName"))
            .unwrap_or_default()
            .to_string(),
        timestamp: string_field(value, "Timestamp")
            .unwrap_or_default()
            .to_string(),
    })
}

fn string_field<'a>(value: &'a serde_json::Value, name: &str) -> Option<&'a str> {
    field(value, name).and_then(serde_json::Value::as_str)
}

fn field<'a>(value: &'a serde_json::Value, name: &str) -> Option<&'a serde_json::Value> {
    value
        .as_object()?
        .iter()
        .find_map(|(key, value)| key.eq_ignore_ascii_case(name).then_some(value))
}

#[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(),
            })
        );
    }

    #[test]
    fn decodes_forgejo_commit_activity_for_presentation() {
        let activity = activity(
            OpType::CommitRepo,
            r#"{
                "Commits": [{
                    "Sha1": "0123456789abcdef",
                    "Message": "Fix the preview\n\nDetails",
                    "AuthorName": "Octo Cat",
                    "Timestamp": "2026-08-04T19:04:30+02:00"
                }],
                "HeadCommit": {"Sha1": "0123456789abcdef"},
                "Len": 1
            }"#,
        );

        assert_eq!(
            commit_activity(&activity),
            Some(CommitActivity {
                count: 1,
                commits: vec![ActivityCommit {
                    sha: "0123456789abcdef".into(),
                    message: "Fix the preview\n\nDetails".into(),
                    author: "Octo Cat".into(),
                    timestamp: "2026-08-04T19:04:30+02:00".into(),
                }],
            })
        );
    }

    #[test]
    fn filters_issue_and_pull_request_activity() {
        let activities = [
            activity(OpType::CreateIssue, ""),
            activity(OpType::CreatePullRequest, ""),
            activity(OpType::PullRequestReadyForReview, ""),
            activity(OpType::CreateRepo, ""),
        ];

        assert_eq!(
            activities
                .iter()
                .filter(|activity| ActivityFilter::Issues.matches(activity))
                .count(),
            1
        );
        assert_eq!(
            activities
                .iter()
                .filter(|activity| ActivityFilter::PullRequests.matches(activity))
                .count(),
            2
        );
    }

    #[tokio::test]
    async fn merges_server_activity_incrementally_in_global_pages() {
        let mut feeds = [VecDeque::new(), VecDeque::new()];
        for id in 1..=65 {
            feeds[id as usize % 2].push_front(models::Activity {
                id: Some(id),
                created: Some(format!("2026-08-15T12:{:02}:{:02}Z", id / 60, id % 60)),
                ..Default::default()
            });
        }
        let mut pager = ServerActivityPager {
            configuration: Default::default(),
            feeds: feeds
                .into_iter()
                .enumerate()
                .map(|(index, activities)| UserActivityFeed {
                    login: format!("user-{index}"),
                    activities,
                    next_page: 1,
                    complete: true,
                })
                .collect(),
        };

        let first = pager.next_page().await.unwrap();
        let second = pager.next_page().await.unwrap();
        let third = pager.next_page().await.unwrap();

        assert_eq!(first.items.first().and_then(|row| row.id), Some(65));
        assert_eq!(first.items.last().and_then(|row| row.id), Some(36));
        assert!(first.has_more);
        assert_eq!(second.items.first().and_then(|row| row.id), Some(35));
        assert_eq!(second.items.last().and_then(|row| row.id), Some(6));
        assert!(second.has_more);
        assert_eq!(third.items.len(), 5);
        assert!(!third.has_more);
    }
}
