use anyhow::{Context, bail};
use clap::Parser;
use std::path::PathBuf;

const SERVER_URL: &str = env!("SERVER_URL");

#[derive(Parser)]
#[command(name = "curlink", about = "Download files from a curlink server")]
struct Args {
    /// Short code of the file to download
    short_code: String,

    /// Save to /tmp/curlink/ instead of current directory
    #[arg(short = 't', long = "tmp")]
    tmp: bool,

    /// Copy the downloaded file path to clipboard
    #[arg(short = 'c', long = "copy")]
    copy: bool,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    let url = format!("{}/{}", SERVER_URL.trim_end_matches('/'), args.short_code);

    let response = reqwest::get(&url)
        .await
        .context("failed to connect to server")?;

    if !response.status().is_success() {
        bail!("server returned {}", response.status());
    }

    let filename = extract_filename(&response, &args.short_code);

    let dest_dir: PathBuf = if args.tmp {
        let dir = PathBuf::from("/tmp/curlink");
        tokio::fs::create_dir_all(&dir)
            .await
            .context("failed to create /tmp/curlink")?;
        dir
    } else {
        std::env::current_dir().context("failed to get current directory")?
    };

    let dest_path = dest_dir.join(&filename);

    let bytes = response.bytes().await.context("failed to read response body")?;
    tokio::fs::write(&dest_path, &bytes)
        .await
        .context("failed to write file")?;

    let abs_path = dest_path
        .canonicalize()
        .context("failed to resolve absolute path")?;

    if args.copy {
        let mut clipboard = arboard::Clipboard::new().context("failed to open clipboard")?;
        clipboard
            .set_text(abs_path.to_string_lossy().to_string())
            .context("failed to write to clipboard")?;
    }

    if !(args.tmp && args.copy) {
        println!("{}", abs_path.display());
    }

    Ok(())
}

fn extract_filename(response: &reqwest::Response, short_code: &str) -> String {
    if let Some(cd) = response.headers().get(reqwest::header::CONTENT_DISPOSITION) {
        if let Ok(value) = cd.to_str() {
            if let Some(name) = parse_content_disposition(value) {
                return name;
            }
        }
    }

    if let Some(ct) = response.headers().get(reqwest::header::CONTENT_TYPE) {
        if let Ok(mime) = ct.to_str() {
            if let Some(ext) = mime_to_ext(mime.split(';').next().unwrap_or("").trim()) {
                return format!("{short_code}.{ext}");
            }
        }
    }

    short_code.to_string()
}

fn parse_content_disposition(value: &str) -> Option<String> {
    for part in value.split(';') {
        let part = part.trim();
        if let Some(rest) = part.strip_prefix("filename=\"") {
            return Some(rest.trim_end_matches('"').to_string());
        }
        if let Some(rest) = part.strip_prefix("filename=") {
            return Some(rest.to_string());
        }
    }
    None
}

fn mime_to_ext(mime: &str) -> Option<&'static str> {
    match mime {
        "image/jpeg" => Some("jpg"),
        "image/png" => Some("png"),
        "image/gif" => Some("gif"),
        "image/webp" => Some("webp"),
        "video/mp4" => Some("mp4"),
        "video/webm" => Some("webm"),
        "audio/mpeg" => Some("mp3"),
        "audio/ogg" => Some("ogg"),
        "audio/wav" => Some("wav"),
        "application/pdf" => Some("pdf"),
        "application/zip" => Some("zip"),
        "text/plain" => Some("txt"),
        _ => None,
    }
}
