use anyhow::Result;
use reqwest::{Client, header::HeaderMap};
use rustc_hash::FxHashMap;
use rustyline::completion::Pair;
use serde::Deserialize;

use crate::{
    cfg::AccessTokens,
    fetcher::{Fetcher, PackageInfo, Revisions, Version, json, success},
};

#[derive(Debug, Deserialize)]
pub struct FetchFromGitea {
    domain: String,
    owner: String,
    repo: String,
}

#[derive(Deserialize)]
struct Repo {
    description: String,
}

#[derive(Deserialize)]
struct Release {
    tag_name: String,
    #[serde(default)]
    body: String,
}

#[derive(Deserialize)]
struct Tag {
    name: String,
}

#[derive(Deserialize)]
struct Commit {
    sha: String,
    commit: CommitInfo,
}

#[derive(Deserialize)]
struct CommitInfo {
    committer: Committer,
    message: String,
}

#[derive(Deserialize)]
struct Committer {
    date: String,
}

impl Fetcher for FetchFromGitea {
    async fn create_client(&self, mut tokens: AccessTokens) -> Result<Client> {
        let mut headers = HeaderMap::new();
        tokens.insert_header(&mut headers, &self.domain).await;
        Client::builder()
            .default_headers(headers)
            .build()
            .map_err(Into::into)
    }

    async fn get_package_info(&self, cl: &Client) -> PackageInfo {
        let root = format!(
            "https://{}/api/v1/repos/{}/{}",
            self.domain, self.owner, self.repo,
        );

        let (description, latest_release, tags, commits) = tokio::join!(
            async {
                json(cl, &root)
                    .await
                    .map_or_else(String::new, |repo: Repo| repo.description)
            },
            async {
                json::<[Release; 1]>(cl, format!("{root}/releases?limit=1"))
                    .await
                    .map(|[release]| release)
            },
            json::<Vec<_>>(cl, format!("{root}/tags?page=1&limit=12")),
            json::<Vec<_>>(cl, format!("{root}/commits?limit=12&stat=false")),
        );

        let mut completions = vec![];
        let mut versions = FxHashMap::default();

        let releases_page = latest_release
            .as_ref()
            .filter(|r| r.body.trim().len() >= 20)
            .map(|_| {
                format!(
                    "https://{}/{}/{}/releases/tag/${{finalAttrs.src.tag}}",
                    self.domain, self.owner, self.repo,
                )
            });

        let mut latest = if let Some(latest) = &latest_release {
            let tag = &latest.tag_name;
            versions.insert(tag.clone(), Version::Latest);
            completions.push(Pair {
                display: format!("{tag} (latest release)"),
                replacement: tag.clone(),
            });
            tag.clone()
        } else {
            "".into()
        };

        if let Some(tags) = tags {
            if latest.is_empty()
                && let Some(Tag { name }) = tags.first()
            {
                latest = name.clone();
            }

            for Tag { name } in tags {
                if matches!(&latest_release, Some(r) if r.tag_name == name) {
                    continue;
                }
                completions.push(Pair {
                    display: format!("{name} (tag)"),
                    replacement: name.clone(),
                });
                versions.insert(name, Version::Tag);
            }
        }

        if let Some(commits) = commits {
            let mut commits = commits.into_iter();

            if let Some(Commit { sha, commit }) = commits.next() {
                if latest.is_empty() {
                    latest = sha.clone();
                }

                let date = &commit.committer.date[0 .. 10];
                let msg = commit.message.lines().next().unwrap_or_default();

                completions.push(Pair {
                    display: format!("{sha} ({date} - HEAD) {msg}"),
                    replacement: sha.clone(),
                });
                versions.insert(
                    sha,
                    Version::Head {
                        date: date.into(),
                        msg: msg.into(),
                    },
                );
            }

            for Commit { sha, commit } in commits {
                let date = &commit.committer.date[0 .. 10];
                let msg = commit.message.lines().next().unwrap_or_default();
                completions.push(Pair {
                    display: format!("{sha} ({date}) {msg}"),
                    replacement: sha.clone(),
                });
                versions.insert(
                    sha,
                    Version::Commit {
                        date: date.into(),
                        msg: msg.into(),
                    },
                );
            }
        };

        let homepage = format!("https://{}/{}/{}", self.domain, self.owner, self.repo);

        PackageInfo {
            pname: self.repo.clone(),
            description,
            file_url_prefix: None,
            homepage,
            license: Vec::new(),
            python_dependencies: Default::default(),
            releases_page,
            revisions: Revisions {
                latest,
                completions,
                versions,
            },
        }
    }

    async fn get_version(&self, cl: &Client, rev: &str) -> Option<Version> {
        let Commit { sha, commit } = json(
            cl,
            format!(
                "https://{}/api/v1/repos/{}/{}/git/commits/{rev}",
                self.domain, self.owner, self.repo,
            ),
        )
        .await?;

        Some(if sha.starts_with(rev) {
            Version::Commit {
                date: commit.committer.date[0 .. 10].into(),
                msg: "".into(),
            }
        } else {
            Version::Tag
        })
    }

    async fn has_submodules(&self, cl: &Client, rev: &str) -> bool {
        success(
            cl,
            format!(
                "https://{}/api/v1/repos/{}/{}/contents/.gitmodules?ref={rev}",
                self.domain, self.owner, self.repo,
            ),
        )
        .await
    }
}
