use fp_checker_core::{
    check_code as run_check_code, check_path as run_check_path, discover_config,
    load_config_from_path, render_report, CheckerConfig, OutputFormat, RuleId, Severity,
};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use std::collections::BTreeSet;
use std::path::PathBuf;

struct PythonPathOptions<'a> {
    format: Option<&'a str>,
    fail_on: Option<&'a str>,
    include: Option<Vec<String>>,
    exclude: Option<Vec<String>>,
    rule: Option<Vec<String>>,
    ignore_decorator_name: Option<String>,
    config: Option<String>,
}

#[pyfunction]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
    path,
    format = None,
    fail_on = None,
    include = None,
    exclude = None,
    rule = None,
    ignore_decorator_name = None,
    config = None
))]
fn check_path(
    path: &str,
    format: Option<&str>,
    fail_on: Option<&str>,
    include: Option<Vec<String>>,
    exclude: Option<Vec<String>>,
    rule: Option<Vec<String>>,
    ignore_decorator_name: Option<String>,
    config: Option<String>,
) -> PyResult<String> {
    let path_buf = PathBuf::from(path);
    let config = build_config(
        &path_buf,
        PythonPathOptions {
            format,
            fail_on,
            include,
            exclude,
            rule,
            ignore_decorator_name,
            config,
        },
    )?;
    let report = run_check_path(&path_buf, &config).map_err(|error| to_runtime_error(&error))?;
    render_report(&report, config.format).map_err(|error| to_runtime_error(&error))
}

#[pyfunction]
#[pyo3(signature = (
    source,
    format = "json",
    fail_on = "warning",
    rule = None,
    ignore_decorator_name = None
))]
fn check_code(
    source: &str,
    format: &str,
    fail_on: &str,
    rule: Option<Vec<String>>,
    ignore_decorator_name: Option<String>,
) -> PyResult<String> {
    let mut config = CheckerConfig {
        format: format
            .parse::<OutputFormat>()
            .map_err(|error| to_value_error(&error))?,
        fail_on: fail_on
            .parse::<Severity>()
            .map_err(|error| to_value_error(&error))?,
        ..CheckerConfig::default()
    };
    if let Some(rule) = rule {
        config.enabled_rules = Some(parse_rules(rule)?);
    }
    if let Some(ignore_decorator_name) = ignore_decorator_name {
        config.ignore_decorator_name = ignore_decorator_name;
    }

    let report = run_check_code(source, "<memory>", &config);
    render_report(&report, config.format).map_err(|error| to_runtime_error(&error))
}

#[pyfunction]
fn available_rules() -> Vec<String> {
    RuleId::all()
        .into_iter()
        .map(|rule_id| rule_id.as_str().to_owned())
        .collect()
}

#[pymodule]
fn fp_checker(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
    module.add_function(wrap_pyfunction!(check_path, module)?)?;
    module.add_function(wrap_pyfunction!(check_code, module)?)?;
    module.add_function(wrap_pyfunction!(available_rules, module)?)?;
    Ok(())
}

fn build_config(target_path: &PathBuf, options: PythonPathOptions<'_>) -> PyResult<CheckerConfig> {
    let config_path = options
        .config
        .map(PathBuf::from)
        .or_else(|| discover_config(target_path));
    let mut checker_config = if let Some(config_path) = config_path {
        load_config_from_path(&config_path).map_err(|error| to_runtime_error(&error))?
    } else {
        CheckerConfig::default()
    };
    if let Some(format) = options.format {
        checker_config.format = format
            .parse::<OutputFormat>()
            .map_err(|error| to_value_error(&error))?;
    }
    if let Some(fail_on) = options.fail_on {
        checker_config.fail_on = fail_on
            .parse::<Severity>()
            .map_err(|error| to_value_error(&error))?;
    }
    if let Some(include) = options.include {
        checker_config.include = include;
    }
    if let Some(exclude) = options.exclude {
        checker_config.exclude = exclude;
    }
    if let Some(rule) = options.rule {
        checker_config.enabled_rules = Some(parse_rules(rule)?);
    }
    if let Some(ignore_decorator_name) = options.ignore_decorator_name {
        checker_config.ignore_decorator_name = ignore_decorator_name;
    }
    checker_config
        .validate()
        .map_err(|error| to_value_error(&error))?;
    Ok(checker_config)
}

fn parse_rules(rules: Vec<String>) -> PyResult<BTreeSet<RuleId>> {
    rules
        .into_iter()
        .map(|rule| {
            rule.parse::<RuleId>()
                .map_err(|error| to_value_error(&error))
        })
        .collect()
}

fn to_value_error(error: &impl ToString) -> PyErr {
    PyValueError::new_err(error.to_string())
}

fn to_runtime_error(error: &impl ToString) -> PyErr {
    PyRuntimeError::new_err(error.to_string())
}
