use serde::{de::DeserializeOwned, Serialize};
use urn::Urn;

use crate::Error;

use super::Event;

/// Composing and decomposing scoped URNs.
///
/// A scoped URN embeds a second URN as a suffix of the NSS:
/// `urn:<nid>:<nss>@<scope_nid>:<scope_nss>`
///
/// This trait is automatically available for every type that implements
/// `Into<Urn> + TryFrom<Urn> + Clone`.  In practice that means all URN
/// newtypes generated by `define_aggregate!` or `#[derive(Urn)]`.
///
/// Import via the prelude to avoid listing the trait explicitly:
///
/// ```rust,ignore
/// use replay::prelude::*;
///
/// let account: BankAccountUrn = BankAccountUrn::new("acct-1")?;
/// let branch:  BranchUrn      = BranchUrn::new("london")?;
///
/// // urn:bank-account:acct-1  +  urn:branch:london  →  urn:bank-account:acct-1@branch:london
/// // Both by value and by reference work:
/// let scoped: BankAccountUrn = account.at(&branch)?;  // branch is still usable after this
///
/// // round-trip
/// let extracted: BranchUrn = scoped.extract_scope::<BranchUrn>()?;
/// ```
pub trait ScopedUrn: Sized + Clone + Into<Urn> + TryFrom<Urn, Error: std::fmt::Debug> {
    /// Scopes `self` under `other`, returning a new URN whose NSS is
    /// `<current_nss>@<other_nid>:<other_nss>`.
    ///
    /// Fails if the current URN is already scoped (NSS contains `@`), or if
    /// `other`'s NSS contains `@` (i.e. `other` is itself a scoped URN).
    /// Both preconditions are symmetric: the resulting URN will always contain
    /// exactly one `@`, satisfying the invariant required by [`Self::extract_scope`].
    #[track_caller]
    fn at(&self, other: impl AsRef<Urn>) -> crate::Result<Self> {
        let current_urn: Urn = self.clone().into();
        let other_urn: &Urn = other.as_ref();

        if current_urn.nss().contains('@') {
            return Err(Error::invalid_input("URN is already scoped (contains '@')")
                .with_operation("at")
                .with_context("urn", current_urn.to_string()));
        }

        if other_urn.nss().contains('@') {
            return Err(
                Error::invalid_input("scope URN is already scoped (contains '@')")
                    .with_operation("at")
                    .with_context("scope", other_urn.to_string()),
            );
        }

        let new_nss = format!(
            "{}@{}:{}",
            current_urn.nss(),
            other_urn.nid(),
            other_urn.nss()
        );

        let new_urn = match urn::UrnBuilder::new(current_urn.nid(), &new_nss).build() {
            Err(e) => {
                return Err(Error::invalid_input("Failed to build scoped URN")
                    .with_operation("at")
                    .with_context("nss", new_nss.clone())
                    .with_context("error", format!("{:?}", e)))
            }
            Ok(u) => u,
        };

        match Self::try_from(new_urn) {
            Err(e) => Err(
                Error::invalid_input("Failed to convert scoped URN to expected type")
                    .with_operation("at")
                    .with_context("error", format!("{:?}", e)),
            ),
            Ok(s) => Ok(s),
        }
    }

    /// Extracts the scope URN embedded by [`Self::at`], converting it into
    /// the output type `O`.
    ///
    /// `O::try_from` is responsible for NID validation, so passing the wrong
    /// output type returns an error rather than silently succeeding.
    #[track_caller]
    fn extract_scope<O>(&self) -> crate::Result<O>
    where
        O: TryFrom<Urn, Error: std::fmt::Debug>,
    {
        let current_urn: Urn = self.clone().into();
        let nss = current_urn.nss();

        let (_own_nss, scope_part) = match nss.split_once('@') {
            None => {
                return Err(Error::invalid_input("URN is not scoped (no '@' in NSS)")
                    .with_operation("extract_scope")
                    .with_context("urn", current_urn.to_string()))
            }
            Some((_, scope)) if scope.contains('@') => {
                return Err(
                    Error::invalid_input("URN has multiple '@' in NSS (ambiguous scope)")
                        .with_operation("extract_scope")
                        .with_context("urn", current_urn.to_string()),
                )
            }
            Some(("", _)) => {
                return Err(Error::invalid_input("URN has empty NSS before '@'")
                    .with_operation("extract_scope")
                    .with_context("urn", current_urn.to_string()))
            }
            Some(parts) => parts,
        };

        let (scope_nid, scope_nss) = match scope_part.split_once(':') {
            None => {
                return Err(Error::invalid_input(
                    "Scope part after '@' is missing ':' (expected '<nid>:<nss>')",
                )
                .with_operation("extract_scope")
                .with_context("urn", current_urn.to_string())
                .with_context("scope_part", scope_part.to_string()))
            }
            Some(("", _)) => {
                return Err(Error::invalid_input("Scope NID is empty")
                    .with_operation("extract_scope")
                    .with_context("urn", current_urn.to_string()))
            }
            Some((_, "")) => {
                return Err(Error::invalid_input("Scope NSS is empty")
                    .with_operation("extract_scope")
                    .with_context("urn", current_urn.to_string()))
            }
            Some(parts) => parts,
        };

        let scope_urn = match urn::UrnBuilder::new(scope_nid, scope_nss).build() {
            Err(e) => {
                return Err(Error::invalid_input("Failed to build scope URN")
                    .with_operation("extract_scope")
                    .with_context("scope_nid", scope_nid.to_string())
                    .with_context("scope_nss", scope_nss.to_string())
                    .with_context("error", format!("{:?}", e)))
            }
            Ok(u) => u,
        };

        match O::try_from(scope_urn) {
            Err(e) => Err(Error::invalid_input(
                "Failed to convert scope URN to expected type (NID mismatch?)",
            )
            .with_operation("extract_scope")
            .with_context("error", format!("{:?}", e))),
            Ok(o) => Ok(o),
        }
    }
}

/// Blanket impl: every type with `Into<Urn> + TryFrom<Urn> + Clone` gets
/// `at` and `extract_scope` for free.
impl<T> ScopedUrn for T where T: Sized + Clone + Into<Urn> + TryFrom<Urn, Error: std::fmt::Debug> {}

/// A trait for types that have a stream identifier.
///
/// This trait provides constructor and accessor methods for working with stream identifiers.
pub trait WithId: Sized {
    type StreamId: Send
        + Sync
        + Into<Urn>
        + TryFrom<Urn, Error: std::fmt::Debug>
        + Clone
        + PartialEq
        + std::fmt::Debug
        + Serialize
        + DeserializeOwned;

    /// Creates a new instance with the given id.
    fn with_id(id: Self::StreamId) -> Self;

    /// Returns a reference to the identifier.
    fn get_id(&self) -> &Self::StreamId;

    fn with_string_id(id: impl Into<String>) -> crate::Result<Self> {
        use std::str::FromStr;
        let id_string = id.into();

        // Parse string as URN
        let urn = Urn::from_str(&id_string).map_err(|e| {
            Error::invalid_input("Invalid URN format")
                .with_operation("with_string_id")
                .with_context("id", id_string.clone())
                .with_context("error", format!("{:?}", e))
        })?;

        // Convert URN to aggregate StreamId type
        let aggregate_id: Self::StreamId = urn.try_into().map_err(|e| {
            Error::invalid_input("Failed to convert URN to StreamId type")
                .with_operation("with_string_id")
                .with_context("id", id_string.clone())
                .with_context("error", format!("{:?}", e))
        })?;

        Ok(Self::with_id(aggregate_id))
    }

    /// Returns a new instance whose stream ID has the suffix `@<other.nid>:<other.nss>` appended
    /// to the current URN's NSS, scoping this stream under `other`.
    ///
    /// `other` can be any type that converts into a [`Urn`] — typically another aggregate's
    /// `StreamId`. Fails if either URN is already scoped (NSS contains `@`).
    ///
    /// Delegates to [`ScopedUrn::at`] on `StreamId`.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // urn:product:sku123  +  urn:catalog:that  →  urn:product:sku123@catalog:that
    /// let scoped: ProductStream = product_stream.at(catalog_urn)?;
    /// ```
    fn at(&self, other: impl AsRef<Urn>) -> crate::Result<Self> {
        let scoped_id = self.get_id().at(other)?;
        Ok(Self::with_id(scoped_id))
    }

    /// Extracts the scope URN embedded by [`Self::at`], converting it into `O`.
    ///
    /// Delegates to [`ScopedUrn::extract_scope`] on `StreamId`.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// // urn:product:sku123@catalog:that  →  CatalogUrn(urn:catalog:that)
    /// let scope: CatalogUrn = scoped_product.extract_scope::<CatalogUrn>()?;
    /// ```
    #[track_caller]
    fn extract_scope<O>(&self) -> crate::Result<O>
    where
        O: TryFrom<Urn, Error: std::fmt::Debug>,
    {
        self.get_id().extract_scope()
    }
}

/// In event sourcing a stream is a sequence of events that are related to a specific entity.
///
/// A stream is expected to be able to apply events and have a type.
/// A stream state is created only from its events, that's why you need to implement `Default` trait.
///
/// Streams are identified using a URN, recommendation is to use speficic types for that.
///
/// # Example
///
/// ```
/// use replay::{Event, EventStream, WithId};
/// use serde::{Deserialize, Serialize};
/// use urn::Urn;
///
/// struct BankAccountStream {
///     pub id: BankAccountUrn,
///     pub balance: f64,
/// }
///
/// #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
/// enum BankAccountEvent {
///     Deposited { amount: f64 },
///     Withdrawn { amount: f64 },
/// }
///
/// impl Event for BankAccountEvent {
///     fn event_type(&self) -> String {
///         match self {
///             BankAccountEvent::Deposited { .. } => "Deposited".to_string(),
///             BankAccountEvent::Withdrawn { .. } => "Withdrawn".to_string(),
///         }
///     }
/// }
///
/// #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
/// struct BankAccountUrn(Urn);
///
/// impl From<BankAccountUrn> for Urn {
///     fn from(urn: BankAccountUrn) -> Self {
///         urn.0
///     }
/// }
///
/// impl TryFrom<Urn> for BankAccountUrn {
///     type Error = String;
///
///     fn try_from(urn: Urn) -> Result<Self, Self::Error> {
///         Ok(BankAccountUrn(urn))
///     }
/// }
///
/// impl WithId for BankAccountStream {
///    type StreamId = BankAccountUrn;
///
///    fn with_id(id: Self::StreamId) -> Self {
///        Self { id, balance: 0.0 }
///    }
///
///    fn get_id(&self) -> &Self::StreamId {
///        &self.id
///    }
/// }
///
/// impl EventStream for BankAccountStream {
///    type Event = BankAccountEvent;
///
///    fn stream_type() -> String {
///        "BankAccount".to_string()
///    }
///
///    fn apply(&mut self, event: Self::Event) {
///        match event {
///            BankAccountEvent::Deposited { amount } => {
///                self.balance += amount;
///            }
///            BankAccountEvent::Withdrawn { amount } => {
///                self.balance -= amount;
///            }
///        }
///    }
///}
/// ```
pub trait EventStream: Sized + WithId {
    type Event: Event;

    fn stream_type() -> String;

    fn apply(&mut self, event: Self::Event);

    fn apply_all(&mut self, events: Vec<Self::Event>) {
        for event in events {
            self.apply(event);
        }
    }
}

/*
/// Stream state is a representation of the current state of a stream, every time an event is applied the state is updated and the version will increment.
///
/// The stream id is a URN that identifies the stream and it's not included in the model to avoid polluting the domain (not all domains have an id).
///
/// The stream type is the type of the stream, it's used to identify the stream.

#[derive(Debug)]
pub struct StreamState {
    pub stream_id: Urn,
    pub stream_type: String,
    pub version: i64,
}
impl Display for StreamState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Stream {} with id: {} and version: {}",
            self.stream_type, self.stream_id, self.version
        )
    }
}
*/

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use serde::{Deserialize, Serialize};
    use urn::Urn;

    use super::*;

    //  bank account stream (id of stream is not part of the model)
    struct BankAccountStream {
        pub id: BankAccountUrn,
        pub balance: f64,
    }

    // create bank account events enum: Deposited and Withdrawn
    #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
    enum BankAccountEvent {
        Deposited { amount: f64 },
        Withdrawn { amount: f64 },
    }

    impl crate::Event for BankAccountEvent {
        fn event_type(&self) -> String {
            match self {
                BankAccountEvent::Deposited { .. } => "Deposited".to_string(),
                BankAccountEvent::Withdrawn { .. } => "Withdrawn".to_string(),
            }
        }
    }

    // bank account urn
    #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
    struct BankAccountUrn(Urn);

    impl From<BankAccountUrn> for Urn {
        fn from(urn: BankAccountUrn) -> Self {
            urn.0
        }
    }

    impl TryFrom<Urn> for BankAccountUrn {
        type Error = String;

        fn try_from(urn: Urn) -> Result<Self, Self::Error> {
            Ok(BankAccountUrn(urn))
        }
    }

    impl WithId for BankAccountStream {
        type StreamId = BankAccountUrn;

        fn with_id(id: Self::StreamId) -> Self {
            BankAccountStream { id, balance: 0.0 }
        }

        fn get_id(&self) -> &Self::StreamId {
            &self.id
        }
    }

    // bank account stream
    impl EventStream for BankAccountStream {
        type Event = BankAccountEvent;

        fn stream_type() -> String {
            "BankAccount".to_string()
        }

        fn apply(&mut self, event: Self::Event) {
            match event {
                BankAccountEvent::Deposited { amount } => {
                    self.balance += amount;
                }
                BankAccountEvent::Withdrawn { amount } => {
                    self.balance -= amount;
                }
            }
        }
    }

    #[test]
    fn test_bank_account_stream() {
        let mut bank_account = BankAccountStream {
            id: BankAccountUrn(Urn::from_str("urn:bank-account:1").unwrap()),
            balance: 0.0,
        };
        let deposited_event = BankAccountEvent::Deposited { amount: 100.0 };
        let withdrawn_event = BankAccountEvent::Withdrawn { amount: 50.0 };

        // check initial state
        assert_eq!(bank_account.balance, 0.0);

        bank_account.apply(deposited_event);
        bank_account.apply(withdrawn_event);

        assert_eq!(bank_account.balance, 50.0);
    }

    // product urn — validates nid == "product"
    #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
    struct ProductUrn(Urn);

    impl From<ProductUrn> for Urn {
        fn from(urn: ProductUrn) -> Self {
            urn.0
        }
    }

    impl TryFrom<Urn> for ProductUrn {
        type Error = String;

        fn try_from(urn: Urn) -> Result<Self, Self::Error> {
            if urn.nid() == "product" {
                Ok(ProductUrn(urn))
            } else {
                Err(format!(
                    "Invalid NID for ProductUrn: expected 'product', got '{}'",
                    urn.nid()
                ))
            }
        }
    }

    // catalog urn — validates nid == "catalog"
    #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
    struct CatalogUrn(Urn);

    impl From<CatalogUrn> for Urn {
        fn from(urn: CatalogUrn) -> Self {
            urn.0
        }
    }

    impl AsRef<Urn> for CatalogUrn {
        fn as_ref(&self) -> &Urn {
            &self.0
        }
    }

    impl TryFrom<Urn> for CatalogUrn {
        type Error = String;

        fn try_from(urn: Urn) -> Result<Self, Self::Error> {
            if urn.nid() == "catalog" {
                Ok(CatalogUrn(urn))
            } else {
                Err(format!(
                    "Invalid NID for CatalogUrn: expected 'catalog', got '{}'",
                    urn.nid()
                ))
            }
        }
    }

    // minimal stream wrappers so we can call WithId methods
    #[derive(Debug)]
    struct ProductStream {
        pub id: ProductUrn,
    }

    impl WithId for ProductStream {
        type StreamId = ProductUrn;

        fn with_id(id: Self::StreamId) -> Self {
            ProductStream { id }
        }

        fn get_id(&self) -> &Self::StreamId {
            &self.id
        }
    }

    #[test]
    fn test_at_creates_scoped_urn() {
        let product = ProductStream::with_string_id("urn:product:sku123").unwrap();
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:that").unwrap());
        let scoped = product.at(catalog).unwrap();
        let scoped_urn: Urn = scoped.get_id().clone().into();
        assert_eq!(scoped_urn.to_string(), "urn:product:sku123@catalog:that");
    }

    #[test]
    fn test_at_fails_if_already_scoped() {
        let product = ProductStream::with_string_id("urn:product:sku123@catalog:that").unwrap();
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:other").unwrap());
        assert!(product.at(catalog).is_err());
    }

    #[test]
    fn test_at_fails_if_scope_is_already_scoped() {
        // other's NSS contains '@' — would produce a double-@ URN
        let product = ProductStream::with_string_id("urn:product:sku123").unwrap();
        let already_scoped_catalog =
            CatalogUrn(Urn::from_str("urn:catalog:sub@tenant:acme").unwrap());
        let err = product.at(already_scoped_catalog).unwrap_err();
        assert!(err.to_string().contains("scope URN is already scoped"));
    }

    #[test]
    fn test_scoped_urn_at_directly_on_urn_type() {
        // ScopedUrn blanket impl lets you call at/extract_scope on a URN value directly.
        let product = ProductUrn(Urn::from_str("urn:product:sku123").unwrap());
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:that").unwrap());
        let scoped: ProductUrn = product.at(&catalog).unwrap();
        let scoped_urn: Urn = scoped.clone().into();
        assert_eq!(scoped_urn.to_string(), "urn:product:sku123@catalog:that");
        let extracted: CatalogUrn = scoped.extract_scope::<CatalogUrn>().unwrap();
        let extracted_urn: Urn = extracted.into();
        assert_eq!(extracted_urn.nss(), "that");
    }

    #[test]
    fn test_extract_scope_happy_path() {
        let product = ProductStream::with_string_id("urn:product:sku123@catalog:that").unwrap();
        let scope: CatalogUrn = product.extract_scope::<CatalogUrn>().unwrap();
        let scope_urn: Urn = scope.into();
        assert_eq!(scope_urn.nid(), "catalog");
        assert_eq!(scope_urn.nss(), "that");
    }

    #[test]
    fn test_extract_scope_wrong_nid_fails() {
        // The scope is urn:catalog:that, but we ask for it as ProductUrn (nid="product").
        let product = ProductStream::with_string_id("urn:product:sku123@catalog:that").unwrap();
        let err = product.extract_scope::<ProductUrn>().unwrap_err();
        assert!(err.to_string().contains("NID mismatch"));
    }

    #[test]
    fn test_extract_scope_roundtrip() {
        let product = ProductStream::with_string_id("urn:product:sku123").unwrap();
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:that").unwrap());
        let scoped = product.at(catalog.clone()).unwrap();
        let extracted: CatalogUrn = scoped.extract_scope::<CatalogUrn>().unwrap();
        assert_eq!(extracted, catalog);
    }

    #[test]
    fn test_extract_scope_not_scoped() {
        let product = ProductStream::with_string_id("urn:product:sku123").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("not scoped"));
    }

    #[test]
    fn test_extract_scope_empty_own_nss() {
        // urn:product:@catalog:that — empty NSS before @
        let product = ProductStream::with_string_id("urn:product:@catalog:that").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("empty NSS before"));
    }

    #[test]
    fn test_extract_scope_missing_colon_in_scope() {
        // urn:product:sku123@catalog — no colon after @
        let product = ProductStream::with_string_id("urn:product:sku123@catalog").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("missing ':'"));
    }

    #[test]
    fn test_extract_scope_multiple_at() {
        // urn:product:sku123@catalog:that@tenant:acme — multiple @
        let product =
            ProductStream::with_string_id("urn:product:sku123@catalog:that@tenant:acme").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("multiple '@'"));
    }

    #[test]
    fn test_at_preserves_own_nid_and_nss() {
        // The base product's nid and the NSS before @ must be unchanged.
        let product = ProductStream::with_string_id("urn:product:sku-999").unwrap();
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:books").unwrap());
        let scoped = product.at(catalog).unwrap();
        let scoped_urn: Urn = scoped.get_id().clone().into();
        assert_eq!(scoped_urn.nid(), "product");
        assert_eq!(scoped_urn.nss(), "sku-999@catalog:books");
    }

    #[test]
    fn test_extract_scope_empty_scope_nid() {
        // urn:product:sku@:nss — empty NID in scope
        let product = ProductStream::with_string_id("urn:product:sku@:nss").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("Scope NID is empty"));
    }

    #[test]
    fn test_extract_scope_empty_scope_nss() {
        // urn:product:sku@nid: — empty NSS in scope
        let product = ProductStream::with_string_id("urn:product:sku@nid:").unwrap();
        let err = product.extract_scope::<CatalogUrn>().unwrap_err();
        assert!(err.to_string().contains("Scope NSS is empty"));
    }

    #[test]
    fn test_scoped_urn_parses_back_from_string() {
        // A scoped URN should survive a round-trip through with_string_id.
        let product = ProductStream::with_string_id("urn:product:sku123").unwrap();
        let catalog = CatalogUrn(Urn::from_str("urn:catalog:that").unwrap());
        let scoped_urn_str: String = {
            let u: Urn = product.at(catalog).unwrap().get_id().clone().into();
            u.to_string()
        };
        // Parse back from the URN string.
        let reparsed = ProductStream::with_string_id(&scoped_urn_str).unwrap();
        let scope: CatalogUrn = reparsed.extract_scope::<CatalogUrn>().unwrap();
        let scope_urn: Urn = scope.into();
        assert_eq!(scope_urn.nid(), "catalog");
        assert_eq!(scope_urn.nss(), "that");
    }
}
