//! Output / envelope layer. The only place bytes reach stdout/stderr (arch §7).
//! Stdout carries data envelopes; stderr carries diagnostics and error envelopes (contracts §1).

pub mod envelope;

use std::io::{IsTerminal, Write};

/// Resolved output structure (contracts §2). Whitespace (pretty/compact) is orthogonal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
    Human,
    Json,
    Ndjson,
    Raw,
}

/// Format precedence (contracts §2): explicit flag > EXA_OUTPUT > auto (TTY=human, pipe=json, D3).
pub fn resolve_mode(
    explicit: Option<OutputMode>,
    env_output: Option<&str>,
    stdout_is_tty: bool,
) -> OutputMode {
    if let Some(m) = explicit {
        return m;
    }
    match env_output {
        Some("human") => OutputMode::Human,
        Some("json") => OutputMode::Json,
        Some("ndjson") => OutputMode::Ndjson,
        _ => {
            if stdout_is_tty {
                OutputMode::Human
            } else {
                OutputMode::Json
            }
        }
    }
}

pub fn stdout_is_tty() -> bool {
    std::io::stdout().is_terminal()
}

/// Serialize a value to stdout in the chosen mode (pretty when `pretty`).
pub fn emit_stdout(value: &serde_json::Value, pretty: bool) {
    let s = if pretty {
        serde_json::to_string_pretty(value).unwrap_or_default()
    } else {
        serde_json::to_string(value).unwrap_or_default()
    };
    println!("{s}");
}

/// Write a JSON value plus trailing newline to a fallible writer.
pub fn write_stdout_value(
    out: &mut impl Write,
    value: &serde_json::Value,
    pretty: bool,
) -> std::io::Result<()> {
    let s = if pretty {
        serde_json::to_string_pretty(value).unwrap_or_default()
    } else {
        serde_json::to_string(value).unwrap_or_default()
    };
    out.write_all(s.as_bytes())?;
    out.write_all(b"\n")?;
    out.flush()
}

/// Emit one compact JSON value as an NDJSON line.
pub fn emit_ndjson(value: &serde_json::Value) {
    println!("{}", serde_json::to_string(value).unwrap_or_default());
}

/// Write one compact JSON value as an NDJSON line to a fallible writer.
pub fn write_ndjson(out: &mut impl Write, value: &serde_json::Value) -> std::io::Result<()> {
    out.write_all(serde_json::to_string(value).unwrap_or_default().as_bytes())?;
    out.write_all(b"\n")?;
    out.flush()
}

/// Emit exact upstream response bytes (`--raw`, contracts §2).
pub fn emit_raw(body: &[u8]) -> std::io::Result<()> {
    let mut out = std::io::stdout().lock();
    write_raw(&mut out, body)
}

pub fn write_raw(out: &mut impl std::io::Write, body: &[u8]) -> std::io::Result<()> {
    out.write_all(body)?;
    out.flush()
}

#[cfg(test)]
mod tests {
    #[test]
    fn write_raw_does_not_append_newline() {
        let mut out = Vec::new();
        super::write_raw(&mut out, b"{\"ok\":true}").unwrap();
        assert_eq!(out, br#"{"ok":true}"#);
    }

    #[test]
    fn write_ndjson_appends_one_newline() {
        let mut out = Vec::new();
        super::write_ndjson(&mut out, &serde_json::json!({"ok":true})).unwrap();
        assert_eq!(out, b"{\"ok\":true}\n");
    }

    #[test]
    fn write_stdout_value_honors_pretty_flag() {
        let mut out = Vec::new();
        super::write_stdout_value(&mut out, &serde_json::json!({"ok":true}), true).unwrap();
        assert_eq!(out, b"{\n  \"ok\": true\n}\n");
    }
}
