use crate::writers::DisplayExt;
use backtrace::Backtrace;
use std::{fmt, panic::Location};
#[cfg(feature = "capture-spantrace")]
use tracing_error::SpanTrace;
use url::Url;

type Display<'a> = Box<dyn std::fmt::Display + Send + Sync + 'a>;

pub(crate) struct IssueSection<'a> {
    url: &'a str,
    msg: &'a str,
    location: Option<&'a Location<'a>>,
    backtrace: Option<&'a Backtrace>,
    #[cfg(feature = "capture-spantrace")]
    span_trace: Option<&'a SpanTrace>,
    metadata: &'a [(String, Display<'a>)],
}

impl<'a> IssueSection<'a> {
    pub(crate) fn new(url: &'a str, msg: &'a str) -> Self {
        IssueSection {
            url,
            msg,
            location: None,
            backtrace: None,
            #[cfg(feature = "capture-spantrace")]
            span_trace: None,
            metadata: &[],
        }
    }

    pub(crate) fn with_location(mut self, location: impl Into<Option<&'a Location<'a>>>) -> Self {
        self.location = location.into();
        self
    }

    pub(crate) fn with_backtrace(mut self, backtrace: impl Into<Option<&'a Backtrace>>) -> Self {
        self.backtrace = backtrace.into();
        self
    }

    #[cfg(feature = "capture-spantrace")]
    pub(crate) fn with_span_trace(mut self, span_trace: impl Into<Option<&'a SpanTrace>>) -> Self {
        self.span_trace = span_trace.into();
        self
    }

    pub(crate) fn with_metadata(mut self, metadata: &'a [(String, Display<'a>)]) -> Self {
        self.metadata = metadata;
        self
    }
}

impl fmt::Display for IssueSection<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let location = self
            .location
            .map(|loc| ("location".to_string(), Box::new(loc) as _));
        let metadata = self.metadata.iter().chain(location.as_ref());
        let metadata = MetadataSection { metadata }.to_string();
        let mut body = Body::new();
        body.push_section("Error", ConsoleSection(self.msg))?;

        if !self.metadata.is_empty() {
            body.push_section("Metadata", metadata)?;
        }

        #[cfg(feature = "capture-spantrace")]
        if let Some(st) = self.span_trace {
            body.push_section(
                "SpanTrace",
                Collapsed(ConsoleSection(st.with_header("SpanTrace:\n"))),
            )?;
        }

        if let Some(bt) = self.backtrace {
            body.push_section(
                "Backtrace",
                Collapsed(ConsoleSection(
                    DisplayFromDebug(bt).with_header("Backtrace:\n"),
                )),
            )?;
        }

        let url_result = Url::parse_with_params(
            self.url,
            &[("title", "<autogenerated-issue>"), ("body", &body.body)],
        );

        let url: &dyn fmt::Display = match &url_result {
            Ok(url_struct) => url_struct,
            Err(_) => &self.url,
        };

        url.with_header("Consider reporting this error using this URL: ")
            .fmt(f)
    }
}

struct Body {
    body: String,
}

impl Body {
    fn new() -> Self {
        Body {
            body: String::new(),
        }
    }
    fn push_section<T>(&mut self, header: &'static str, section: T) -> fmt::Result
    where
        T: fmt::Display,
    {
        use std::fmt::Write;

        let separator = if self.body.is_empty() { "" } else { "\n\n" };
        let header = header
            .with_header("## ")
            .with_header(separator)
            .with_footer("\n");

        write!(&mut self.body, "{}", section.with_header(header))
    }
}

struct MetadataSection<T> {
    metadata: T,
}

impl<'a, T> MetadataSection<T>
where
    T: IntoIterator<Item = &'a (String, Display<'a>)>,
{
    // This is implemented as a free functions so it can consume the `metadata`
    // iterator, rather than being forced to leave it unmodified if its behind a
    // `&self` shared reference via the Display trait
    #[allow(clippy::inherent_to_string, clippy::wrong_self_convention)]
    fn to_string(self) -> String {
        use std::fmt::Write;

        let mut out = String::new();
        let f = &mut out;

        writeln!(f, "|key|value|").expect("writing to a string doesn't panic");
        writeln!(f, "|--|--|").expect("writing to a string doesn't panic");

        for (key, value) in self.metadata {
            writeln!(f, "|**{}**|{}|", key, value).expect("writing to a string doesn't panic");
        }

        out
    }
}

struct ConsoleSection<T>(T);

impl<T> fmt::Display for ConsoleSection<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        (&self.0).with_header("```\n").with_footer("\n```").fmt(f)
    }
}

struct Collapsed<T>(T);

impl<T> fmt::Display for Collapsed<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        (&self.0)
            .with_header("\n<details>\n\n")
            .with_footer("\n</details>")
            .fmt(f)
    }
}

struct DisplayFromDebug<T>(T);

impl<T> fmt::Display for DisplayFromDebug<T>
where
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}
