use serde::{Deserialize, Serialize};
use time::{Date, OffsetDateTime};

use crate::ObjectId;

/// Publication state of a catalogue record.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, utoipa::ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
    /// Work in progress; not shown anywhere public.
    #[default]
    Draft,
    /// Complete but internal-only.
    Internal,
    /// Published; eligible for the public API.
    Public,
}

impl Visibility {
    pub const fn as_str(&self) -> &'static str {
        match self {
            Visibility::Draft => "draft",
            Visibility::Internal => "internal",
            Visibility::Public => "public",
        }
    }

    pub fn from_db(s: &str) -> Option<Self> {
        match s {
            "draft" => Some(Visibility::Draft),
            "internal" => Some(Visibility::Internal),
            "public" => Some(Visibility::Public),
            _ => None,
        }
    }
}

impl Visibility {
    /// Whether `self` may move directly to `target`. Legal single steps are
    /// `draft↔internal` and `internal↔public`; `draft↔public` is not one step.
    pub fn can_transition_to(self, target: Visibility) -> bool {
        use Visibility::*;

        matches!(
            (self, target),
            (Draft, Internal) | (Internal, Draft) | (Internal, Public) | (Public, Internal)
        )
    }

    /// Validate a stepwise transition to `target`. Setting to the current value is an
    /// idempotent no-op (`Ok`). A forbidden jump returns [`IllegalTransition`].
    pub fn transition_to(self, target: Visibility) -> Result<Visibility, IllegalTransition> {
        if self == target || self.can_transition_to(target) {
            Ok(target)
        } else {
            Err(IllegalTransition {
                from: self,
                to: target,
            })
        }
    }
}

/// An attempted visibility change the state machine forbids.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IllegalTransition {
    pub from: Visibility,
    pub to: Visibility,
}

impl std::fmt::Display for IllegalTransition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "illegal visibility transition: {} -> {}",
            self.from.as_str(),
            self.to.as_str()
        )
    }
}

impl std::error::Error for IllegalTransition {}

/// The mutable inventory-minimum fields of a catalogue object.
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectInput {
    pub object_number: String,
    pub object_name: String,
    pub number_of_objects: i32,
    pub brief_description: Option<String>,
    pub current_location: Option<String>,
    pub current_owner: Option<String>,
    pub recorder: Option<String>,
    pub recording_date: Option<Date>,
    pub visibility: Visibility,
}

/// A catalogue object (or group of objects), read back from storage.
#[derive(Debug, Clone, PartialEq)]
pub struct CatalogueObject {
    pub id: ObjectId,
    pub object_number: String,
    pub object_name: String,
    pub number_of_objects: i32,
    pub brief_description: Option<String>,
    pub current_location: Option<String>,
    pub current_owner: Option<String>,
    pub recorder: Option<String>,
    pub recording_date: Option<Date>,
    pub visibility: Visibility,
    /// Flexible field values (field key -> value), validated against the registry.
    pub fields: serde_json::Value,
    pub created_at: OffsetDateTime,
    pub updated_at: OffsetDateTime,
}

impl CatalogueObject {
    /// The mutable fields as an [`ObjectInput`] (used to diff against an update).
    pub fn to_input(&self) -> ObjectInput {
        ObjectInput {
            object_number: self.object_number.clone(),
            object_name: self.object_name.clone(),
            number_of_objects: self.number_of_objects,
            brief_description: self.brief_description.clone(),
            current_location: self.current_location.clone(),
            current_owner: self.current_owner.clone(),
            recorder: self.recorder.clone(),
            recording_date: self.recording_date,
            visibility: self.visibility,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn visibility_round_trips_and_defaults_to_draft() {
        for v in [Visibility::Draft, Visibility::Internal, Visibility::Public] {
            assert_eq!(Visibility::from_db(v.as_str()), Some(v));
        }
        assert_eq!(Visibility::from_db("secret"), None);
        assert_eq!(Visibility::default(), Visibility::Draft);
    }

    #[test]
    fn visibility_serde_matches_as_str() {
        for v in [Visibility::Draft, Visibility::Internal, Visibility::Public] {
            assert_eq!(
                serde_json::to_value(v).unwrap(),
                serde_json::Value::String(v.as_str().to_owned())
            );
        }
    }

    #[test]
    fn stepwise_transitions_are_legal() {
        use Visibility::*;
        assert_eq!(Draft.transition_to(Internal), Ok(Internal));
        assert_eq!(Internal.transition_to(Public), Ok(Public));
        assert_eq!(Public.transition_to(Internal), Ok(Internal));
        assert_eq!(Internal.transition_to(Draft), Ok(Draft));
    }

    #[test]
    fn skipping_a_step_is_illegal() {
        use Visibility::*;
        assert_eq!(
            Draft.transition_to(Public),
            Err(IllegalTransition {
                from: Draft,
                to: Public
            })
        );
        assert_eq!(
            Public.transition_to(Draft),
            Err(IllegalTransition {
                from: Public,
                to: Draft
            })
        );
        // the Display message is the user-visible surface of the error
        assert_eq!(
            Draft.transition_to(Public).unwrap_err().to_string(),
            "illegal visibility transition: draft -> public"
        );
    }

    #[test]
    fn setting_to_current_value_is_a_noop_ok() {
        for v in [Visibility::Draft, Visibility::Internal, Visibility::Public] {
            assert_eq!(v.transition_to(v), Ok(v));
        }
    }
}
