use std::{error, fmt, str::FromStr};

use reqwest::{
    Client as HttpClient,
    header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue},
};
pub use reqwest::{Method, RequestBuilder, Response, StatusCode, Url};
use serde::{Deserialize, Serialize};

use gitea_openapi::apis;
pub use gitea_openapi::models;
pub use models::{Repository, ServerVersion, User};

mod actions;
pub mod activity;
mod config;
pub mod diff;
mod domain;
mod issues;
mod milestones;
pub mod notifications;
mod pulls;
mod repositories;

pub use actions::{ActionJobLog, ActionRunDetails};
pub use activity::{ActivityFilter, ServerActivityPager};
pub use config::{Config, Selection, ServerProfile, TuiPreferences, server_url};
pub use domain::{
    CreateIssue, DEFAULT_PAGE_SIZE, EditIssue, HistoryCommit, HomeData, IssueDetails, IssueDraft,
    IssueEditorData, IssueQuery, MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryId,
    api_date, api_timestamp, civil_from_days, days_from_civil, parse_api_date,
};
pub use issues::comment_can_edit;
pub use pulls::{PullFileSource, pull_file_source, pull_state};

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
    #[default]
    Gitea,
    Forgejo,
}

impl fmt::Display for Provider {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::Gitea => "gitea",
            Self::Forgejo => "forgejo",
        })
    }
}

impl FromStr for Provider {
    type Err = Error;

    fn from_str(value: &str) -> Result<Self> {
        match value.to_ascii_lowercase().as_str() {
            "gitea" => Ok(Self::Gitea),
            "forgejo" => Ok(Self::Forgejo),
            _ => Err(Error::InvalidInput(
                "provider must be gitea or forgejo".into(),
            )),
        }
    }
}

pub(crate) fn validate_page(page: i32, limit: i32) -> Result<()> {
    if page < 1 || limit < 1 {
        return Err(Error::InvalidInput(
            "page and limit must be positive".into(),
        ));
    }
    Ok(())
}

pub(crate) fn positive(value: i64, name: &str) -> Result<()> {
    if value < 1 {
        return Err(Error::InvalidInput(format!(
            "{name} must be a positive integer"
        )));
    }
    Ok(())
}

#[derive(Debug)]
pub enum Error {
    Configuration(String),
    InvalidInput(String),
    Forbidden(String),
    Transport(reqwest::Error),
    Generated(String),
    Api { status: StatusCode, message: String },
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Configuration(message) => formatter.write_str(message),
            Self::InvalidInput(message) => formatter.write_str(message),
            Self::Forbidden(message) => formatter.write_str(message),
            Self::Transport(error) => error.fmt(formatter),
            Self::Generated(message) => formatter.write_str(message),
            Self::Api { status, message } => {
                write!(formatter, "Server returned {status}: {message}")
            }
        }
    }
}

impl error::Error for Error {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            Self::Transport(error) => Some(error),
            _ => None,
        }
    }
}

impl From<reqwest::Error> for Error {
    fn from(error: reqwest::Error) -> Self {
        Self::Transport(error)
    }
}

impl Error {
    pub(crate) fn generated(error: impl fmt::Display) -> Self {
        Self::Generated(error.to_string())
    }
}

#[derive(Clone, Debug)]
pub struct Client {
    api_url: Url,
    forgejo_api_url: Url,
    http: HttpClient,
    provider: Provider,
}

impl Client {
    pub fn new(instance_url: &str, token: Option<&str>) -> Result<Self> {
        Self::with_provider(instance_url, token, Provider::Gitea)
    }

    pub fn with_provider(
        instance_url: &str,
        token: Option<&str>,
        provider: Provider,
    ) -> Result<Self> {
        let mut instance_url = Url::parse(instance_url)
            .map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;

        if !matches!(instance_url.scheme(), "http" | "https") {
            return Err(Error::Configuration(
                "Server URL must use http or https".into(),
            ));
        }
        if !instance_url.username().is_empty() || instance_url.password().is_some() {
            return Err(Error::Configuration(
                "Server URL must not contain credentials".into(),
            ));
        }
        if !instance_url.path().ends_with('/') {
            instance_url.set_path(&format!("{}/", instance_url.path()));
        }

        let api_url = instance_url
            .join("api/v1/")
            .map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;
        let forgejo_api_url = instance_url
            .join("api/forgejo/v1/")
            .map_err(|error| Error::Configuration(format!("invalid server URL: {error}")))?;
        let mut headers = HeaderMap::new();
        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
        if let Some(token) = token {
            let value = HeaderValue::from_str(&format!("token {token}"))
                .map_err(|_| Error::Configuration("invalid server token".into()))?;
            headers.insert(AUTHORIZATION, value);
        }

        Ok(Self {
            api_url,
            forgejo_api_url,
            http: HttpClient::builder().default_headers(headers).build()?,
            provider,
        })
    }

    pub async fn discover(
        instance_url: &str,
        token: Option<&str>,
        fallback: Provider,
    ) -> Result<Self> {
        let mut client = Self::with_provider(instance_url, token, fallback)?;
        let version = client.gitea_version().await?;
        if is_forgejo_version(&version) {
            client.provider = Provider::Forgejo;
            return Ok(client);
        }

        match client.forgejo_version().await {
            Ok(_) => client.provider = Provider::Forgejo,
            Err(Error::Api { status, .. }) if status == StatusCode::NOT_FOUND => {
                if fallback == Provider::Forgejo {
                    return Err(Error::Configuration(
                        "Server does not expose the Forgejo API".into(),
                    ));
                }
                client.provider = Provider::Gitea;
            }
            Err(error) => return Err(error),
        }
        Ok(client)
    }

    pub fn api_url(&self) -> &Url {
        &self.api_url
    }

    pub fn provider(&self) -> Provider {
        self.provider
    }

    /// Returns an authenticated configuration for every generated typed API.
    pub(crate) fn configuration(&self) -> apis::configuration::Configuration {
        apis::configuration::Configuration {
            base_path: self.api_url.as_str().trim_end_matches('/').into(),
            client: self.http.clone(),
            user_agent: Some(format!("gotcha_gitea/{}", env!("CARGO_PKG_VERSION"))),
            ..Default::default()
        }
    }

    /// Builds an authenticated request to an API-relative endpoint.
    ///
    /// The returned reqwest builder supports JSON, forms, multipart uploads,
    /// query parameters, and byte streams without duplicating those APIs here.
    pub fn request(&self, method: Method, endpoint: &str) -> Result<RequestBuilder> {
        if endpoint.starts_with('/')
            || Url::parse(endpoint).is_ok()
            || endpoint.split('/').any(|segment| segment == "..")
        {
            return Err(Error::Configuration(
                "endpoint must be a relative path below /api/v1".into(),
            ));
        }

        let url = self
            .api_url
            .join(endpoint)
            .map_err(|error| Error::Configuration(format!("invalid endpoint: {error}")))?;
        Ok(self.http.request(method, url))
    }

    pub async fn execute(&self, request: RequestBuilder) -> Result<Response> {
        let response = request.send().await?;
        let status = response.status();
        if status.is_success() {
            return Ok(response);
        }

        let body = response.text().await?;
        let message = serde_json::from_str::<ApiError>(&body)
            .ok()
            .map(|error| error.message)
            .filter(|message| !message.is_empty())
            .unwrap_or(body);
        Err(Error::Api { status, message })
    }

    pub async fn version(&self) -> Result<models::ServerVersion> {
        match self.provider {
            Provider::Gitea => self.gitea_version().await,
            Provider::Forgejo => self.forgejo_version().await,
        }
    }

    async fn gitea_version(&self) -> Result<models::ServerVersion> {
        apis::miscellaneous_api::get_version(&self.configuration())
            .await
            .map_err(|error| Error::Generated(error.to_string()))
    }

    async fn forgejo_version(&self) -> Result<models::ServerVersion> {
        let request =
            self.http
                .get(self.forgejo_api_url.join("version").map_err(|error| {
                    Error::Configuration(format!("invalid Forgejo endpoint: {error}"))
                })?);
        Ok(self.execute(request).await?.json().await?)
    }

    pub async fn current_user(&self) -> Result<models::User> {
        apis::user_api::user_get_current(&self.configuration())
            .await
            .map_err(|error| Error::Generated(error.to_string()))
    }

    pub async fn current_user_repositories(&self) -> Result<Vec<models::Repository>> {
        apis::user_api::user_current_list_repos(&self.configuration(), None, None)
            .await
            .map_err(|error| Error::Generated(error.to_string()))
    }

    pub async fn repository(&self, owner: &str, repository: &str) -> Result<models::Repository> {
        if owner.is_empty() || repository.is_empty() {
            return Err(Error::Configuration(
                "repository owner and name must not be empty".into(),
            ));
        }

        apis::repository_api::repo_get(&self.configuration(), owner, repository)
            .await
            .map_err(|error| Error::Generated(error.to_string()))
    }
}

fn is_forgejo_version(version: &models::ServerVersion) -> bool {
    version
        .version
        .as_deref()
        .is_some_and(|version| version.contains("+gitea-"))
}

#[derive(Debug, Deserialize)]
struct ApiError {
    #[serde(default)]
    message: String,
}

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

    #[test]
    fn keeps_instance_subpaths_and_rejects_endpoint_escapes() {
        let client = Client::with_provider(
            "https://example.com/gitea",
            Some("secret"),
            Provider::Forgejo,
        )
        .unwrap();

        assert_eq!(
            client.api_url().as_str(),
            "https://example.com/gitea/api/v1/"
        );
        assert!(client.request(Method::GET, "repos/octo/demo").is_ok());
        assert!(client.request(Method::GET, "../admin").is_err());
        assert!(
            client
                .request(Method::GET, "https://example.net/user")
                .is_err()
        );
        assert_eq!(
            client.configuration().base_path,
            "https://example.com/gitea/api/v1"
        );
        assert_eq!(client.provider(), Provider::Forgejo);
        assert_eq!(
            client.forgejo_api_url.as_str(),
            "https://example.com/gitea/api/forgejo/v1/"
        );
        assert_eq!("GITEA".parse::<Provider>().unwrap(), Provider::Gitea);
        assert_eq!(Provider::Forgejo.to_string(), "forgejo");
        assert!("github".parse::<Provider>().is_err());
        assert!(is_forgejo_version(&models::ServerVersion {
            version: Some("16.0.1+gitea-1.24.6".into()),
        }));
        assert!(!is_forgejo_version(&models::ServerVersion {
            version: Some("1.25.2".into()),
        }));
        let _typed_model = models::Issue::default();
    }
}
