use std::{
    cmp::Reverse,
    collections::{BTreeSet, BinaryHeap, HashMap},
};

use gotcha_gitea::{Client, apis, models};
use tokio::task::JoinSet;

use crate::{
    diff,
    domain::{
        HistoryCommit, HomeData, IssueDetails, MilestoneDetails, PullDetails, RepositoryData,
        Server,
    },
    presentation::compact_date,
};

pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let user = client
        .current_user()
        .await
        .map_err(|error| error.to_string())?;
    let login = user.login.unwrap_or_default();
    let repositories = apis::user_api::user_current_list_repos(&configuration, Some(1), Some(100))
        .await
        .map_err(|error| error.to_string())?;
    Ok(repositories
        .into_iter()
        .filter(|repository| {
            repository
                .owner
                .as_ref()
                .and_then(|owner| owner.login.as_deref())
                == Some(login.as_str())
        })
        .filter_map(|repository| {
            Some(RepositoryData {
                owner: repository.owner?.login?,
                name: repository.name?,
                description: repository
                    .description
                    .filter(|text| !text.is_empty())
                    .unwrap_or_else(|| "No description".into()),
                language: repository
                    .language
                    .filter(|text| !text.is_empty())
                    .unwrap_or_else(|| "Unknown language".into()),
                open_issues: repository.open_issues_count.unwrap_or_default(),
                updated: compact_date(repository.updated_at.as_deref()),
                default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
            })
        })
        .collect())
}

pub async fn load_issues(
    server: &Server,
    owner: &str,
    repository: &str,
    status: &str,
) -> Result<Vec<models::Issue>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    apis::issue_api::issue_list_issues(
        &client.configuration(),
        owner,
        repository,
        Some(status),
        None,
        None,
        Some("issues"),
        None,
        None,
        None,
        None,
        None,
        None,
        Some(1),
        Some(100),
    )
    .await
    .map_err(|error| error.to_string())
}

pub async fn load_milestones(
    server: &Server,
    owner: &str,
    repository: &str,
) -> Result<Vec<models::Milestone>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let mut milestones = Vec::new();
    for page in 1.. {
        let batch = apis::issue_api::issue_get_milestones_list(
            &configuration,
            owner,
            repository,
            Some("all"),
            None,
            Some(page),
            Some(100),
        )
        .await
        .map_err(|error| error.to_string())?;
        let done = batch.len() < 100;
        milestones.extend(batch);
        if done {
            break;
        }
    }
    Ok(milestones)
}

pub async fn load_milestone(
    server: &Server,
    owner: &str,
    repository: &str,
    id: i64,
) -> Result<MilestoneDetails, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let milestone =
        apis::issue_api::issue_get_milestone(&configuration, owner, repository, &id.to_string())
            .await
            .map_err(|error| error.to_string())?;
    let issues = apis::issue_api::issue_list_issues(
        &configuration,
        owner,
        repository,
        Some("all"),
        None,
        None,
        Some("issues"),
        milestone.title.as_deref(),
        None,
        None,
        None,
        None,
        None,
        Some(1),
        Some(100),
    )
    .await
    .map_err(|error| error.to_string())?;
    Ok(MilestoneDetails { milestone, issues })
}

pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let owner = client
        .current_user()
        .await
        .map_err(|error| error.to_string())?
        .login
        .ok_or("The server account has no username.")?;
    let mut pulls = Vec::new();
    for page in 1.. {
        let batch = apis::issue_api::issue_search_issues(
            &configuration,
            Some(status),
            None,
            None,
            None,
            None,
            Some("pulls"),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(&owner),
            None,
            Some(page),
            Some(100),
        )
        .await
        .map_err(|error| error.to_string())?;
        if batch.is_empty() {
            break;
        }
        pulls.extend(batch);
    }
    Ok(pulls)
}

pub async fn load_branches(
    server: &Server,
    owner: &str,
    repository: &str,
    default_branch: &str,
) -> Result<Vec<String>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let mut branches = Vec::new();
    for page in 1.. {
        let batch = apis::repository_api::repo_list_branches(
            &configuration,
            owner,
            repository,
            Some(page),
            Some(100),
        )
        .await
        .map_err(|error| error.to_string())?;
        let done = batch.len() < 100;
        branches.extend(batch.into_iter().filter_map(|branch| branch.name));
        if done {
            break;
        }
    }
    branches.sort_by_key(|branch| (branch != default_branch, branch.to_lowercase()));
    Ok(branches)
}

pub async fn load_repository_contents(
    server: &Server,
    owner: &str,
    repository: &str,
    path: &str,
) -> Result<Vec<models::ContentsResponse>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    if path.is_empty() {
        apis::repository_api::repo_get_contents_list(&configuration, owner, repository, None)
            .await
            .map_err(|error| error.to_string())
    } else {
        apis::repository_api::repo_get_contents_ext(
            &configuration,
            owner,
            repository,
            path,
            None,
            None,
        )
        .await
        .map(|contents| contents.dir_contents.unwrap_or_default())
        .map_err(|error| error.to_string())
    }
}

pub async fn load_repository_file(
    server: &Server,
    owner: &str,
    repository: &str,
    path: &str,
) -> Result<Vec<u8>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    apis::repository_api::repo_get_raw_file(&client.configuration(), owner, repository, path, None)
        .await
        .map_err(|error| error.to_string())?
        .bytes()
        .await
        .map(|bytes| bytes.to_vec())
        .map_err(|error| error.to_string())
}

pub async fn load_branch_commits(
    server: &Server,
    owner: &str,
    repository: &str,
    branch: &str,
) -> Result<Vec<HistoryCommit>, String> {
    load_commits_for_ref(server, owner, repository, Some(branch))
        .await
        .map(|commits| {
            commits
                .into_iter()
                .map(|commit| HistoryCommit {
                    commit,
                    top_lanes: Vec::new(),
                    bottom_lanes: Vec::new(),
                    node_lane: None,
                    connections: Vec::new(),
                    refs: Vec::new(),
                })
                .collect()
        })
}

pub async fn load_all_commits(
    server: &Server,
    owner: &str,
    repository: &str,
    branches: &[String],
) -> Result<Vec<HistoryCommit>, String> {
    let mut tasks = JoinSet::new();
    for (lane, branch) in branches.iter().cloned().enumerate() {
        let (server, owner, repository) =
            (server.clone(), owner.to_string(), repository.to_string());
        tasks.spawn(async move {
            let commits = load_commits_for_ref(&server, &owner, &repository, Some(&branch)).await?;
            Ok::<_, String>((lane, branch, commits))
        });
    }

    let mut histories = Vec::new();
    while let Some(result) = tasks.join_next().await {
        histories.push(result.map_err(|error| error.to_string())??);
    }
    histories.sort_by_key(|(lane, _, _)| *lane);

    let pull_refs = load_pull_refs(server, owner, repository)
        .await
        .unwrap_or_default();
    Ok(build_graph(histories, pull_refs))
}

pub async fn load_pull_files(
    configuration: &apis::configuration::Configuration,
    owner: &str,
    repository: &str,
    number: i64,
) -> Result<Vec<models::ChangedFile>, String> {
    let mut files = Vec::new();
    for page in 1.. {
        let batch = apis::repository_api::repo_get_pull_request_files(
            configuration,
            owner,
            repository,
            number,
            None,
            None,
            Some(page),
            Some(100),
        )
        .await
        .map_err(|error| error.to_string())?;
        let done = batch.len() < 100;
        files.extend(batch);
        if done {
            break;
        }
    }
    Ok(files)
}

pub async fn load_issue(
    server: &Server,
    owner: &str,
    repository: &str,
    number: i64,
) -> Result<IssueDetails, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let (issue, comments) = tokio::join!(
        apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
        apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
    );
    Ok(IssueDetails {
        issue: issue.map_err(|error| error.to_string())?,
        comments: comments.map_err(|error| error.to_string())?,
    })
}

pub async fn load_pull(
    server: &Server,
    owner: &str,
    repository: &str,
    number: i64,
) -> Result<PullDetails, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let (pull, comments, files) = tokio::join!(
        apis::repository_api::repo_get_pull_request(&configuration, owner, repository, number,),
        apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,),
        load_pull_files(&configuration, owner, repository, number),
    );
    Ok(PullDetails {
        pull: pull.map_err(|error| error.to_string())?,
        comments: comments.map_err(|error| error.to_string())?,
        files: files.map_err(|error| error.to_string())?,
    })
}

pub async fn load_commit_files(
    server: &Server,
    owner: &str,
    repository: &str,
    sha: &str,
) -> Result<Vec<models::CommitAffectedFiles>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    apis::repository_api::repo_get_single_commit(
        &client.configuration(),
        owner,
        repository,
        sha,
        Some(true),
        None,
        Some(true),
    )
    .await
    .map(|commit| commit.files.unwrap_or_default())
    .map_err(|error| error.to_string())
}

pub async fn load_commit_diff(
    server: &Server,
    owner: &str,
    repository: &str,
    sha: &str,
    path: &str,
) -> Result<diff::Parsed, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    apis::repository_api::repo_download_commit_diff_or_patch(
        &client.configuration(),
        owner,
        repository,
        sha,
        "diff",
    )
    .await
    .map(|text| diff::parse_file(&text, path))
    .map_err(|error| error.to_string())
}

pub async fn load_pull_diff(
    server: &Server,
    owner: &str,
    repository: &str,
    number: i64,
    path: &str,
) -> Result<diff::Parsed, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    apis::repository_api::repo_download_pull_diff_or_patch(
        &client.configuration(),
        owner,
        repository,
        number,
        "diff",
        Some(false),
    )
    .await
    .map(|text| diff::parse_file(&text, path))
    .map_err(|error| error.to_string())
}

async fn load_pull_refs(
    server: &Server,
    owner: &str,
    repository: &str,
) -> Result<HashMap<String, Vec<String>>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let mut refs: HashMap<String, Vec<String>> = HashMap::new();
    for page in 1.. {
        let batch = apis::repository_api::repo_list_pull_requests(
            &configuration,
            owner,
            repository,
            None,
            Some("all"),
            None,
            None,
            None,
            None,
            Some(page),
            Some(100),
        )
        .await
        .map_err(|error| error.to_string())?;
        let done = batch.len() < 100;
        for pull in batch {
            let Some(head) = pull.head else { continue };
            let Some(sha) = head.sha else { continue };
            let Some(label) = head.label.or(head.r#ref) else {
                continue;
            };
            if !label.is_empty() {
                refs.entry(sha).or_default().push(label);
            }
        }
        if done {
            break;
        }
    }
    Ok(refs)
}

fn build_graph(
    histories: Vec<(usize, String, Vec<models::Commit>)>,
    mut extra_refs: HashMap<String, Vec<String>>,
) -> Vec<HistoryCommit> {
    let mut commits = HashMap::new();
    let mut ranks: HashMap<String, (usize, usize)> = HashMap::new();
    let mut refs: HashMap<String, Vec<String>> = HashMap::new();
    for (branch_index, branch, history) in histories {
        if let Some(sha) = history.iter().find_map(|commit| commit.sha.clone()) {
            refs.entry(sha).or_default().push(branch);
        }
        for (index, commit) in history.into_iter().enumerate() {
            let Some(sha) = commit.sha.clone() else {
                continue;
            };
            ranks
                .entry(sha.clone())
                .and_modify(|rank| *rank = (*rank).min((branch_index, index)))
                .or_insert((branch_index, index));
            commits.entry(sha).or_insert(commit);
        }
    }
    for (sha, labels) in extra_refs.drain() {
        let row = refs.entry(sha).or_default();
        for label in labels {
            if !row.contains(&label) {
                row.push(label);
            }
        }
    }

    let mut children = HashMap::new();
    for sha in commits.keys() {
        children.insert(sha.clone(), 0_usize);
    }
    for commit in commits.values() {
        for parent in commit_parents(commit) {
            if let Some(count) = children.get_mut(parent) {
                *count += 1;
            }
        }
    }
    let mut ready = BinaryHeap::new();
    for (sha, count) in &children {
        if *count == 0 {
            ready.push(Reverse((ranks[sha], sha.clone())));
        }
    }
    let mut ordered = Vec::with_capacity(commits.len());
    while let Some(Reverse((_, sha))) = ready.pop() {
        let Some(commit) = commits.remove(&sha) else {
            continue;
        };
        for parent in commit_parents(&commit) {
            if let Some(count) = children.get_mut(parent) {
                *count -= 1;
                if *count == 0 {
                    ready.push(Reverse((ranks[parent], parent.to_string())));
                }
            }
        }
        ordered.push(HistoryCommit {
            commit,
            top_lanes: Vec::new(),
            bottom_lanes: Vec::new(),
            node_lane: None,
            connections: Vec::new(),
            refs: refs.remove(&sha).unwrap_or_default(),
        });
    }
    layout_graph(&mut ordered);
    ordered
}

fn layout_graph(commits: &mut [HistoryCommit]) {
    let mut lanes: Vec<Option<String>> = Vec::new();
    for row in commits {
        let Some(sha) = row.commit.sha.as_deref() else {
            continue;
        };
        let matching_lanes: Vec<_> = lanes
            .iter()
            .enumerate()
            .filter_map(|(index, next)| (next.as_deref() == Some(sha)).then_some(index))
            .collect();
        let existing_node = matching_lanes.first().copied();
        let node = existing_node.unwrap_or_else(|| allocate_lane(&mut lanes, sha));
        row.top_lanes = lanes
            .iter()
            .enumerate()
            .filter_map(|(index, lane)| lane.as_ref().map(|_| index))
            .filter(|index| existing_node.is_some() || *index != node)
            .collect();
        let mut connections = BTreeSet::new();
        for duplicate in matching_lanes.into_iter().skip(1) {
            lanes[duplicate] = None;
            connect(node, duplicate, &mut connections);
        }
        let parents: Vec<_> = commit_parents(&row.commit).map(str::to_string).collect();
        lanes[node] = None;
        for (index, parent) in parents.into_iter().enumerate() {
            if index == 0 {
                lanes[node] = Some(parent);
            } else if let Some(existing) = lanes
                .iter()
                .position(|next| next.as_deref() == Some(&parent))
            {
                connect(node, existing, &mut connections);
            } else {
                let branch = allocate_lane(&mut lanes, &parent);
                connect(node, branch, &mut connections);
            }
        }
        row.node_lane = Some(node);
        row.connections = connections.into_iter().collect();
        row.bottom_lanes = lanes
            .iter()
            .enumerate()
            .filter_map(|(index, lane)| lane.as_ref().map(|_| index))
            .collect();
    }
}

fn allocate_lane(lanes: &mut Vec<Option<String>>, sha: &str) -> usize {
    if let Some(index) = lanes.iter().position(Option::is_none) {
        lanes[index] = Some(sha.into());
        index
    } else {
        lanes.push(Some(sha.into()));
        lanes.len() - 1
    }
}

fn connect(left: usize, right: usize, connections: &mut BTreeSet<usize>) {
    for lane in left.min(right) + 1..=left.max(right) {
        connections.insert(lane);
    }
}

fn commit_parents(commit: &models::Commit) -> impl Iterator<Item = &str> {
    commit
        .parents
        .as_deref()
        .unwrap_or_default()
        .iter()
        .filter_map(|parent| parent.sha.as_deref())
}

async fn load_commits_for_ref(
    server: &Server,
    owner: &str,
    repository: &str,
    branch: Option<&str>,
) -> Result<Vec<models::Commit>, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let mut commits = Vec::new();
    for page in 1.. {
        let batch = apis::repository_api::repo_get_all_commits(
            &configuration,
            owner,
            repository,
            branch,
            None,
            None,
            None,
            None,
            None,
            Some(false),
            Some(page),
            Some(100),
            None,
        )
        .await
        .map_err(|error| error.to_string())?;
        let done = batch.len() < 100;
        commits.extend(batch);
        if done {
            break;
        }
    }
    Ok(commits)
}

pub async fn load_home(server: &Server) -> Result<HomeData, String> {
    let client =
        Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
    let configuration = client.configuration();
    let login = client
        .current_user()
        .await
        .map_err(|error| error.to_string())?
        .login
        .ok_or("The server account has no username.")?;
    let (activities, heatmap) = tokio::join!(
        apis::user_api::user_list_activity_feeds(
            &configuration,
            &login,
            Some(true),
            None,
            Some(1),
            Some(40),
        ),
        apis::user_api::user_get_heatmap_data(&configuration, &login),
    );
    Ok(HomeData {
        activities: activities.map_err(|error| error.to_string())?,
        heatmap: heatmap.map_err(|error| error.to_string())?,
    })
}

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

    fn commit(sha: &str, parents: &[&str]) -> models::Commit {
        models::Commit {
            sha: Some(sha.into()),
            parents: Some(
                parents
                    .iter()
                    .map(|sha| models::CommitMeta {
                        sha: Some((*sha).into()),
                        ..Default::default()
                    })
                    .collect(),
            ),
            ..Default::default()
        }
    }

    #[test]
    fn lays_out_merged_pull_request_on_its_parent_lane() {
        let commits = build_graph(
            vec![(
                0,
                "main".into(),
                vec![
                    commit("tip", &["merge"]),
                    commit("merge", &["main-before", "feature"]),
                    commit("feature", &["base"]),
                    commit("main-before", &["base"]),
                    commit("base", &[]),
                ],
            )],
            HashMap::from([("feature".into(), vec!["linux-refactoring-base".into()])]),
        );

        assert_eq!(
            commits
                .iter()
                .map(|commit| commit.commit.sha.as_deref().unwrap())
                .collect::<Vec<_>>(),
            ["tip", "merge", "feature", "main-before", "base"]
        );
        assert_eq!(commits[0].refs, ["main"]);
        assert_eq!(commits[1].connections, [1]);
        assert_eq!(commits[2].refs, ["linux-refactoring-base"]);
        assert_eq!(commits[2].node_lane, Some(1));
        assert_eq!(commits[1].top_lanes, [0]);
        assert_eq!(commits[1].bottom_lanes, [0, 1]);
        assert_eq!(commits[2].top_lanes, [0, 1]);
        assert_eq!(commits[2].bottom_lanes, [0, 1]);
        assert!(commits[3].connections.is_empty());
        assert_eq!(commits[3].bottom_lanes, [0, 1]);
        assert_eq!(commits[4].top_lanes, [0, 1]);
        assert_eq!(commits[4].connections, [1]);
        assert!(commits[4].bottom_lanes.is_empty());
        assert_eq!(commits[4].node_lane, Some(0));
    }
}
