#[macro_use]
extern crate rocket;

use std::fs;
use std::path::PathBuf;
use std::sync::LazyLock;

use rocket::Data;
use rocket::data::ToByteUnit;
use rocket::serde::json::Json;
use rocket::{fs::FileServer, serde::Deserialize};

use rocket_dyn_templates::{Template, context};

use dotenv::dotenv;
use serde::Serialize;
use tracing::warn;

use crate::error::ApiError;
use crate::model::{Package, PackageMeta};
use dsx_common::config::DsxConfig;

mod error;
mod model;

static DATA_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
    PathBuf::from(std::env::var("$DATA_DIR").unwrap_or("./data".to_string()))
});

// Search for a package
#[get("/?<q>")]
fn search_packages(q: Option<String>) -> Result<Template, ApiError> {
    #[derive(Serialize)]
    struct Package {
        name: String,
        description: String,
        updated_at: String,
    }

    let mut packages = Vec::new();

    let dir = match fs::read_dir(DATA_DIR.join("repos")) {
        Ok(dir) => dir,
        Err(e) => {
            warn!("failed to read repos directory: {}", e);
            return Err(ApiError::InternalServerError(()));
        }
    };

    for entry in dir {
        let entry = entry.map_err(|e| {
            warn!("failed to read entry: {}", e);
            ApiError::InternalServerError(())
        })?;

        let config_path = entry.path().join("repo").join("Dsx.toml");
        if config_path.exists() {
            let text = fs::read_to_string(&config_path).map_err(|e| {
                warn!("failed to read config file: {}", e);
                ApiError::InternalServerError(())
            })?;
            let config: DsxConfig = toml::from_str(&text).map_err(|e| {
                warn!("failed to parse config file: {}", e);
                ApiError::InternalServerError(())
            })?;

            error!("{}", config.description.clone().unwrap_or_default());

            // skip repo if it doesnt match query params
            if let Some(query) = &q
                && !(config.name.contains(query)
                    || config
                        .description
                        .clone()
                        .unwrap_or_default()
                        .contains(query))
            {
                continue;
            }

            packages.push(Package {
                name: config.name,
                description: config.description.unwrap_or_default(),
                updated_at: String::from("0:00"),
            })
        }
    }

    Ok(Template::render(
        "packages",
        context! {
            packages,
            query: q.clone().unwrap_or_default()
        },
    ))
}

// Main page for a repository, shows status, files, name etc.
#[get("/<name>")]
fn package_main(name: &str) -> Result<Template, ApiError> {
    // get package info
    let package = Package::load(name)?;

    println!("{}", package.config.name);

    Ok(Template::render(
        "package_home",
        context! {
            parent_path: String::new(),
            current_path: String::from("/"),
            package: package,
        },
    ))
}

// Path for a file within a repo
#[get("/<name>/~repo/<path..>")]
fn repo_file(name: &str, path: std::path::PathBuf) -> Result<Template, ApiError> {
    let package = Package::load(name)?;

    Ok(Template::render(
        "file",
        context! {
            package: package,
        },
    ))
}

// Search within a package's files
#[get("/<name>/~repo?<q>")]
fn search_repo_files(name: &str, q: Option<String>) -> String {
    format!("Search within {} for {:?}", name, q)
}

// Page listing repo artifacts by date
#[get("/<name>/artifacts")]
fn list_artifacts(name: &str) -> String {
    format!("Artifacts for package {}", name)
}

// Page for a specific artifact and status/logs
#[get("/<name>/artifacts/<id>")]
fn artifact_detail(name: &str, id: u64) -> String {
    format!("Artifact {} details for package {}", id, name)
}

#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct NewRepo<'r> {
    name: &'r str,
}

// Create repo
#[post("/pkg", data = "<repo>")]
fn create_repo(repo: Json<NewRepo<'_>>) -> Result<(), &'static str> {
    let path = DATA_DIR.join("repos").join(repo.name);

    if repo.name.is_empty() {
        return Err("Repository name cannot be empty!");
    }

    if path.exists() {
        tracing::info!(
            "Attempt to create repository '{}' which already exists.",
            repo.name
        );
        return Err("This repository already exists!");
    }

    if let Err(e) = fs::create_dir_all(path) {
        tracing::error!(
            "Attempted to create package with name {} - Error: {e},",
            repo.name
        );
        return Err("Internal server error");
    }

    Ok(())
}

// Repo status/metadata
#[get("/pkg/<name>")]
fn get_pkg(name: &str) -> Result<Json<(PackageMeta, DsxConfig)>, ApiError> {
    let package = Package::load(name).map_err(|e| {
        ApiError::NotFound(String::from("repo with name {name} does not exist"))
    })?;

    Ok(Json((package.meta, package.config)))
}

// Upload source tarball
#[post("/pkg/<name>/push", data = "<data>")]
async fn push_tarball(name: &str, data: Data<'_>) -> Result<(), ApiError> {
    let repo_dir = DATA_DIR.join("repos").join(name);
    let tmp_path = repo_dir.join("upload.tar.gz");
    let stream = data
        .open(256.mebibytes())
        .into_file(&tmp_path)
        .await
        .map_err(|e| ApiError::InternalServerError(()))?;

    if !stream.is_complete() {
        return Err(ApiError::BadRequest("Incomplete upload".to_string()));
    }

    // Unpack over the existing repo dir.
    if repo_dir.exists() {
        fs::remove_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?;
    }
    fs::create_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?;

    let _ = Package::unpack(&tmp_path)?;
    fs::remove_file(&tmp_path).ok();
    Ok(())
}

// Download source tarball
#[get("/pkg/<name>/pull")]
fn pull_tarball(
    name: &str,
) -> Result<(rocket::http::ContentType, Vec<u8>), Json<ApiError>> {
    if let Ok(package) = Package::load(name) {
        let tarball = package.tarball()?;
        Ok((
            rocket::http::ContentType::new("application", "octet-stream"),
            tarball,
        ))
    } else {
        Err(Json(ApiError::NotFound(format!(
            "repo with name {name} does not exist"
        ))))
    }
}

// Download compiled binary
#[get("/pkg/<name>/artifact")]
fn download_artifact(name: &str) -> &'static str {
    "Download artifact"
}

#[launch]
fn rocket() -> _ {
    dotenv().unwrap();

    rocket::build()
        .mount(
            "/packages",
            routes![
                search_packages,
                package_main,
                repo_file,
                search_repo_files,
                list_artifacts,
                artifact_detail,
            ],
        )
        .mount(
            "/api",
            routes![
                create_repo,
                get_pkg,
                push_tarball,
                pull_tarball,
                download_artifact
            ],
        )
        .attach(Template::fairing())
        .mount("/static", FileServer::from("./static"))
}
