use super::{
    close_array, close_object, write_array_item_prefix, write_json_display, write_json_string,
    write_object_field,
};
use crate::report::{
    AttachmentValue, AttachmentVisit, ContextValue, JsonContext, JsonContextEntry, Report,
    SeverityState,
};
use alloc::vec::Vec;
use core::error::Error;
use core::fmt::{self, Display, Formatter, Write};

pub(super) fn write_context_object<E, State>(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    report: &Report<E, State>,
) -> fmt::Result
where
    E: Error,
    State: SeverityState,
{
    let mut first = true;
    f.write_char('{')?;
    let context = build_json_context(report.context());
    for entry in context.entries {
        write_object_field(f, pretty, depth, &mut first, entry.key.as_ref(), |f| {
            write_context_value(f, pretty, depth + 1, &entry.value)
        })?;
    }
    close_object(f, pretty, depth, first)
}

pub(super) fn write_system_object<E, State>(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    report: &Report<E, State>,
) -> fmt::Result
where
    E: Error,
    State: SeverityState,
{
    let mut first = true;
    f.write_char('{')?;
    let system = build_json_system(report.system());
    for entry in &system.entries {
        write_object_field(f, pretty, depth, &mut first, entry.key.as_ref(), |f| {
            write_context_value(f, pretty, depth + 1, &entry.value)
        })?;
    }
    close_object(f, pretty, depth, first)
}

pub(super) fn write_context_value(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    value: &ContextValue,
) -> fmt::Result {
    match value {
        ContextValue::String(v) => write_kind_and_value(f, pretty, depth, "string", |f| {
            write_json_string(f, v.as_ref())
        }),
        ContextValue::Integer(v) => {
            write_kind_and_value(f, pretty, depth, "integer", |f| write!(f, "{v}"))
        }
        ContextValue::Unsigned(v) => {
            write_kind_and_value(f, pretty, depth, "unsigned", |f| write!(f, "{v}"))
        }
        ContextValue::Float(v) => {
            if !v.is_finite() {
                Err(fmt::Error)
            } else {
                write_kind_and_value(f, pretty, depth, "float", |f| write!(f, "{v}"))
            }
        }
        ContextValue::Bool(v) => {
            write_kind_and_value(f, pretty, depth, "bool", |f| write!(f, "{v}"))
        }
        ContextValue::StringArray(values) => {
            write_context_array(f, pretty, depth, "string_array", values, |f, value| {
                write_json_string(f, value.as_ref())
            })
        }
        ContextValue::IntegerArray(values) => {
            write_context_array(f, pretty, depth, "integer_array", values, |f, value| {
                write!(f, "{value}")
            })
        }
        ContextValue::UnsignedArray(values) => {
            write_context_array(f, pretty, depth, "unsigned_array", values, |f, value| {
                write!(f, "{value}")
            })
        }
        ContextValue::FloatArray(values) => {
            if values.iter().any(|value| !value.is_finite()) {
                return Err(fmt::Error);
            }
            write_context_array(f, pretty, depth, "float_array", values, |f, value| {
                write!(f, "{value}")
            })
        }
        ContextValue::BoolArray(values) => {
            write_context_array(f, pretty, depth, "bool_array", values, |f, value| {
                write!(f, "{value}")
            })
        }
        ContextValue::Redacted { kind, reason } => {
            write_redacted_obj(f, pretty, depth, kind.as_deref(), reason.as_deref())
        }
    }
}

fn write_context_array<T, F>(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    kind: &str,
    values: &[T],
    mut write_value: F,
) -> fmt::Result
where
    F: FnMut(&mut Formatter<'_>, &T) -> fmt::Result,
{
    write_kind_and_value(f, pretty, depth, kind, |f| {
        let mut first = true;
        f.write_char('[')?;
        for value in values {
            write_array_item_prefix(f, pretty, depth + 1, &mut first)?;
            write_value(f, value)?;
        }
        close_array(f, pretty, depth + 1, first)
    })
}

fn build_json_context(context: &crate::report::ContextMap) -> JsonContext {
    let entries: Vec<JsonContextEntry> = context
        .sorted_entries()
        .into_iter()
        .map(|(key, value)| JsonContextEntry {
            key: key.clone(),
            value: value.clone(),
        })
        .collect();
    JsonContext { entries }
}

struct JsonSystem {
    entries: Vec<JsonContextEntry>,
}

fn build_json_system(system: &crate::report::ContextMap) -> JsonSystem {
    JsonSystem {
        entries: build_json_context(system).entries,
    }
}

pub(super) fn write_attachments_array<E, State>(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    report: &Report<E, State>,
) -> fmt::Result
where
    E: Error,
    State: SeverityState,
{
    let mut first = true;
    f.write_char('[')?;
    report.visit_attachments(|item| {
        match item {
            AttachmentVisit::Note { message } => {
                write_array_item_prefix(f, pretty, depth, &mut first)?;
                write_note_obj(f, pretty, depth + 1, message)?;
            }
            AttachmentVisit::Payload {
                name,
                value,
                media_type,
            } => {
                write_array_item_prefix(f, pretty, depth, &mut first)?;
                write_payload_obj(PayloadArgs {
                    f,
                    pretty,
                    depth: depth + 1,
                    name: name.as_ref(),
                    value,
                    media_type: media_type.map(|m| m.as_ref()),
                })?;
            }
        }
        Ok(())
    })?;
    close_array(f, pretty, depth, first)
}

fn write_note_obj(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    message: &(impl Display + ?Sized),
) -> fmt::Result {
    let mut first = true;
    f.write_char('{')?;
    write_object_field(f, pretty, depth, &mut first, "kind", |f| {
        write_json_string(f, "note")
    })?;
    write_object_field(f, pretty, depth, &mut first, "message", |f| {
        write_json_display(f, message)
    })?;
    close_object(f, pretty, depth, first)
}

struct PayloadArgs<'a, 'b> {
    f: &'a mut Formatter<'b>,
    pretty: bool,
    depth: usize,
    name: &'a str,
    value: &'a AttachmentValue,
    media_type: Option<&'a str>,
}

fn write_payload_obj(args: PayloadArgs<'_, '_>) -> fmt::Result {
    let PayloadArgs {
        f,
        pretty,
        depth,
        name,
        value,
        media_type,
    } = args;
    let mut first = true;
    f.write_char('{')?;
    write_object_field(f, pretty, depth, &mut first, "kind", |f| {
        write_json_string(f, "payload")
    })?;
    write_object_field(f, pretty, depth, &mut first, "name", |f| {
        write_json_string(f, name)
    })?;
    write_object_field(f, pretty, depth, &mut first, "value", |f| {
        write_attachment_value(f, pretty, depth + 1, value)
    })?;
    write_object_field(
        f,
        pretty,
        depth,
        &mut first,
        "media_type",
        |f| match media_type {
            Some(media_type) => write_json_string(f, media_type),
            None => f.write_str("null"),
        },
    )?;
    close_object(f, pretty, depth, first)
}

pub(super) fn write_attachment_value(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    value: &AttachmentValue,
) -> fmt::Result {
    if let Some(result) = write_scalar_value(f, pretty, depth, value) {
        return result;
    }

    match value {
        AttachmentValue::String(_)
        | AttachmentValue::Integer(_)
        | AttachmentValue::Unsigned(_)
        | AttachmentValue::Float(_)
        | AttachmentValue::Bool(_) => Err(fmt::Error),
        AttachmentValue::Array(values) => write_kind_and_value(f, pretty, depth, "array", |f| {
            let mut first = true;
            f.write_char('[')?;
            for item in values {
                write_array_item_prefix(f, pretty, depth + 1, &mut first)?;
                write_attachment_value(f, pretty, depth + 2, item)?;
            }
            close_array(f, pretty, depth + 1, first)
        }),
        AttachmentValue::Object(values) => write_kind_and_value(f, pretty, depth, "object", |f| {
            let mut first = true;
            f.write_char('{')?;
            for (key, item) in values.sorted_entries() {
                write_object_field(f, pretty, depth + 1, &mut first, key, |f| {
                    write_attachment_value(f, pretty, depth + 2, item)
                })?;
            }
            close_object(f, pretty, depth + 1, first)
        }),
        AttachmentValue::Bytes(bytes) => write_kind_and_value(f, pretty, depth, "bytes", |f| {
            let mut first = true;
            f.write_char('[')?;
            for byte in bytes {
                write_array_item_prefix(f, pretty, depth + 1, &mut first)?;
                write!(f, "{byte}")?;
            }
            close_array(f, pretty, depth + 1, first)
        }),
        AttachmentValue::Redacted { kind, reason } => {
            write_redacted_obj(f, pretty, depth, kind.as_deref(), reason.as_deref())
        }
    }
}

fn write_redacted_obj(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    kind: Option<&str>,
    reason: Option<&str>,
) -> fmt::Result {
    let mut first = true;
    f.write_char('{')?;
    write_object_field(f, pretty, depth, &mut first, "kind", |f| {
        write_json_string(f, "redacted")
    })?;
    write_object_field(f, pretty, depth, &mut first, "value", |f| {
        let mut inner_first = true;
        f.write_char('{')?;
        if let Some(kind) = kind {
            write_object_field(f, pretty, depth + 1, &mut inner_first, "kind", |f| {
                write_json_string(f, kind)
            })?;
        }
        if let Some(reason) = reason {
            write_object_field(f, pretty, depth + 1, &mut inner_first, "reason", |f| {
                write_json_string(f, reason)
            })?;
        }
        close_object(f, pretty, depth + 1, inner_first)
    })?;
    close_object(f, pretty, depth, first)
}

fn write_scalar_value(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    value: &AttachmentValue,
) -> Option<fmt::Result> {
    match value {
        AttachmentValue::String(v) => Some(write_kind_and_value(f, pretty, depth, "string", |f| {
            write_json_string(f, v.as_ref())
        })),
        AttachmentValue::Integer(v) => {
            Some(write_kind_and_value(f, pretty, depth, "integer", |f| {
                write!(f, "{v}")
            }))
        }
        AttachmentValue::Unsigned(v) => {
            Some(write_kind_and_value(f, pretty, depth, "unsigned", |f| {
                write!(f, "{v}")
            }))
        }
        AttachmentValue::Float(v) => {
            if !v.is_finite() {
                Some(Err(fmt::Error))
            } else {
                Some(write_kind_and_value(f, pretty, depth, "float", |f| {
                    write!(f, "{v}")
                }))
            }
        }
        AttachmentValue::Bool(v) => Some(write_kind_and_value(f, pretty, depth, "bool", |f| {
            write!(f, "{v}")
        })),
        _ => None,
    }
}

fn write_kind_and_value<F>(
    f: &mut Formatter<'_>,
    pretty: bool,
    depth: usize,
    kind: &str,
    mut write_value: F,
) -> fmt::Result
where
    F: FnMut(&mut Formatter<'_>) -> fmt::Result,
{
    let mut first = true;
    f.write_char('{')?;
    write_object_field(f, pretty, depth, &mut first, "kind", |f| {
        write_json_string(f, kind)
    })?;
    write_object_field(f, pretty, depth, &mut first, "value", |f| write_value(f))?;
    close_object(f, pretty, depth, first)
}
