use core::fmt::Debug;
use core::ops::Index;
use core::str::FromStr;

use beancount_account::AccountError;
use beancount_account::SegmentError;
use miette::Diagnostic;
use miette::SourceSpan;
use serde::Deserialize;
use snafu::ResultExt as _;
use snafu::Snafu;

use crate::Acc;
use crate::Account;
use crate::Seg;

#[derive(Debug, Diagnostic, Snafu)]
pub enum Error<E>
where
    E: 'static + Diagnostic,
{
    #[snafu(display("empty templates are not supported"))]
    EmptyTemplate {},

    #[snafu(display("errors while parsing template"))]
    Parsing {
        #[related]
        inner: Vec<ParsingError<E>>,

        #[source_code]
        template: String,
    },
}

#[derive(Debug, Diagnostic, Snafu)]
pub enum ParsingError<E>
where
    E: 'static + Diagnostic,
{
    #[snafu(display("could not parse base account"))]
    BaseAccount {
        source: AccountError,

        #[label]
        span: SourceSpan,
    },

    #[snafu(display("could not parse literal segment"))]
    LiteralSegment {
        #[diagnostic_source]
        source: SegmentError,

        #[label]
        span: SourceSpan,
    },

    #[snafu(display("could not parse selector segment"))]
    SelectorSegment {
        source: E,

        #[label]
        span: SourceSpan,
    },
}

#[derive(Clone, Debug, Deserialize)]
#[serde(
    bound = "Selector: FromStr, Selector::Err: 'static + Diagnostic",
    try_from = "String"
)]
pub struct Template<Selector> {
    base: Account,
    segments: Vec<Segment<Selector>>,
}

impl<Selector> Template<Selector>
where
    Selector: FromStr,
    Selector::Err: Diagnostic,
{
    /// Parse a [`str`] into a template.
    ///
    /// # Errors
    ///
    /// This function will return an error if the given `str` is not a valid template.
    /// This happens, when
    ///
    /// * the template does not start with a valid base account;
    /// * the template uses an unknown selector.
    pub fn parse(template: &str) -> Result<Self, Error<Selector::Err>> {
        snafu::ensure!(!template.is_empty(), EmptyTemplateSnafu {});

        let (base, segments) = template.find(":{").map_or((template, ""), |mid| {
            (&template[..mid], &template[mid + 1..])
        });

        let initial_offset = base.len();
        let base = base.try_into().context(BaseAccountSnafu {
            span: 0..base.len(),
        });
        let segments = parse_trailing_segments(segments, initial_offset);

        let errors = match (base, segments) {
            (Ok(base), Ok(segments)) => return Ok(Self { base, segments }),
            (Ok(_), Err(errors)) => errors,
            (Err(error), Ok(_)) => vec![error],
            (Err(error), Err(mut errors)) => {
                errors.insert(0, error);
                errors
            }
        };

        ParsingSnafu {
            inner: errors,
            template,
        }
        .fail()
    }
}

impl<'t, Selector> Template<Selector> {
    #[must_use]
    pub fn base(&self) -> &Acc {
        &self.base
    }

    pub fn render<'c, Context>(&'t self, context: &'c Context) -> Account
    where
        Context: Index<&'t Selector, Output = Seg>,
    {
        let fields = self.segments.iter().map(|field| match field {
            Segment::Literal(seg) => seg,
            Segment::Selector(selector) => context.index(selector),
        });

        fields.fold(self.base.clone(), Account::join)
    }
}

impl<Selector> TryFrom<&str> for Template<Selector>
where
    Selector: FromStr,
    Selector::Err: 'static + Diagnostic,
{
    type Error = Error<Selector::Err>;

    fn try_from(template: &str) -> Result<Self, Self::Error> {
        Self::parse(template)
    }
}

impl<Selector> TryFrom<String> for Template<Selector>
where
    Selector: FromStr,
    Selector::Err: 'static + Diagnostic,
{
    type Error = Error<Selector::Err>;

    fn try_from(template: String) -> Result<Self, Self::Error> {
        Self::parse(&template)
    }
}

#[derive(Clone, Debug)]
enum Segment<Selector> {
    Literal(crate::Segment),
    Selector(Selector),
}

impl<Selector> Segment<Selector>
where
    Selector: FromStr,
    Selector::Err: Diagnostic,
{
    fn parse(segment: &str, span: SourceSpan) -> Result<Self, ParsingError<Selector::Err>> {
        if segment.starts_with('{') && segment.ends_with('}') {
            Ok(Self::Selector(
                segment[1..segment.len() - 1]
                    .trim()
                    .parse()
                    .context(SelectorSegmentSnafu { span })?,
            ))
        } else {
            crate::Segment::try_from(segment)
                .map(Self::from)
                .context(LiteralSegmentSnafu { span })
        }
    }
}

impl<Selector> From<crate::Segment> for Segment<Selector> {
    fn from(seg: crate::Segment) -> Self {
        Self::Literal(seg)
    }
}

fn parse_trailing_segments<Selector>(
    segments: &str,
    initial_offset: usize,
) -> Result<Vec<Segment<Selector>>, Vec<ParsingError<Selector::Err>>>
where
    Selector: FromStr,
    Selector::Err: Diagnostic,
{
    if segments.is_empty() {
        return Ok(vec![]);
    }

    let segments = segments.split(':').scan(initial_offset, {
        |offset, segment| {
            let start = *offset + 1;
            *offset += segment.len() + 1;

            Some(Segment::parse(segment, (start..*offset).into()))
        }
    });

    let (length, _) = segments.size_hint();
    #[allow(clippy::manual_try_fold, /* reason = "we want to collect multiple errors here" */)]
    segments.fold(Ok(Vec::with_capacity(length)), |result, segment| {
        match (result, segment) {
            (Ok(mut segments), Ok(segment)) => {
                segments.push(segment);
                Ok(segments)
            }
            (Ok(_), Err(error)) => Err(vec![error]),
            (Err(errors), Ok(_)) => Err(errors),
            (Err(mut errors), Err(error)) => {
                errors.push(error);
                Err(errors)
            }
        }
    })
}