s3ec 0.1.4

File sync daemon and CLI for an S3-compatible event server
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use chrono::Utc;
use reqwest::Client;
use serde_json::Value;
use tokio::fs;

use crate::config::{self, Config};

pub async fn login(server_url: &str, api_key: &str) -> anyhow::Result<()> {
    let client = Client::new();
    let resp = client
        .post(format!("{}/api/auth/login", server_url.trim_end_matches('/')))
        .json(&serde_json::json!({ "api_key": api_key }))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await?;
        anyhow::bail!("Login failed ({}): {}", status, body);
    }

    let data: Value = resp.json().await?;
    let cfg = Config {
        server_url: server_url.trim_end_matches('/').to_string(),
        api_key: api_key.to_string(),
        token: data["token"].as_str().unwrap_or_default().to_string(),
        expires_at: data["expires_at"].as_str().unwrap_or_default().to_string(),
    };
    config::save(&cfg)?;
    println!("Logged in. Token expires at {}", cfg.expires_at);
    Ok(())
}

pub async fn refresh_token(cfg: &Config) -> anyhow::Result<()> {
    let client = Client::new();
    let resp = client
        .post(format!("{}/api/auth/login", cfg.server_url))
        .json(&serde_json::json!({ "api_key": &cfg.api_key }))
        .send()
        .await?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await?;
        anyhow::bail!("Token refresh failed ({}): {}", status, body);
    }

    let data: Value = resp.json().await?;
    let mut new_cfg = cfg.clone();
    new_cfg.token = data["token"].as_str().unwrap_or_default().to_string();
    new_cfg.expires_at = data["expires_at"].as_str().unwrap_or_default().to_string();
    config::save(&new_cfg)?;
    tracing::info!("Token refreshed, expires at {}", new_cfg.expires_at);
    Ok(())
}

pub async fn ensure_valid_token() -> anyhow::Result<()> {
    let cfg = config::load()?;
    if cfg.is_token_expired() {
        refresh_token(&cfg).await?;
    }
    Ok(())
}

fn build_client() -> Client {
    Client::new()
}

async fn get(endpoint: &str) -> anyhow::Result<reqwest::Response> {
    let cfg = config::load()?;
    let resp = build_client()
        .get(format!("{}{}", cfg.server_url, endpoint))
        .bearer_auth(&cfg.token)
        .send()
        .await?;
    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await?;
        anyhow::bail!("Server error ({}): {}", status, body);
    }
    Ok(resp)
}

pub async fn upload(file_path: &str, remote_path: Option<&str>) -> anyhow::Result<()> {
    let cfg = config::load()?;
    let path = Path::new(file_path);
    let file_name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("unknown");

    let meta = fs::metadata(path).await?;
    if meta.len() == 0 {
        anyhow::bail!("File is empty, skipping upload");
    }

    let data = fs::read(path).await?;

    let upload_meta = serde_json::json!({
        "path": remote_path.unwrap_or(""),
        "mode": Some(meta.permissions().mode() as i64),
        "mtime": meta.modified().ok().map(|t| {
            let dt: chrono::DateTime<Utc> = t.into();
            dt.to_rfc3339()
        }),
    });

    let part = reqwest::multipart::Part::bytes(data)
        .file_name(file_name.to_string())
        .mime_str(
            mime_guess::from_path(file_name)
                .first_or_octet_stream()
                .as_ref(),
        )?;
    let metadata_part = reqwest::multipart::Part::bytes(upload_meta.to_string().into_bytes())
        .mime_str("application/json")?;

    let form = reqwest::multipart::Form::new()
        .part("file", part)
        .text("file_name", file_name.to_string())
        .part("metadata", metadata_part);

    let resp = build_client()
        .put(format!("{}/api/files/upload", cfg.server_url))
        .bearer_auth(&cfg.token)
        .multipart(form)
        .send()
        .await?;

    let status = resp.status();
    if status == reqwest::StatusCode::NO_CONTENT {
        println!("Uploaded: {} (unchanged)", file_name);
        return Ok(());
    }

    let body: Value = resp.json().await?;

    if status.is_success() {
        println!("Uploaded: {} (id: {})", file_name, body["id"]);
    } else {
        anyhow::bail!("Upload failed ({}): {}", status, body);
    }
    Ok(())
}

pub async fn download(id: &str, output: Option<&str>) -> anyhow::Result<()> {
    let resp = get(&format!("/api/files/{}", id)).await?;
    let file_info: Value = resp.json().await?;
    let file_name = output
        .map(|s| s.to_string())
        .unwrap_or_else(|| file_info["name"].as_str().unwrap_or("download").to_string());

    let resp = get(&format!("/api/files/{}/download", id)).await?;
    let bytes = resp.bytes().await?;
    fs::write(&file_name, &bytes).await?;

    if let Some(mode) = file_info["mode"].as_i64() {
        if let Ok(m) = std::fs::metadata(&file_name) {
            let mut perms = m.permissions();
            perms.set_mode(mode as u32);
            let _ = std::fs::set_permissions(&file_name, perms);
        }
    }
    if let Some(mtime_str) = file_info["mtime"].as_str() {
        if let Ok(mtime) = chrono::DateTime::parse_from_rfc3339(mtime_str) {
            let ts = filetime::FileTime::from_unix_time(mtime.timestamp(), 0);
            let _ = filetime::set_file_times(&file_name, ts, ts);
        }
    }

    println!("Downloaded: {} ({} bytes)", file_name, bytes.len());
    Ok(())
}

pub async fn list(
    path: Option<&str>,
    search: Option<&str>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> anyhow::Result<()> {
    let mut query = Vec::new();
    if let Some(p) = path {
        query.push(format!("path={}", urlencoding::encode(p)));
    }
    if let Some(s) = search {
        query.push(format!("search={}", urlencoding::encode(s)));
    }
    if let Some(l) = limit {
        query.push(format!("limit={}", l));
    }
    if let Some(o) = offset {
        query.push(format!("offset={}", o));
    }
    let qs = if query.is_empty() {
        String::new()
    } else {
        format!("?{}", query.join("&"))
    };

    let resp = get(&format!("/api/files{}", qs)).await?;
    let files: Vec<Value> = resp.json().await?;

    if files.is_empty() {
        println!("No files found.");
    } else {
        for f in &files {
            let size = f["size"].as_i64().unwrap_or(0);
            let size_str = if size > 1024 * 1024 {
                format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
            } else if size > 1024 {
                format!("{:.1} KB", size as f64 / 1024.0)
            } else {
                format!("{} B", size)
            };
            let display_path = match f["path"].as_str() {
                Some(p) if !p.is_empty() => format!("{}/", p),
                _ => String::new(),
            };
            println!(
                "{}  {}  {}  {}{}",
                f["id"].as_str().unwrap_or("?"),
                size_str,
                f["updated_at"].as_str().unwrap_or(""),
                display_path,
                f["name"].as_str().unwrap_or("")
            );
        }
    }
    Ok(())
}

pub async fn info(id: &str) -> anyhow::Result<()> {
    let resp = get(&format!("/api/files/{}", id)).await?;
    let file: Value = resp.json().await?;
    println!("{}", serde_json::to_string_pretty(&file)?);
    Ok(())
}

pub async fn rm(id: &str) -> anyhow::Result<()> {
    let cfg = config::load()?;
    let resp = build_client()
        .delete(format!("{}/api/files/{}", cfg.server_url, id))
        .bearer_auth(&cfg.token)
        .send()
        .await?;
    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await?;
        anyhow::bail!("Delete failed ({}): {}", status, body);
    }
    println!("Deleted file: {}", id);
    Ok(())
}