use crate::{Result, config::ConfigProfile};
use anyhow::{Context, anyhow, bail};
use reqwest::{
    Client, Response,
    header::{AUTHORIZATION, HeaderValue},
};
use serde::{Deserialize, de::DeserializeOwned};
use url::Url;

#[derive(Clone)]
pub struct StudipClient {
    http: Client,
    base: Url,
    root: Url,
    auth_header: HeaderValue,
}

impl StudipClient {
    pub fn from_profile(profile: &ConfigProfile) -> Result<Self> {
        let auth_b64 = profile.basic_auth_b64.as_ref().ok_or_else(|| {
            anyhow!("No credentials configured for this profile. Run `studip-sync auth` first.")
        })?;

        let mut auth_header = HeaderValue::from_str(&format!("Basic {}", auth_b64))
            .context("Invalid base64 credentials")?;
        auth_header.set_sensitive(true);

        let (base, root) = build_root_and_api_urls(profile)?;

        let http = Client::builder()
            .user_agent("studip-sync/0.1")
            .build()
            .context("Failed to build HTTP client")?;

        Ok(Self {
            http,
            base,
            root,
            auth_header,
        })
    }

    pub async fn current_user(&self) -> Result<UserResponse> {
        let url = self.endpoint("users/me")?;
        self.get_json(url).await
    }

    pub async fn semester(&self, semester_id: &str) -> Result<SemesterResponse> {
        let url = self.endpoint(&format!("semesters/{semester_id}"))?;
        self.get_json(url).await
    }

    pub async fn list_courses(&self, user_id: &str) -> Result<Vec<Course>> {
        let path = format!("users/{user_id}/courses");
        self.fetch_all_pages(&path).await
    }

    pub async fn list_course_folders(&self, course_id: &str) -> Result<Vec<Folder>> {
        let path = format!("courses/{course_id}/folders");
        self.fetch_all_pages(&path).await
    }

    pub async fn list_subfolders(&self, folder_id: &str) -> Result<Vec<Folder>> {
        let path = format!("folders/{folder_id}/folders");
        self.fetch_all_pages(&path).await
    }

    pub async fn list_file_refs(&self, folder_id: &str) -> Result<Vec<FileRef>> {
        let path = format!("folders/{folder_id}/file-refs");
        self.fetch_all_pages(&path).await
    }

    pub async fn download_file(&self, relative_path: &str) -> Result<Response> {
        let url = self.download_endpoint(relative_path)?;
        self.send_request(url).await
    }

    fn endpoint(&self, path: &str) -> Result<Url> {
        let normalized = path.trim_start_matches('/');
        self.root.join(normalized).map_err(Into::into)
    }

    async fn get_json<T>(&self, url: Url) -> Result<T>
    where
        T: DeserializeOwned,
    {
        self.send_request(url)
            .await?
            .json::<T>()
            .await
            .map_err(Into::into)
    }

    async fn fetch_all_pages<T>(&self, path: &str) -> Result<Vec<T>>
    where
        T: DeserializeOwned,
    {
        let mut offset = 0usize;
        let limit = 100usize;
        let mut items = Vec::new();

        loop {
            let mut url = self.endpoint(path)?;
            {
                let mut pairs = url.query_pairs_mut();
                pairs.append_pair("page[offset]", &offset.to_string());
                pairs.append_pair("page[limit]", &limit.to_string());
            }

            let ListResponse { data, meta, .. } = self.get_json(url).await?;
            let count = data.len();
            items.extend(data);

            let total = meta
                .and_then(|meta| meta.page)
                .map(|page| page.total)
                .unwrap_or(offset + count);

            if offset + limit >= total || count == 0 {
                break;
            }
            offset += limit;
        }

        Ok(items)
    }
    fn download_endpoint(&self, path: &str) -> Result<Url> {
        let normalized = path.trim_start_matches('/');
        self.base.join(normalized).map_err(Into::into)
    }

    async fn send_request(&self, url: Url) -> Result<Response> {
        let response = self
            .http
            .get(url.clone())
            .header(AUTHORIZATION, self.auth_header.clone())
            .send()
            .await
            .with_context(|| format!("GET {}", url))?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            bail!("Stud.IP request failed ({status}) - {body}");
        }

        Ok(response)
    }
}

fn build_root_and_api_urls(profile: &ConfigProfile) -> Result<(Url, Url)> {
    let base = profile.base_url.trim_end_matches('/');
    let jsonapi_path = profile.jsonapi_path.trim_start_matches('/');

    let base_url = Url::parse(base).context("Invalid base_url")?;
    let mut api_url = base_url.clone();
    api_url.set_path(jsonapi_path);
    if !api_url.path().ends_with('/') {
        api_url.set_path(&format!("{}/", api_url.path().trim_end_matches('/')));
    }

    Ok((base_url, api_url))
}

#[derive(Debug, Deserialize)]
pub struct UserResponse {
    pub data: UserData,
}

#[derive(Debug, Deserialize)]
pub struct UserData {
    pub id: String,
    pub attributes: UserAttributes,
}

#[derive(Debug, Deserialize)]
pub struct UserAttributes {
    pub username: String,
    #[serde(rename = "formatted-name")]
    pub formatted_name: String,
}

#[derive(Debug, Deserialize)]
pub struct SemesterResponse {
    pub data: SemesterData,
}

#[derive(Debug, Deserialize)]
pub struct SemesterData {
    pub id: String,
    pub attributes: SemesterAttributes,
}

#[derive(Debug, Deserialize)]
pub struct SemesterAttributes {
    pub title: String,
}

#[derive(Debug, Deserialize)]
pub struct Course {
    pub id: String,
    pub attributes: CourseAttributes,
    pub relationships: CourseRelationships,
}

#[derive(Debug, Deserialize)]
pub struct CourseAttributes {
    #[serde(rename = "course-number")]
    pub course_number: Option<String>,
    pub title: String,
    #[serde(default)]
    pub subtitle: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct CourseRelationships {
    #[serde(rename = "start-semester")]
    pub start_semester: Option<Relationship>,
    #[serde(rename = "end-semester")]
    pub end_semester: Option<Relationship>,
}

impl CourseRelationships {
    pub fn semester_id(&self) -> Option<&str> {
        self.start_semester
            .as_ref()
            .and_then(|rel| rel.data.as_ref())
            .map(|link| link.id.as_str())
    }
}

#[derive(Debug, Deserialize)]
pub struct Relationship {
    pub data: Option<ResourceIdentifier>,
}

#[derive(Debug, Deserialize)]
pub struct ResourceIdentifier {
    #[serde(rename = "type")]
    pub resource_type: String,
    pub id: String,
}

#[derive(Debug, Deserialize)]
struct ListResponse<T> {
    pub data: Vec<T>,
    pub meta: Option<ListMeta>,
}

#[derive(Debug, Deserialize)]
struct ListMeta {
    pub page: Option<PageMeta>,
}

#[derive(Debug, Deserialize)]
struct PageMeta {
    #[allow(dead_code)]
    pub offset: usize,
    #[allow(dead_code)]
    pub limit: usize,
    pub total: usize,
}

#[derive(Debug, Deserialize, Clone)]
pub struct Folder {
    pub id: String,
    pub attributes: FolderAttributes,
}

#[derive(Debug, Deserialize, Clone)]
pub struct FolderAttributes {
    #[serde(rename = "folder-type")]
    pub folder_type: String,
    pub name: String,
    #[serde(rename = "is-readable")]
    pub is_readable: bool,
    #[serde(rename = "is-visible")]
    pub is_visible: bool,
}

#[derive(Debug, Deserialize, Clone)]
pub struct FileRef {
    pub id: String,
    pub attributes: FileRefAttributes,
    pub meta: Option<FileRefMeta>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct FileRefAttributes {
    pub name: String,
    #[serde(rename = "mkdate")]
    pub created: String,
    #[serde(rename = "chdate")]
    pub modified: String,
    #[serde(rename = "filesize")]
    #[serde(default)]
    pub file_size: Option<u64>,
    #[serde(rename = "mime-type")]
    #[serde(default)]
    pub mime_type: Option<String>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct FileRefMeta {
    #[serde(rename = "download-url")]
    pub download_url: String,
}
