use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;

use color_eyre::eyre::{Report, Result, WrapErr};
use schemaui::{DocumentFormat, parse_document_str};
use serde_json::Value;

#[derive(Debug)]
pub enum InputSource {
    File(PathBuf),
    Stdin,
}

pub fn load_value(spec: &str, format: DocumentFormat, label: &str) -> Result<Value> {
    if spec == "-" {
        let contents = read_from_source(&InputSource::Stdin)?;
        return parse_contents(&contents, format, label);
    }

    let path = PathBuf::from(spec);
    match read_from_source(&InputSource::File(path.clone())) {
        Ok(contents) => parse_contents(&contents, format, label),
        Err(err) => {
            if is_not_found(&err) {
                let inline_label = format!("inline {label}");
                return parse_contents(spec, format, &inline_label);
            }
            Err(err.wrap_err(format!("failed to load {label} from {}", path.display())))
        }
    }
}

fn read_from_source(source: &InputSource) -> Result<String> {
    match source {
        InputSource::Stdin => {
            let mut buffer = String::new();
            io::stdin()
                .read_to_string(&mut buffer)
                .wrap_err("failed to read from stdin")?;
            Ok(buffer)
        }
        InputSource::File(path) => fs::read_to_string(path)
            .wrap_err_with(|| format!("failed to read file {}", path.display())),
    }
}

fn parse_contents(contents: &str, format: DocumentFormat, label: &str) -> Result<Value> {
    match parse_document_str(contents, format) {
        Ok(value) => Ok(value),
        Err(primary) => {
            for candidate in DocumentFormat::available_formats() {
                if candidate == format {
                    continue;
                }
                if let Ok(value) = parse_document_str(contents, candidate) {
                    return Ok(value);
                }
            }
            Err(Report::msg(format!(
                "failed to parse {label}: tried {} (first error: {primary})",
                format_list()
            )))
        }
    }
}

pub fn is_not_found(err: &Report) -> bool {
    err.downcast_ref::<io::Error>()
        .is_some_and(|io_err| io_err.kind() == io::ErrorKind::NotFound)
}

fn format_list() -> &'static str {
    #[cfg(all(feature = "yaml", feature = "toml"))]
    {
        "JSON/YAML/TOML"
    }
    #[cfg(all(feature = "yaml", not(feature = "toml")))]
    {
        "JSON/YAML"
    }
    #[cfg(all(not(feature = "yaml"), feature = "toml"))]
    {
        "JSON/TOML"
    }
    #[cfg(all(not(feature = "yaml"), not(feature = "toml")))]
    {
        "JSON"
    }
}
