use std::path::PathBuf;

use clap::{Parser, Subcommand};
use edifact_mapper::DataDir;
use mig_bo4e::engine::DataBundle;

#[derive(Parser)]
#[command(name = "edifact-data", about = "Manage EDIFACT mapping data bundles")]
struct Cli {
    #[command(subcommand)]
    command: Command,

    /// GitHub token for private repo access. Also reads GITHUB_TOKEN env var.
    #[arg(long, global = true, env = "GITHUB_TOKEN")]
    token: Option<String>,
}

#[derive(Subcommand)]
enum Command {
    /// Download data bundle(s) from GitHub releases.
    Fetch {
        /// Format version to fetch (e.g., FV2504). Omit for --all.
        fv: Option<String>,
        #[arg(long)]
        all: bool,
        #[arg(long)]
        data_dir: Option<String>,
        /// Bundle version to download (e.g., 0.5.0). Defaults to this binary's version.
        #[arg(long)]
        version: Option<String>,
    },
    /// List locally available data bundles.
    List {
        #[arg(long)]
        data_dir: Option<String>,
    },
    /// Update bundles to latest compatible version.
    Update {
        #[arg(long)]
        data_dir: Option<String>,
        /// Bundle version to update to (e.g., 0.5.0). Defaults to this binary's version.
        #[arg(long)]
        version: Option<String>,
    },
}

const GITHUB_REPO: &str = "Hochfrequenz/edifact_mapper";
const KNOWN_FVS: &[&str] = &["FV2504", "FV2510", "FV2604"];

fn resolve_data_dir(data_dir: Option<&str>) -> PathBuf {
    data_dir
        .map(PathBuf::from)
        .unwrap_or_else(|| DataDir::auto().data_path().to_path_buf())
}

fn main() {
    let cli = Cli::parse();
    let token = cli.token.clone().or_else(resolve_gh_token);
    let token = token.as_deref();

    match cli.command {
        Command::Fetch {
            fv,
            all,
            data_dir,
            version,
        } => {
            let dir = resolve_data_dir(data_dir.as_deref());
            std::fs::create_dir_all(&dir).expect("Failed to create data directory");
            let fvs: Vec<&str> = if all {
                KNOWN_FVS.to_vec()
            } else if let Some(ref fv) = fv {
                vec![fv.as_str()]
            } else {
                eprintln!("Specify a format version or use --all");
                std::process::exit(1);
            };
            let version = version.as_deref().unwrap_or(env!("CARGO_PKG_VERSION"));
            for fv in fvs {
                match fetch_bundle(fv, version, &dir, token) {
                    Ok(size) => println!(
                        "Fetched {fv}: {:.1}MB -> {}",
                        size as f64 / 1_048_576.0,
                        dir.join(format!("edifact-data-{fv}.bin")).display()
                    ),
                    Err(e) => eprintln!("Failed to fetch {fv}: {e}"),
                }
            }
        }
        Command::List { data_dir } => {
            let dir = resolve_data_dir(data_dir.as_deref());
            if !dir.exists() {
                println!("No data directory found at {}", dir.display());
                return;
            }
            let mut found = false;
            for fv in KNOWN_FVS {
                let path = dir.join(format!("edifact-data-{fv}.bin"));
                if path.exists() {
                    let meta = std::fs::metadata(&path).unwrap();
                    println!(
                        "  {fv}  {:.1}MB  {}",
                        meta.len() as f64 / 1_048_576.0,
                        path.display()
                    );
                    found = true;
                }
            }
            if !found {
                println!("No bundles found in {}", dir.display());
            }
        }
        Command::Update { data_dir, version } => {
            let dir = resolve_data_dir(data_dir.as_deref());
            std::fs::create_dir_all(&dir).expect("Failed to create data directory");
            let version = version.as_deref().unwrap_or(env!("CARGO_PKG_VERSION"));
            for fv in KNOWN_FVS {
                let path = dir.join(format!("edifact-data-{fv}.bin"));
                if path.exists() {
                    match fetch_bundle(fv, version, &dir, token) {
                        Ok(size) => {
                            println!("Updated {fv}: {:.1}MB", size as f64 / 1_048_576.0)
                        }
                        Err(e) => eprintln!("Failed to update {fv}: {e}"),
                    }
                }
            }
        }
    }
}

/// Download a bundle from GitHub releases and decompress it.
///
/// For private repos, uses the GitHub API to find the release asset ID,
/// then downloads via the asset API with auth. For public repos (no token),
/// uses the direct download URL.
fn fetch_bundle(
    fv: &str,
    version: &str,
    data_dir: &std::path::Path,
    token: Option<&str>,
) -> Result<u64, Box<dyn std::error::Error>> {
    let asset_name = format!("edifact-data-{fv}.bin.gz");
    let client = reqwest::blocking::Client::builder()
        .user_agent("edifact-data")
        .build()?;

    let gz_bytes = if let Some(token) = token {
        // Private repo: use GitHub API to resolve asset and download
        download_private_asset(&client, token, version, &asset_name)?
    } else {
        // Public repo: direct download URL
        download_public_asset(&client, version, &asset_name)?
    };

    eprintln!(
        "Downloaded {:.1}MB compressed",
        gz_bytes.len() as f64 / 1_048_576.0
    );

    let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
    let out_path = data_dir.join(format!("edifact-data-{fv}.bin"));
    let mut out_file = std::fs::File::create(&out_path)?;
    let size = std::io::copy(&mut decoder, &mut out_file)?;
    DataBundle::load(&out_path)?; // verify loadable
    Ok(size)
}

fn download_public_asset(
    client: &reqwest::blocking::Client,
    version: &str,
    asset_name: &str,
) -> Result<bytes::Bytes, Box<dyn std::error::Error>> {
    let url = format!("https://github.com/{GITHUB_REPO}/releases/download/v{version}/{asset_name}");
    eprintln!("Downloading {url} ...");
    let response = client
        .get(&url)
        .header("Accept", "application/octet-stream")
        .send()?;
    if !response.status().is_success() {
        return Err(format!(
            "HTTP {}: Is v{version} released with {asset_name}? \
             For private repos, set GITHUB_TOKEN or use --token.",
            response.status()
        )
        .into());
    }
    Ok(response.bytes()?)
}

/// Try to resolve a GitHub token from the `gh` CLI (GitHub CLI).
/// Returns None silently if `gh` is not installed or not authenticated.
fn resolve_gh_token() -> Option<String> {
    std::process::Command::new("gh")
        .args(["auth", "token"])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| {
            let token = String::from_utf8(o.stdout).ok()?.trim().to_string();
            if token.is_empty() {
                None
            } else {
                eprintln!("Using GitHub token from `gh auth token`");
                Some(token)
            }
        })
}

fn download_private_asset(
    client: &reqwest::blocking::Client,
    token: &str,
    version: &str,
    asset_name: &str,
) -> Result<bytes::Bytes, Box<dyn std::error::Error>> {
    // Step 1: Get release by tag to find asset ID
    let release_url =
        format!("https://api.github.com/repos/{GITHUB_REPO}/releases/tags/v{version}");
    eprintln!("Fetching release v{version} from GitHub API ...");
    let release: serde_json::Value = client
        .get(&release_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .send()?
        .error_for_status()?
        .json()?;

    // Step 2: Find the asset by name
    let assets = release["assets"].as_array().ok_or("No assets in release")?;
    let asset = assets
        .iter()
        .find(|a| a["name"].as_str() == Some(asset_name))
        .ok_or_else(|| format!("Asset {asset_name} not found in release v{version}"))?;
    let asset_url = asset["url"].as_str().ok_or("Asset missing url field")?;

    // Step 3: Download the asset binary
    eprintln!("Downloading {asset_name} ...");
    let response = client
        .get(asset_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/octet-stream")
        .send()?
        .error_for_status()?;
    Ok(response.bytes()?)
}
