use std::env;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;

use dotenvy::dotenv;

use crate::model::{LayoutConfig, RingDistribution, SvgConfig};

const INPUT_DIR_KEY: &str = "RADIAL_INPUT_DIR";
const INPUT_FILE_KEY: &str = "RADIAL_INPUT_FILE";
const ROOT_CLASS_IRI_KEY: &str = "RADIAL_ROOT_CLASS_IRI";
const OUTPUT_DIR_KEY: &str = "RADIAL_OUTPUT_DIR";
const OUTPUT_FILE_KEY: &str = "RADIAL_OUTPUT_FILE";
const SVG_SHORTEST_EDGES_KEY: &str = "RADIAL_SVG_SHORTEST_EDGES";
const SVG_SHOW_LABELS_KEY: &str = "RADIAL_SVG_SHOW_LABELS";
const MIN_RADIUS_KEY: &str = "RADIAL_MIN_RADIUS";
const LEVEL_DISTANCE_KEY: &str = "RADIAL_LEVEL_DISTANCE";
const ALIGN_POSITIVE_KEY: &str = "RADIAL_ALIGN_POSITIVE_COORDS";
const SPIRAL_QUALITY_KEY: &str = "RADIAL_SPIRAL_QUALITY";
const LEFT_BORDER_KEY: &str = "RADIAL_LEFT_BORDER";
const UPPER_BORDER_KEY: &str = "RADIAL_UPPER_BORDER";
const NODE_DISTANCE_KEY: &str = "RADIAL_NODE_DISTANCE";
const RING_DISTRIBUTION_KEY: &str = "RADIAL_RING_DISTRIBUTION";
const DEFAULT_OUTPUT_DIR: &str = "./out";
const DEFAULT_OUTPUT_FILE: &str = "layout.svg";
const DEFAULT_ROOT_CLASS_IRI: &str = "http://purl.obolibrary.org/obo/BFO_0000001";

#[derive(Debug, Clone, PartialEq)]
pub struct EnvConfig {
    pub input_dir: PathBuf,
    pub input_file: PathBuf,
    pub root_class_iri: String,
    pub output_dir: PathBuf,
    pub output_file: PathBuf,
    pub layout: LayoutConfig,
    pub svg: SvgConfig,
}

impl EnvConfig {
    pub fn from_env() -> Result<Self, EnvConfigError> {
        let _ = dotenv();
        Self::from_lookup(|key| env::var(key).ok())
    }

    pub fn input_path(&self) -> PathBuf {
        self.input_dir.join(&self.input_file)
    }

    pub fn output_path(&self) -> PathBuf {
        self.output_dir.join(&self.output_file)
    }

    fn from_lookup<F>(mut lookup: F) -> Result<Self, EnvConfigError>
    where
        F: FnMut(&str) -> Option<String>,
    {
        let defaults = LayoutConfig::default();
        let input_dir = PathBuf::from(require_var(&mut lookup, INPUT_DIR_KEY)?);
        let input_file = PathBuf::from(require_var(&mut lookup, INPUT_FILE_KEY)?);
        let root_class_iri =
            lookup(ROOT_CLASS_IRI_KEY).unwrap_or_else(|| DEFAULT_ROOT_CLASS_IRI.to_owned());
        let output_dir =
            PathBuf::from(lookup(OUTPUT_DIR_KEY).unwrap_or_else(|| DEFAULT_OUTPUT_DIR.to_owned()));
        let output_file = PathBuf::from(
            lookup(OUTPUT_FILE_KEY).unwrap_or_else(|| DEFAULT_OUTPUT_FILE.to_owned()),
        );
        let layout = LayoutConfig {
            min_radius: parse_f64(&mut lookup, MIN_RADIUS_KEY, defaults.min_radius)?,
            level_distance: parse_f64(&mut lookup, LEVEL_DISTANCE_KEY, defaults.level_distance)?,
            align_positive_coords: parse_bool(
                &mut lookup,
                ALIGN_POSITIVE_KEY,
                defaults.align_positive_coords,
            )?,
            spiral_quality: parse_usize(&mut lookup, SPIRAL_QUALITY_KEY, defaults.spiral_quality)?,
            left_border: parse_f64(&mut lookup, LEFT_BORDER_KEY, defaults.left_border)?,
            upper_border: parse_f64(&mut lookup, UPPER_BORDER_KEY, defaults.upper_border)?,
            node_distance: parse_f64(&mut lookup, NODE_DISTANCE_KEY, defaults.node_distance)?,
            ring_distribution: parse_ring_distribution(
                &mut lookup,
                RING_DISTRIBUTION_KEY,
                defaults.ring_distribution,
            )?,
        };
        let svg = SvgConfig {
            shortest_edges: parse_bool(
                &mut lookup,
                SVG_SHORTEST_EDGES_KEY,
                SvgConfig::default().shortest_edges,
            )?,
            show_labels: parse_bool(
                &mut lookup,
                SVG_SHOW_LABELS_KEY,
                SvgConfig::default().show_labels,
            )?,
        };

        Ok(Self {
            input_dir,
            input_file,
            root_class_iri,
            output_dir,
            output_file,
            layout,
            svg,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EnvConfigError {
    MissingVar(&'static str),
    InvalidFloat { key: &'static str, value: String },
    InvalidUsize { key: &'static str, value: String },
    InvalidBool { key: &'static str, value: String },
    InvalidRingDistribution { key: &'static str, value: String },
}

impl Display for EnvConfigError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            EnvConfigError::MissingVar(key) => write!(f, "missing required environment variable {key}"),
            EnvConfigError::InvalidFloat { key, value } => {
                write!(f, "environment variable {key} must be a float, got {value}")
            }
            EnvConfigError::InvalidUsize { key, value } => {
                write!(f, "environment variable {key} must be a non-negative integer, got {value}")
            }
            EnvConfigError::InvalidBool { key, value } => {
                write!(f, "environment variable {key} must be a boolean, got {value}")
            }
            EnvConfigError::InvalidRingDistribution { key, value } => write!(
                f,
                "environment variable {key} must be 'packed', 'distributed', or 'adaptive', got {value}"
            ),
        }
    }
}

impl Error for EnvConfigError {}

fn require_var<F>(lookup: &mut F, key: &'static str) -> Result<String, EnvConfigError>
where
    F: FnMut(&str) -> Option<String>,
{
    lookup(key).ok_or(EnvConfigError::MissingVar(key))
}

fn parse_f64<F>(lookup: &mut F, key: &'static str, default: f64) -> Result<f64, EnvConfigError>
where
    F: FnMut(&str) -> Option<String>,
{
    match lookup(key) {
        Some(value) => value
            .parse::<f64>()
            .map_err(|_| EnvConfigError::InvalidFloat { key, value }),
        None => Ok(default),
    }
}

fn parse_usize<F>(
    lookup: &mut F,
    key: &'static str,
    default: usize,
) -> Result<usize, EnvConfigError>
where
    F: FnMut(&str) -> Option<String>,
{
    match lookup(key) {
        Some(value) => value
            .parse::<usize>()
            .map_err(|_| EnvConfigError::InvalidUsize { key, value }),
        None => Ok(default),
    }
}

fn parse_bool<F>(lookup: &mut F, key: &'static str, default: bool) -> Result<bool, EnvConfigError>
where
    F: FnMut(&str) -> Option<String>,
{
    match lookup(key) {
        Some(value) => match value.to_ascii_lowercase().as_str() {
            "true" | "1" | "yes" | "on" => Ok(true),
            "false" | "0" | "no" | "off" => Ok(false),
            _ => Err(EnvConfigError::InvalidBool { key, value }),
        },
        None => Ok(default),
    }
}

fn parse_ring_distribution<F>(
    lookup: &mut F,
    key: &'static str,
    default: RingDistribution,
) -> Result<RingDistribution, EnvConfigError>
where
    F: FnMut(&str) -> Option<String>,
{
    match lookup(key) {
        Some(value) => match value.to_ascii_lowercase().as_str() {
            "packed" => Ok(RingDistribution::Packed),
            "distributed" => Ok(RingDistribution::Distributed),
            "adaptive" => Ok(RingDistribution::Adaptive),
            _ => Err(EnvConfigError::InvalidRingDistribution { key, value }),
        },
        None => Ok(default),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn config_from_map(entries: &[(&str, &str)]) -> Result<EnvConfig, EnvConfigError> {
        let vars = entries
            .iter()
            .map(|(key, value)| ((*key).to_owned(), (*value).to_owned()))
            .collect::<std::collections::HashMap<_, _>>();
        EnvConfig::from_lookup(|key| vars.get(key).cloned())
    }

    #[test]
    fn parses_env_config_and_path() {
        let config = config_from_map(&[
            (INPUT_DIR_KEY, "./ttl"),
            (INPUT_FILE_KEY, "ontology.ttl"),
            (ROOT_CLASS_IRI_KEY, "http://example.com/root"),
            (OUTPUT_DIR_KEY, "./svg"),
            (OUTPUT_FILE_KEY, "graph.svg"),
            (SVG_SHORTEST_EDGES_KEY, "true"),
            (SVG_SHOW_LABELS_KEY, "false"),
            (MIN_RADIUS_KEY, "2.5"),
            (LEVEL_DISTANCE_KEY, "3.0"),
            (ALIGN_POSITIVE_KEY, "false"),
            (SPIRAL_QUALITY_KEY, "800"),
            (LEFT_BORDER_KEY, "120.0"),
            (UPPER_BORDER_KEY, "140.0"),
            (NODE_DISTANCE_KEY, "90.0"),
            (RING_DISTRIBUTION_KEY, "adaptive"),
        ])
        .unwrap();

        assert_eq!(
            config.input_path(),
            PathBuf::from("./ttl").join("ontology.ttl")
        );
        assert_eq!(config.root_class_iri, "http://example.com/root");
        assert_eq!(
            config.output_path(),
            PathBuf::from("./svg").join("graph.svg")
        );
        assert_eq!(config.layout.min_radius, 2.5);
        assert_eq!(config.layout.level_distance, 3.0);
        assert!(!config.layout.align_positive_coords);
        assert_eq!(config.layout.spiral_quality, 800);
        assert_eq!(config.layout.left_border, 120.0);
        assert_eq!(config.layout.upper_border, 140.0);
        assert_eq!(config.layout.node_distance, 90.0);
        assert_eq!(config.layout.ring_distribution, RingDistribution::Adaptive);
        assert!(config.svg.shortest_edges);
        assert!(!config.svg.show_labels);
    }

    #[test]
    fn missing_input_file_is_reported() {
        let error = config_from_map(&[(INPUT_DIR_KEY, "./ttl")]).unwrap_err();
        assert_eq!(error, EnvConfigError::MissingVar(INPUT_FILE_KEY));
    }

    #[test]
    fn invalid_boolean_is_reported() {
        let error = config_from_map(&[
            (INPUT_DIR_KEY, "./ttl"),
            (INPUT_FILE_KEY, "ontology.ttl"),
            (ALIGN_POSITIVE_KEY, "maybe"),
        ])
        .unwrap_err();

        assert_eq!(
            error,
            EnvConfigError::InvalidBool {
                key: ALIGN_POSITIVE_KEY,
                value: "maybe".to_owned(),
            }
        );
    }

    #[test]
    fn uses_default_output_location_when_not_provided() {
        let config =
            config_from_map(&[(INPUT_DIR_KEY, "./ttl"), (INPUT_FILE_KEY, "ontology.ttl")]).unwrap();

        assert_eq!(
            config.root_class_iri,
            "http://purl.obolibrary.org/obo/BFO_0000001"
        );
        assert_eq!(
            config.output_path(),
            PathBuf::from("./out").join("layout.svg")
        );
        assert!(!config.svg.shortest_edges);
        assert!(config.svg.show_labels);
        assert_eq!(config.layout.ring_distribution, RingDistribution::Packed);
    }

    #[test]
    fn invalid_ring_distribution_is_reported() {
        let error = config_from_map(&[
            (INPUT_DIR_KEY, "./ttl"),
            (INPUT_FILE_KEY, "ontology.ttl"),
            (RING_DISTRIBUTION_KEY, "arc"),
        ])
        .unwrap_err();

        assert_eq!(
            error,
            EnvConfigError::InvalidRingDistribution {
                key: RING_DISTRIBUTION_KEY,
                value: "arc".to_owned(),
            }
        );
    }
}
