use crate::errors::{CliError, ErrorLocationValue};
use serde::Serialize;
use std::io::{self, Write};

const VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Debug, Serialize)]
struct CliSuccess<T: Serialize> {
    ok: bool,
    biors_version: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    input_hash: Option<String>,
    data: T,
}

#[derive(Debug, Serialize)]
struct CliFailure {
    ok: bool,
    error: CliErrorBody,
}

#[derive(Debug, Serialize)]
struct CliErrorBody {
    code: &'static str,
    message: String,
    location: Option<ErrorLocationValue>,
    #[serde(skip_serializing_if = "Option::is_none")]
    details: Option<serde_json::Value>,
}

pub(crate) fn print_success<T: Serialize>(
    input_hash: Option<String>,
    data: T,
) -> Result<(), CliError> {
    let stdout = io::stdout();
    let mut handle = stdout.lock();
    write_success_to(&mut handle, input_hash, data)?;
    Ok(())
}

fn write_success_to<W: Write, T: Serialize>(
    writer: &mut W,
    input_hash: Option<String>,
    data: T,
) -> Result<(), CliError> {
    let payload = CliSuccess {
        ok: true,
        biors_version: VERSION,
        input_hash,
        data,
    };
    serde_json::to_writer_pretty(&mut *writer, &payload).map_err(CliError::Serialization)?;
    writeln!(writer).map_err(CliError::Write)?;
    Ok(())
}

pub(crate) fn print_json_error(error: CliError) {
    let payload = CliFailure {
        ok: false,
        error: CliErrorBody {
            code: error.code(),
            message: error.to_string(),
            location: error.location(),
            details: error.details().cloned(),
        },
    };
    match to_json(&payload) {
        Ok(json) => println!("{json}"),
        Err(_) => println!(
            r#"{{"ok":false,"error":{{"code":"cli.internal_error","message":"failed to serialize error","location":null}}}}"#
        ),
    }
}

pub(crate) fn print_json_parse_error(error: &clap::Error) {
    let payload = CliFailure {
        ok: false,
        error: CliErrorBody {
            code: "cli.invalid_arguments",
            message: error.to_string(),
            location: None,
            details: None,
        },
    };
    match to_json(&payload) {
        Ok(json) => println!("{json}"),
        Err(_) => println!(
            r#"{{"ok":false,"error":{{"code":"cli.internal_error","message":"failed to serialize error","location":null}}}}"#
        ),
    }
}

fn to_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
    serde_json::to_string_pretty(value)
}

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

    #[test]
    fn write_success_to_serializes_envelope_to_writer() {
        let mut output = Vec::new();

        assert!(
            write_success_to(&mut output, Some("fnv1a64:test".to_string()), vec![1, 2]).is_ok(),
            "write success envelope failed"
        );

        assert!(output.ends_with(b"\n"));
        let value: Value = match serde_json::from_slice(&output) {
            Ok(v) => v,
            Err(e) => panic!("invalid JSON: {e}"),
        };
        assert_eq!(value["ok"], true);
        assert_eq!(value["biors_version"], env!("CARGO_PKG_VERSION"));
        assert_eq!(value["input_hash"], "fnv1a64:test");
        assert_eq!(value["data"], serde_json::json!([1, 2]));
    }
}
