use anyhow::{anyhow, Context, Result};
use futures::io::BufReader;
use futures::AsyncReadExt;
use http_client::{AsyncBody, HttpClient, Request as HttpRequest};
use paths::supermaven_dir;
use serde::{Deserialize, Serialize};
use smol::fs::{self, File};
use smol::stream::StreamExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;

#[derive(Serialize)]
pub struct GetExternalUserRequest {
    pub id: String,
}

#[derive(Serialize)]
pub struct CreateExternalUserRequest {
    pub id: String,
    pub email: String,
}

#[derive(Serialize)]
pub struct DeleteExternalUserRequest {
    pub id: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateExternalUserResponse {
    pub api_key: String,
}

#[derive(Deserialize)]
pub struct SupermavenApiError {
    pub message: String,
}

pub struct SupermavenBinary {}

pub struct SupermavenAdminApi {
    admin_api_key: String,
    api_url: String,
    http_client: Arc<dyn HttpClient>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupermavenDownloadResponse {
    pub download_url: String,
    pub version: u64,
    pub sha256_hash: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupermavenUser {
    id: String,
    email: String,
    api_key: String,
}

impl SupermavenAdminApi {
    pub fn new(admin_api_key: String, http_client: Arc<dyn HttpClient>) -> Self {
        Self {
            admin_api_key,
            api_url: "https://supermaven.com/api/".to_string(),
            http_client,
        }
    }

    pub async fn try_get_user(
        &self,
        request: GetExternalUserRequest,
    ) -> Result<Option<SupermavenUser>> {
        let uri = format!("{}external-user/{}", &self.api_url, &request.id);

        let request = HttpRequest::get(&uri).header("Authorization", self.admin_api_key.clone());

        let mut response = self
            .http_client
            .send(request.body(AsyncBody::default())?)
            .await
            .with_context(|| "Unable to get Supermaven API Key".to_string())?;

        let mut body = Vec::new();
        response.body_mut().read_to_end(&mut body).await?;

        if response.status().is_client_error() {
            let error: SupermavenApiError = serde_json::from_slice(&body)?;
            if error.message == "User not found" {
                return Ok(None);
            } else {
                return Err(anyhow!("Supermaven API error: {}", error.message));
            }
        } else if response.status().is_server_error() {
            let error: SupermavenApiError = serde_json::from_slice(&body)?;
            return Err(anyhow!("Supermaven API server error").context(error.message));
        }

        let body_str = std::str::from_utf8(&body)?;

        Ok(Some(
            serde_json::from_str::<SupermavenUser>(body_str)
                .with_context(|| "Unable to parse Supermaven user response".to_string())?,
        ))
    }

    pub async fn try_create_user(
        &self,
        request: CreateExternalUserRequest,
    ) -> Result<CreateExternalUserResponse> {
        let uri = format!("{}external-user", &self.api_url);

        let request = HttpRequest::post(&uri)
            .header("Authorization", self.admin_api_key.clone())
            .body(AsyncBody::from(serde_json::to_vec(&request)?))?;

        let mut response = self
            .http_client
            .send(request)
            .await
            .with_context(|| "Unable to create Supermaven API Key".to_string())?;

        let mut body = Vec::new();
        response.body_mut().read_to_end(&mut body).await?;

        let body_str = std::str::from_utf8(&body)?;

        if !response.status().is_success() {
            let error: SupermavenApiError = serde_json::from_slice(&body)?;
            return Err(anyhow!("Supermaven API server error").context(error.message));
        }

        serde_json::from_str::<CreateExternalUserResponse>(body_str)
            .with_context(|| "Unable to parse Supermaven API Key response".to_string())
    }

    pub async fn try_delete_user(&self, request: DeleteExternalUserRequest) -> Result<()> {
        let uri = format!("{}external-user/{}", &self.api_url, &request.id);

        let request = HttpRequest::delete(&uri).header("Authorization", self.admin_api_key.clone());

        let mut response = self
            .http_client
            .send(request.body(AsyncBody::default())?)
            .await
            .with_context(|| "Unable to delete Supermaven User".to_string())?;

        let mut body = Vec::new();
        response.body_mut().read_to_end(&mut body).await?;

        if response.status().is_client_error() {
            let error: SupermavenApiError = serde_json::from_slice(&body)?;
            if error.message == "User not found" {
                return Ok(());
            } else {
                return Err(anyhow!("Supermaven API error: {}", error.message));
            }
        } else if response.status().is_server_error() {
            let error: SupermavenApiError = serde_json::from_slice(&body)?;
            return Err(anyhow!("Supermaven API server error").context(error.message));
        }

        Ok(())
    }

    pub async fn try_get_or_create_user(
        &self,
        request: CreateExternalUserRequest,
    ) -> Result<CreateExternalUserResponse> {
        let get_user_request = GetExternalUserRequest {
            id: request.id.clone(),
        };

        match self.try_get_user(get_user_request).await? {
            None => self.try_create_user(request).await,
            Some(SupermavenUser { api_key, .. }) => Ok(CreateExternalUserResponse { api_key }),
        }
    }
}

pub async fn latest_release(
    client: Arc<dyn HttpClient>,
    platform: &str,
    arch: &str,
) -> Result<SupermavenDownloadResponse> {
    let uri = format!(
        "https://supermaven.com/api/download-path?platform={}&arch={}",
        platform, arch
    );

    // Download is not authenticated
    let request = HttpRequest::get(&uri);

    let mut response = client
        .send(request.body(AsyncBody::default())?)
        .await
        .with_context(|| "Unable to acquire Supermaven Agent".to_string())?;

    let mut body = Vec::new();
    response.body_mut().read_to_end(&mut body).await?;

    if response.status().is_client_error() || response.status().is_server_error() {
        let body_str = std::str::from_utf8(&body)?;
        let error: SupermavenApiError = serde_json::from_str(body_str)?;
        return Err(anyhow!("Supermaven API error: {}", error.message));
    }

    serde_json::from_slice::<SupermavenDownloadResponse>(&body)
        .with_context(|| "Unable to parse Supermaven Agent response".to_string())
}

pub fn version_path(version: u64) -> PathBuf {
    supermaven_dir().join(format!(
        "sm-agent-{}{}",
        version,
        std::env::consts::EXE_SUFFIX
    ))
}

pub async fn has_version(version_path: &Path) -> bool {
    fs::metadata(version_path)
        .await
        .map_or(false, |m| m.is_file())
}

pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> {
    fs::create_dir_all(supermaven_dir())
        .await
        .with_context(|| {
            format!(
                "Could not create Supermaven Agent Directory at {:?}",
                supermaven_dir()
            )
        })?;

    let platform = match std::env::consts::OS {
        "macos" => "darwin",
        "windows" => "windows",
        "linux" => "linux",
        _ => return Err(anyhow!("unsupported platform")),
    };

    let arch = match std::env::consts::ARCH {
        "x86_64" => "amd64",
        "aarch64" => "arm64",
        _ => return Err(anyhow!("unsupported architecture")),
    };

    let download_info = latest_release(client.clone(), platform, arch).await?;

    let binary_path = version_path(download_info.version);

    if has_version(&binary_path).await {
        return Ok(binary_path);
    }

    let request = HttpRequest::get(&download_info.download_url);

    let mut response = client
        .send(request.body(AsyncBody::default())?)
        .await
        .with_context(|| "Unable to download Supermaven Agent".to_string())?;

    let mut file = File::create(&binary_path)
        .await
        .with_context(|| format!("Unable to create file at {:?}", binary_path))?;

    futures::io::copy(BufReader::new(response.body_mut()), &mut file)
        .await
        .with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?;

    #[cfg(not(windows))]
    {
        file.set_permissions(<fs::Permissions as fs::unix::PermissionsExt>::from_mode(
            0o755,
        ))
        .await?;
    }

    let mut old_binary_paths = fs::read_dir(supermaven_dir()).await?;
    while let Some(old_binary_path) = old_binary_paths.next().await {
        let old_binary_path = old_binary_path?;
        if old_binary_path.path() != binary_path {
            fs::remove_file(old_binary_path.path()).await?;
        }
    }

    Ok(binary_path)
}
