//! Helper items for printing text to a terminal.

use std::fmt::{self, Write as _};

use console::{measure_text_width, style};
use ocpi_tariffs::{
    json::{Component, Path},
    price::TariffReport,
    timezone, warning, Warning,
};
use tracing::error;

/// A general purpose horizontal break.
const LINE: &str = "----------------------------------------------------------------";
/// The initial amount of memory allocated for writing out the table.
const TABLE_BUF_LEN: usize = 4096;

/// A helper for printing `Option<T>` without creating a `String`.
pub struct Optional<T>(pub Option<T>)
where
    T: fmt::Display;

impl<T> fmt::Display for Optional<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            Some(v) => fmt::Display::fmt(v, f),
            None => f.write_str("-"),
        }
    }
}

/// Renders a [`Path`] with each component styled independently: dim delimiters,
/// cyan object members and yellow array indices.
///
/// `console` disables the ANSI codes automatically when the stream is not a
/// terminal, so the output stays plain when redirected.
pub struct DisplayPath<'a>(pub &'a Path);

impl fmt::Display for DisplayPath<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", style('$').dim())?;

        for component in self.0.components() {
            match component {
                Component::Member(name) => {
                    write!(f, "{}{}", style('.').dim(), style(name).cyan())?;
                }
                Component::Index(index) => {
                    write!(
                        f,
                        "{}{}{}",
                        style('[').dim(),
                        style(index).yellow(),
                        style(']').dim(),
                    )?;
                }
            }
        }

        Ok(())
    }
}

/// Print the error produced by a call to `ocpi-tariffs::timezone::find_or_infer`.
pub fn timezone_error(error: &warning::Error<timezone::Warning>) {
    eprintln!(
        "{}: Unable to find timezone due to error at path `{}`: {}",
        style("ERR").red(),
        DisplayPath(&error.element().path),
        error.warning()
    );
}

/// Print a set of warnings to `stderr`, one per line with its source path.
///
/// `feature` names the operation the warnings came from (for example `"pricing the CDR"` or
/// `"the timezone search"`); it appears in the header `N warnings from {feature}`. Nothing is
/// printed when the set is empty.
pub fn warning_set<W: Warning>(feature: &'static str, warnings: &warning::Set<W>) {
    if warnings.is_empty() {
        return;
    }

    eprintln!(
        "{}: {} warnings from {}",
        style("WARN").yellow(),
        warnings.len_warnings(),
        feature
    );

    for group in warnings {
        let (element, warnings) = group.to_parts();
        for warning in warnings {
            eprintln!(
                "  - path: {}: {}",
                DisplayPath(&element.path),
                style(warning).yellow()
            );
        }
    }

    let line = style(LINE).yellow();
    eprintln!("{line}");
}

/// Print the unexpected fields of a list of tariffs to `stderr`.
pub fn tariff_reports(reports: &[TariffReport]) {
    if reports.iter().all(|report| report.warnings.is_empty()) {
        return;
    }

    let line = style(LINE).yellow();

    eprintln!("{}: warnings found in tariffs", style("WARN").yellow());

    for report in reports {
        let TariffReport { origin, warnings } = report;

        if warnings.is_empty() {
            continue;
        }

        eprintln!(
            "{}: {} warnings from tariff with id: {}",
            style("WARN").yellow(),
            warnings.len(),
            style(&origin.id).yellow(),
        );

        for (elem_path, warnings) in warnings {
            eprintln!("  {}", DisplayPath(elem_path));

            for warning in warnings {
                eprintln!("  - {}", style(warning).yellow());
            }
        }

        eprintln!("{line}");
    }

    eprintln!("{line}");
}

/// A helper for printing tables with fixed width cols.
pub struct Table {
    /// The widths given in the `header` fn.
    widths: Vec<usize>,

    /// The table is written into this buffer.
    buf: String,
}

/// The column of a table.
pub struct Col<'caller> {
    /// The label to display for this column.
    pub label: &'caller dyn fmt::Display,

    /// The width this column should occupy.
    pub width: usize,
}

impl Col<'_> {
    /// Create a `Col` without a name.
    pub fn empty(width: usize) -> Self {
        Self { label: &"", width }
    }
}

impl Table {
    /// Print the table header and use the column widths for all the following rows.
    pub fn header(header: &[Col<'_>]) -> Self {
        let widths = header
            .iter()
            .map(|Col { label: _, width }| *width)
            .collect::<Vec<_>>();
        let mut buf = String::with_capacity(TABLE_BUF_LEN);
        let labels = header
            .iter()
            .map(|Col { label, width: _ }| *label)
            .collect::<Vec<_>>();

        print_table_line(&mut buf, &widths);
        print_table_row(&mut buf, &widths, &labels);
        print_table_line(&mut buf, &widths);

        Self { widths, buf }
    }

    /// Print a separating line.
    pub fn print_line(&mut self) {
        print_table_line(&mut self.buf, &self.widths);
    }

    /// Print a single row of values.
    pub fn print_row(&mut self, values: &[&dyn fmt::Display]) {
        print_table_row(&mut self.buf, &self.widths, values);
    }

    /// Print a single row with a label stylized based of the validity.
    ///
    /// If the row represents a valid value, the label is colored green.
    /// Otherwise, the label is colored red.
    pub fn print_valid_row(
        &mut self,
        is_valid: bool,
        label: &'static str,
        values: &[&dyn fmt::Display],
    ) {
        let label = if is_valid {
            style(label).green()
        } else {
            style(label).red()
        };
        print_table_row_with_label(&mut self.buf, &self.widths, &label, values);
    }

    /// Print the bottom line of the table and return the buffer for printing.
    pub fn finish(self) -> String {
        let Self { widths, mut buf } = self;
        print_table_line(&mut buf, &widths);
        buf
    }
}

/// Just like the std lib `write!` macro except that it suppresses in `fmt::Result`.
///
/// This should only be used if you are in control of the buffer you're writing to
/// and the only way it can fail is if the OS allocator fails.
///
/// * See: <https://doc.rust-lang.org/std/io/trait.Write.html#method.write_fmt>.
#[macro_export]
macro_rules! write_or {
    ($dst:expr, $($arg:tt)*) => {{
        let _ignore_result = $dst.write_fmt(std::format_args!($($arg)*));
    }};
}

/// Print a separation line for a table.
fn print_table_line(buf: &mut String, widths: &[usize]) {
    write_or!(buf, "+");

    for width in widths {
        write_or!(
            buf,
            "{0:->1$}+",
            "",
            width.checked_add(2).unwrap_or_default()
        );
    }

    write_or!(buf, "\n");
}

/// Print a single row to the buffer with a label.
fn print_table_row(buf: &mut String, widths: &[usize], values: &[&dyn fmt::Display]) {
    assert_eq!(
        widths.len(),
        values.len(),
        "The widths and values amounts should be the same"
    );
    print_table_row_(buf, widths, values, None);
}

/// Print a single row to the buffer with a distinct label.
///
/// This fn is used to create a row with a stylized label.
fn print_table_row_with_label(
    buf: &mut String,
    widths: &[usize],
    label: &dyn fmt::Display,
    values: &[&dyn fmt::Display],
) {
    print_table_row_(buf, widths, values, Some(label));
}

/// Print a single row to the buffer.
fn print_table_row_(
    buf: &mut String,
    widths: &[usize],
    values: &[&dyn fmt::Display],
    label: Option<&dyn fmt::Display>,
) {
    write_or!(buf, "|");

    if let Some(label) = label {
        let mut widths = widths.iter();
        let Some(width) = widths.next() else {
            return;
        };
        print_col(buf, label, *width);

        for (value, width) in values.iter().zip(widths) {
            print_col(buf, *value, *width);
        }
    } else {
        for (value, width) in values.iter().zip(widths) {
            print_col(buf, *value, *width);
        }
    }

    write_or!(buf, "\n");
}

/// Print a single column to the buffer.
fn print_col(buf: &mut String, value: &dyn fmt::Display, width: usize) {
    write_or!(buf, " ");

    // The value could contain ANSI escape codes and the `Display` impl of the type
    // may not implement fill and alignment logic. So we need to implement left-aligned text ourselves.
    let len_before = buf.len();
    write_or!(buf, "{value}");
    let len_after = buf.len();

    // Use the length before and after to capture the str just written.
    // And compute it's visible length in the terminal.
    let Some(s) = &buf.get(len_before..len_after) else {
        error!("Non UTF8 values were written as a column value");
        return;
    };

    let len = measure_text_width(s);
    // Calculate the padding we need to apply at the end of the str.
    let padding = width.saturating_sub(len);

    // and apply the padding
    for _ in 0..padding {
        write_or!(buf, " ");
    }

    write_or!(buf, " |");
}
