use crate::ForkCondition;
use alloc::{
    format,
    string::{String, ToString},
    vec::Vec,
};
use alloy_hardforks::Hardfork;

/// A container to pretty-print a hardfork.
///
/// The fork is formatted depending on its fork condition:
///
/// - Block and timestamp based forks are formatted in the same manner (`{name} <({eip})>
///   @{condition}`)
/// - TTD based forks are formatted separately as `{name} <({eip})> @{ttd} (network is <not> known
///   to be merged)`
///
/// An optional EIP can be attached to the fork to display as well. This should generally be in the
/// form of just `EIP-x`, e.g. `EIP-1559`.
#[derive(Debug)]
struct DisplayFork {
    /// The name of the hardfork (e.g. Frontier)
    name: String,
    /// The fork condition
    activated_at: ForkCondition,
    /// An optional EIP (e.g. `EIP-1559`).
    eip: Option<String>,
    /// Optional metadata to display alongside the fork (e.g. blob parameters)
    metadata: Option<String>,
}

impl core::fmt::Display for DisplayFork {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let name_with_eip = if let Some(eip) = &self.eip {
            format!("{} ({})", self.name, eip)
        } else {
            self.name.clone()
        };

        match self.activated_at {
            ForkCondition::Block(at) | ForkCondition::Timestamp(at) => {
                write!(f, "{name_with_eip:32} @{at}")?;
                if let Some(metadata) = &self.metadata {
                    write!(f, "          {metadata}")?;
                }
            }
            ForkCondition::TTD { total_difficulty, .. } => {
                // All networks that have merged are finalized.
                write!(
                    f,
                    "{name_with_eip:32} @{total_difficulty} (network is known to be merged)",
                )?;
                if let Some(metadata) = &self.metadata {
                    write!(f, "          {metadata}")?;
                }
            }
            ForkCondition::Never => unreachable!(),
        }

        Ok(())
    }
}

// # Examples
//
// ```
// # use reth_chainspec::MAINNET;
// println!("{}", MAINNET.display_hardforks());
// ```
//
/// A container for pretty-printing a list of hardforks.
///
/// An example of the output:
///
/// ```text
/// Pre-merge hard forks (block based):
// - Frontier                         @0
// - Homestead                        @1150000
// - Dao                              @1920000
// - Tangerine                        @2463000
// - SpuriousDragon                   @2675000
// - Byzantium                        @4370000
// - Constantinople                   @7280000
// - Petersburg                       @7280000
// - Istanbul                         @9069000
// - MuirGlacier                      @9200000
// - Berlin                           @12244000
// - London                           @12965000
// - ArrowGlacier                     @13773000
// - GrayGlacier                      @15050000
// Merge hard forks:
// - Paris                            @58750000000000000000000 (network is known to be merged)
// Post-merge hard forks (timestamp based):
// - Shanghai                         @1681338455
// - Cancun                           @1710338135
// - Prague                           @1746612311
/// ```
#[derive(Debug)]
pub struct DisplayHardforks {
    /// A list of pre-merge (block based) hardforks
    pre_merge: Vec<DisplayFork>,
    /// A list of merge (TTD based) hardforks
    with_merge: Vec<DisplayFork>,
    /// A list of post-merge (timestamp based) hardforks
    post_merge: Vec<DisplayFork>,
}

impl core::fmt::Display for DisplayHardforks {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        fn format(
            header: &str,
            forks: &[DisplayFork],
            next_is_empty: bool,
            f: &mut core::fmt::Formatter<'_>,
        ) -> core::fmt::Result {
            writeln!(f, "{header}:")?;
            let mut iter = forks.iter().peekable();
            while let Some(fork) = iter.next() {
                write!(f, "- {fork}")?;
                if !next_is_empty || iter.peek().is_some() {
                    writeln!(f)?;
                }
            }
            Ok(())
        }

        if !self.pre_merge.is_empty() {
            format(
                "Pre-merge hard forks (block based)",
                &self.pre_merge,
                self.with_merge.is_empty(),
                f,
            )?;
        }

        if self.with_merge.is_empty() {
            if !self.pre_merge.is_empty() && !self.post_merge.is_empty() {
                // need an extra line here in case we don't have a merge block (optimism)
                writeln!(f)?;
            }
        } else {
            format("Merge hard forks", &self.with_merge, self.post_merge.is_empty(), f)?;
        }

        if !self.post_merge.is_empty() {
            format("Post-merge hard forks (timestamp based)", &self.post_merge, true, f)?;
        }

        Ok(())
    }
}

impl DisplayHardforks {
    /// Creates a new [`DisplayHardforks`] from an iterator of hardforks.
    pub fn new<'a, I>(hardforks: I) -> Self
    where
        I: IntoIterator<Item = (&'a dyn Hardfork, ForkCondition)>,
    {
        // Delegate to with_meta by mapping the iterator to include None for metadata
        Self::with_meta(hardforks.into_iter().map(|(fork, condition)| (fork, condition, None)))
    }

    /// Creates a new [`DisplayHardforks`] from an iterator of hardforks with optional metadata.
    pub fn with_meta<'a, I>(hardforks: I) -> Self
    where
        I: IntoIterator<Item = (&'a dyn Hardfork, ForkCondition, Option<String>)>,
    {
        let mut pre_merge = Vec::new();
        let mut with_merge = Vec::new();
        let mut post_merge = Vec::new();

        for (fork, condition, metadata) in hardforks {
            let display_fork = DisplayFork {
                name: fork.name().to_string(),
                activated_at: condition,
                eip: None,
                metadata,
            };

            match condition {
                ForkCondition::Block(_) => {
                    pre_merge.push(display_fork);
                }
                ForkCondition::TTD { .. } => {
                    with_merge.push(display_fork);
                }
                ForkCondition::Timestamp(_) => {
                    post_merge.push(display_fork);
                }
                ForkCondition::Never => {}
            }
        }

        Self { pre_merge, with_merge, post_merge }
    }
}
