//! Output rendering for the CLI.
//!
//! Two formats:
//!   * **TSV** (default): aligned columns separated by `\t`. Stays
//!     pipe-friendly — `pic apps ls | awk -F'\t' '{print $1}'` works
//!     without parsing box-drawing.
//!   * **JSON**: array of `{column: value, …}` objects (for tables) or
//!     a flat object (for single-row `show`/`whoami`). Designed to be
//!     `jq`-friendly without escaping the table column names.
//!
//! Mode is set globally by the top-level `--output` flag and threaded
//! through every command. Single-row commands (`whoami`, `apps show`)
//! use `KvBlock`; everything plural uses `Table`.

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

use clap::ValueEnum;
use serde_json::{Map, Value};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub enum OutputMode {
    #[default]
    Tsv,
    Json,
}

// ----------------------------------------------------------------------------
// Table — list views (`apps ls`, `scripts ls`, `logs`)
// ----------------------------------------------------------------------------

pub struct Table {
    headers: Vec<String>,
    rows: Vec<Vec<String>>,
}

impl Table {
    pub fn new<I, S>(headers: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self {
            headers: headers.into_iter().map(Into::into).collect(),
            rows: Vec::new(),
        }
    }

    pub fn row<I, S>(&mut self, cells: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.rows.push(cells.into_iter().map(Into::into).collect());
        self
    }

    pub fn render_tsv(&self) -> String {
        let mut widths: Vec<usize> = self.headers.iter().map(String::len).collect();
        for row in &self.rows {
            for (i, cell) in row.iter().enumerate() {
                if i >= widths.len() {
                    widths.push(cell.len());
                } else if cell.len() > widths[i] {
                    widths[i] = cell.len();
                }
            }
        }

        let mut out = String::new();
        write_row(&mut out, &self.headers, &widths);
        for row in &self.rows {
            write_row(&mut out, row, &widths);
        }
        out
    }

    /// JSON form: `[{header: cell, …}, …]`. Cells go in as strings even
    /// when they happen to look like numbers — the CLI doesn't carry
    /// type information all the way through (e.g., `version` is already
    /// `to_string`'d at the call site). Consumers that need typed
    /// numbers should parse `jq -r '.[].version|tonumber'`.
    pub fn render_json(&self) -> String {
        let arr: Vec<Value> = self
            .rows
            .iter()
            .map(|row| {
                let mut obj = Map::new();
                for (i, header) in self.headers.iter().enumerate() {
                    let cell = row.get(i).cloned().unwrap_or_default();
                    obj.insert(header.clone(), Value::String(cell));
                }
                Value::Object(obj)
            })
            .collect();
        serde_json::to_string_pretty(&Value::Array(arr)).unwrap_or_else(|_| "[]".to_string())
    }

    pub fn print(&self, mode: OutputMode) {
        let s = match mode {
            OutputMode::Tsv => self.render_tsv(),
            OutputMode::Json => {
                let mut s = self.render_json();
                s.push('\n');
                s
            }
        };
        // Best-effort write — broken pipe from `| head` etc. shouldn't
        // surface as an error.
        let _ = io::stdout().write_all(s.as_bytes());
    }
}

fn write_row(out: &mut String, row: &[String], widths: &[usize]) {
    for (i, cell) in row.iter().enumerate() {
        if i > 0 {
            out.push('\t');
        }
        out.push_str(cell);
        // Right-pad with spaces so tabs land on the column grid for
        // human readers. Skip on the final column.
        if i + 1 < row.len() {
            let w = widths.get(i).copied().unwrap_or(cell.len());
            for _ in cell.len()..w {
                out.push(' ');
            }
        }
    }
    out.push('\n');
}

// ----------------------------------------------------------------------------
// KvBlock — single-row views (`whoami`, `apps show`)
// ----------------------------------------------------------------------------

/// One row's worth of fields, rendered as aligned `key: value` lines in
/// TSV mode (one line per field — easier on the eye than a 1-row table)
/// or a flat JSON object.
pub struct KvBlock {
    fields: Vec<(String, String)>,
}

impl KvBlock {
    pub fn new() -> Self {
        Self { fields: Vec::new() }
    }

    pub fn field(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        self.fields.push((key.into(), value.into()));
        self
    }

    pub fn render_tsv(&self) -> String {
        let key_width = self.fields.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
        let mut out = String::new();
        for (k, v) in &self.fields {
            out.push_str(k);
            for _ in k.len()..key_width {
                out.push(' ');
            }
            out.push('\t');
            out.push_str(v);
            out.push('\n');
        }
        out
    }

    pub fn render_json(&self) -> String {
        let mut obj = Map::new();
        for (k, v) in &self.fields {
            obj.insert(k.clone(), Value::String(v.clone()));
        }
        serde_json::to_string_pretty(&Value::Object(obj)).unwrap_or_else(|_| "{}".to_string())
    }

    pub fn print(&self, mode: OutputMode) {
        let s = match mode {
            OutputMode::Tsv => self.render_tsv(),
            OutputMode::Json => {
                let mut s = self.render_json();
                s.push('\n');
                s
            }
        };
        let _ = io::stdout().write_all(s.as_bytes());
    }
}

// ----------------------------------------------------------------------------
// Errors
// ----------------------------------------------------------------------------

pub fn print_error(err: &anyhow::Error) {
    let mut stderr = io::stderr();
    let _ = writeln!(stderr, "error: {err:#}");
}

// ----------------------------------------------------------------------------
// Tests
// ----------------------------------------------------------------------------

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

    #[test]
    fn table_aligns_columns_tsv() {
        let mut t = Table::new(["slug", "name"]);
        t.row(["a", "Alpha"]).row(["bravo", "B"]);
        let out = t.render_tsv();
        assert_eq!(out, "slug \tname\na    \tAlpha\nbravo\tB\n");
    }

    #[test]
    fn table_empty_rows_tsv() {
        let t = Table::new(["a", "b"]);
        assert_eq!(t.render_tsv(), "a\tb\n");
    }

    #[test]
    fn table_render_json_is_array_of_objects() {
        let mut t = Table::new(["slug", "name"]);
        t.row(["a", "Alpha"]).row(["bravo", "B"]);
        let raw = t.render_json();
        let v: Value = serde_json::from_str(&raw).expect("valid JSON");
        let arr = v.as_array().expect("array");
        assert_eq!(arr.len(), 2);
        assert_eq!(arr[0]["slug"], "a");
        assert_eq!(arr[0]["name"], "Alpha");
        assert_eq!(arr[1]["slug"], "bravo");
        assert_eq!(arr[1]["name"], "B");
    }

    #[test]
    fn kv_block_tsv_aligns_keys() {
        let mut b = KvBlock::new();
        b.field("username", "admin").field("role", "owner");
        let out = b.render_tsv();
        // username (8 chars) defines the key width.
        assert_eq!(out, "username\tadmin\nrole    \towner\n");
    }

    #[test]
    fn kv_block_json_is_flat_object() {
        let mut b = KvBlock::new();
        b.field("username", "admin").field("role", "owner");
        let raw = b.render_json();
        let v: Value = serde_json::from_str(&raw).expect("valid JSON");
        assert_eq!(v["username"], "admin");
        assert_eq!(v["role"], "owner");
    }
}
