// Copyright (c) 2025 Adam Ulichny
//
// This source code is licensed under the MIT OR Apache-2.0 license
// that can be found in the LICENSE-MIT or LICENSE-APACHE files
// at the root of this source tree.

use clap::{Parser, Subcommand};
use powerlaw::{dist, util};
use std::path::PathBuf;

#[derive(Parser)]
#[command(version, about = "A CLI tool for analyzing powerlaw distributed data.", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Fit a power-law distribution and find the best parameters (x_min, alpha)
    Fit {
        /// The path to the input data file (one value per line).
        #[arg(required = true)]
        filepath: PathBuf,
    },
    /// Test the hypothesis that the data follows a power-law distribution
    Test {
        /// The path to the input data file (one value per line).
        #[arg(required = true)]
        filepath: PathBuf,

        /// Sets the desired precision for the p-value calculation.
        #[arg(short, long, required = true)]
        precision: f64,
    },
}

fn main() {
    let cli = Cli::parse();

    match cli.command {
        Commands::Fit { filepath } => {
            run_analysis(filepath, None);
        }
        Commands::Test {
            filepath,
            precision,
        } => {
            run_analysis(filepath, Some(precision));
        }
    }
}

/// Runs the core pareto type 1 fit, with an optional hypothesis test.
fn run_analysis(filepath: PathBuf, precision: Option<f64>) {
    let file_path_str = filepath.to_str().unwrap_or("invalid path");

    // read data from csv into vector
    let mut data = util::read_csv(file_path_str).expect("Should be able to read the CSV file");

    println!("Data: {}", file_path_str);
    if let Some(p) = precision {
        println!("Precision: {}", p);
    }
    println!("n: {}", data.len());

    // Step 1: test finding the MLE alphas for a given range of x_min and the data (sec 3.1)
    let alphas = dist::pareto::find_alphas_fast(&mut data);

    // Step 2: gof KS test the cdf of the proposed x_min and alpha hat vs the sample with x >= x_min. (Sec 3.3)
    // this is to find the best x_min/alpha pair given the data

    let best_fit = dist::pareto::gof(&data, &alphas.0, &alphas.1);
    println!(
        "Pareto Type I parameters - alpha: {:?} x_min {:?} KS stat: {:?} tail length: {:?}",
        best_fit.alpha, best_fit.x_min, best_fit.D, best_fit.len_tail
    );
    println!("Generic Power-Law [Cx^(-alpha)] parameters - alpha: {:?} x_min {:?} KS stat: {:?} tail length: {:?}", best_fit.alpha + 1. , best_fit.x_min, best_fit.D, best_fit.len_tail);

    if let Some(prec) = precision {
        //calculate uncertainly of the estimates by generating synthetic datasets and finding the best fit for them (sec 3.4)
        // this needs more work
        println!("Calculating the degree of uncertainty of the parameters...");
        let (xm, a) = dist::pareto::estimation::param_est(&data, 100);
        println!("x_min std: {} alpha std: {}", xm, a);

        // Test the hypothesis that the sample data could have been generated by a pareto process
        // Section 4.1 from Clauset et al.
        // this one is super slow
        println!("Testing the hypothesis that a Power-Law is a plausible fit to the data...");
        let h_0 =
            dist::pareto::hypothesis_test(data, prec, best_fit.alpha, best_fit.x_min, best_fit.D);
        println!(
            "Qty of sims with KS statistic > empirical data {:?}",
            h_0.gt
        );
        println!("Total sims {:?}", h_0.total);
        println!("p-value: {:?}", h_0.p);

        // 0.1 was proposed by Clauset et al. as the level of significance by which to accept or reject H_0.
        if h_0.p <= 0.1 {
            // reject the null H0 that the distributions are the same.
            println!("Power-Law distribution is unlikely to be a plausible fit to the data.")
        } else {
            // fail to reject the null H0 that the distributions are the same.
            println!("Power-Law distribution is a plausible fit to the data.")
        }
    }
}

fn parse_config(args: &[String]) -> (f64, &str) {
    if args.len() != 3 {
        eprintln!("Usage: {} <precision> <filepath>", args[0]);
        std::process::exit(1);
    }

    // first param
    let prec: f64 = args[1].parse().unwrap_or_else(|_| {
        eprintln!("Error: The first argument must be a valid floating-point number (f64).");
        std::process::exit(1);
    });

    // second param
    let filepath = &args[2];

    (prec, filepath)
}
