use std::fmt;

/// A thing to be learned.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Datum {
    card_id: Option<u64>,
    reversal_policy: ReversalPolicy,
    prompt: String,
    response: String,
}

impl Datum {
    pub fn new(
        prompt: impl Into<String>,
        response: impl Into<String>,
    ) -> Result<Self, DatumValidationError> {
        let prompt = prompt.into();
        let response = response.into();

        if prompt.trim().is_empty() {
            return Err(DatumValidationError::EmptyPrompt);
        }

        if response.trim().is_empty() {
            return Err(DatumValidationError::EmptyResponse);
        }

        Ok(Self {
            card_id: None,
            reversal_policy: ReversalPolicy::Unspecified,
            prompt,
            response,
        })
    }

    pub fn with_card_id(mut self, card_id: u64) -> Self {
        self.card_id = Some(card_id);
        self
    }

    pub fn card_id(&self) -> Option<u64> {
        self.card_id
    }

    pub fn with_reversal_policy(mut self, reversal_policy: ReversalPolicy) -> Self {
        self.reversal_policy = reversal_policy;
        self
    }

    pub fn with_reversal_prohibited(mut self, prohibited: bool) -> Self {
        self.reversal_policy = if prohibited {
            ReversalPolicy::Prohibited
        } else {
            ReversalPolicy::Unspecified
        };
        self
    }

    pub fn reversal_policy(&self) -> ReversalPolicy {
        self.reversal_policy
    }

    pub fn is_reversal_prohibited(&self) -> bool {
        self.reversal_policy == ReversalPolicy::Prohibited
    }

    pub fn prompt(&self) -> &str {
        &self.prompt
    }

    pub fn response(&self) -> &str {
        &self.response
    }

    pub fn into_parts(self) -> (String, String) {
        (self.prompt, self.response)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ReversalPolicy {
    #[default]
    Unspecified,
    Prohibited,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DatumValidationError {
    EmptyPrompt,
    EmptyResponse,
}

impl fmt::Display for DatumValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyPrompt => f.write_str("datum prompt must not be empty"),
            Self::EmptyResponse => f.write_str("datum response must not be empty"),
        }
    }
}

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

/// Stable identifier used to connect review history to an editable datum.
///
/// New deck files store an explicit card ID so editing prompt or response text
/// preserves review history. Legacy data without an ID falls back to a
/// content-derived key until the deck is upgraded.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DatumKey(String);

impl DatumKey {
    pub fn for_datum(datum: &Datum) -> Self {
        if let Some(card_id) = datum.card_id() {
            return Self::for_card_id(card_id);
        }

        Self::for_legacy_content(datum)
    }

    pub fn for_card_id(card_id: u64) -> Self {
        Self(format!("card:{card_id}"))
    }

    pub fn for_reverse_of(datum_key: &Self) -> Self {
        Self(format!("{}:reverse", datum_key.as_str()))
    }

    pub fn parent_key_for_reverse(&self) -> Option<Self> {
        self.0
            .strip_suffix(":reverse")
            .map(|parent| Self(parent.to_string()))
    }

    pub fn is_reverse(&self) -> bool {
        self.parent_key_for_reverse().is_some()
    }

    pub fn for_legacy_content(datum: &Datum) -> Self {
        let mut hash = FNV_1A_64_OFFSET_BASIS;
        hash = fnv_1a_64_update(hash, datum.prompt().as_bytes());
        hash = fnv_1a_64_update(hash, &[0]);
        hash = fnv_1a_64_update(hash, datum.response().as_bytes());

        Self(format!("fnv1a64:{hash:016x}"))
    }

    pub fn card_id(&self) -> Option<u64> {
        self.0.strip_prefix("card:")?.parse().ok()
    }

    pub fn from_storage_key(value: impl Into<String>) -> Self {
        Self(value.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn into_string(self) -> String {
        self.0
    }
}

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

const FNV_1A_64_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_1A_64_PRIME: u64 = 0x100000001b3;

fn fnv_1a_64_update(mut hash: u64, bytes: &[u8]) -> u64 {
    for byte in bytes {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(FNV_1A_64_PRIME);
    }

    hash
}

/// The user's optional guess after seeing a prompt but before seeing the response.
///
/// `NotAnswered` means the user revealed the response without recording a
/// confidence rating. Recall feedback is still required before a review event
/// is recorded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum PromptGuess {
    NotAnswered = 0,
    DontKnow = 1,
    ProbablyDontKnow = 2,
    ProbablyKnow = 3,
    CertainlyKnow = 4,
}

impl PromptGuess {
    pub const MIN_VALUE: u8 = 0;
    pub const MAX_VALUE: u8 = 4;

    pub fn value(self) -> u8 {
        self as u8
    }
}

impl TryFrom<u8> for PromptGuess {
    type Error = RatingValueError;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::NotAnswered),
            1 => Ok(Self::DontKnow),
            2 => Ok(Self::ProbablyDontKnow),
            3 => Ok(Self::ProbablyKnow),
            4 => Ok(Self::CertainlyKnow),
            value => Err(RatingValueError::new(value)),
        }
    }
}

impl From<PromptGuess> for u8 {
    fn from(value: PromptGuess) -> Self {
        value.value()
    }
}

/// The user's feedback after seeing the response.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum RecallFeedback {
    Again = 1,
    Hard = 2,
    Good = 3,
    Easy = 4,
}

impl RecallFeedback {
    pub const MIN_VALUE: u8 = 1;
    pub const MAX_VALUE: u8 = 4;

    pub fn value(self) -> u8 {
        self as u8
    }
}

impl TryFrom<u8> for RecallFeedback {
    type Error = RatingValueError;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            1 => Ok(Self::Again),
            2 => Ok(Self::Hard),
            3 => Ok(Self::Good),
            4 => Ok(Self::Easy),
            value => Err(RatingValueError::new(value)),
        }
    }
}

impl From<RecallFeedback> for u8 {
    fn from(value: RecallFeedback) -> Self {
        value.value()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RatingValueError {
    value: u8,
}

impl RatingValueError {
    pub fn new(value: u8) -> Self {
        Self { value }
    }

    pub fn value(self) -> u8 {
        self.value
    }
}

impl fmt::Display for RatingValueError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "rating value must be between {} and {}, got {}",
            PromptGuess::MIN_VALUE,
            PromptGuess::MAX_VALUE,
            self.value
        )
    }
}

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

/// One completed exposure to a datum.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Review {
    prompt_guess: PromptGuess,
    recall_feedback: RecallFeedback,
}

impl Review {
    pub fn new(prompt_guess: PromptGuess, recall_feedback: RecallFeedback) -> Self {
        Self {
            prompt_guess,
            recall_feedback,
        }
    }

    pub fn prompt_guess(&self) -> PromptGuess {
        self.prompt_guess
    }

    pub fn recall_feedback(&self) -> RecallFeedback {
        self.recall_feedback
    }
}

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

    #[test]
    fn datum_requires_prompt_and_response() {
        assert_eq!(
            Datum::new("", "answer"),
            Err(DatumValidationError::EmptyPrompt)
        );
        assert_eq!(
            Datum::new("question", "   "),
            Err(DatumValidationError::EmptyResponse)
        );
    }

    #[test]
    fn datum_preserves_prompt_and_response() {
        let datum = Datum::new("capital of Spain", "Madrid").unwrap();

        assert_eq!(datum.prompt(), "capital of Spain");
        assert_eq!(datum.response(), "Madrid");
        assert_eq!(
            datum.into_parts(),
            ("capital of Spain".to_string(), "Madrid".to_string())
        );
    }

    #[test]
    fn datum_rejects_whitespace_only_prompt() {
        assert_eq!(
            Datum::new("   \n\t", "answer"),
            Err(DatumValidationError::EmptyPrompt)
        );
    }

    #[test]
    fn datum_key_is_stable_for_the_same_content() {
        let datum = Datum::new("capital of Spain", "Madrid").unwrap();
        let same_datum = Datum::new("capital of Spain", "Madrid").unwrap();
        let changed_datum = Datum::new("capital of Spain", "Madrid, Spain").unwrap();

        assert_eq!(
            DatumKey::for_datum(&datum),
            DatumKey::for_datum(&same_datum)
        );
        assert_ne!(
            DatumKey::for_datum(&datum),
            DatumKey::for_datum(&changed_datum)
        );
        assert_eq!(
            DatumKey::for_datum(&datum).as_str(),
            "fnv1a64:6a2d81ba73cc3266"
        );
    }

    #[test]
    fn datum_key_uses_card_id_when_present() {
        let datum = Datum::new("capital of Spain", "Madrid")
            .unwrap()
            .with_card_id(42);
        let changed_datum = Datum::new("capital of Spain", "Madrid, Spain")
            .unwrap()
            .with_card_id(42);

        assert_eq!(DatumKey::for_datum(&datum), DatumKey::for_card_id(42));
        assert_eq!(
            DatumKey::for_datum(&datum),
            DatumKey::for_datum(&changed_datum)
        );
        assert_eq!(DatumKey::for_datum(&datum).card_id(), Some(42));
    }

    #[test]
    fn datum_key_can_address_generated_reverse_card() {
        let parent = DatumKey::for_card_id(42);
        let reverse = DatumKey::for_reverse_of(&parent);

        assert_eq!(reverse.as_str(), "card:42:reverse");
        assert_eq!(reverse.parent_key_for_reverse(), Some(parent));
        assert!(reverse.is_reverse());
        assert_eq!(reverse.card_id(), None);
    }

    #[test]
    fn datum_key_depends_on_prompt_response_order() {
        let datum = Datum::new("question", "answer").unwrap();
        let reversed = Datum::new("answer", "question").unwrap();

        assert_ne!(DatumKey::for_datum(&datum), DatumKey::for_datum(&reversed));
    }

    #[test]
    fn datum_key_can_round_trip_through_storage() {
        let datum = Datum::new("Capital of Spain", "Madrid").unwrap();
        let key = DatumKey::for_datum(&datum);

        let loaded = DatumKey::from_storage_key(key.as_str());

        assert_eq!(loaded.as_str(), key.as_str());
        assert_eq!(loaded.to_string(), key.as_str());
        assert_eq!(loaded.into_string(), key.as_str().to_string());
    }

    #[test]
    fn prompt_guess_round_trips_numeric_values() {
        assert_eq!(PromptGuess::try_from(0), Ok(PromptGuess::NotAnswered));
        assert_eq!(PromptGuess::try_from(1), Ok(PromptGuess::DontKnow));
        assert_eq!(PromptGuess::try_from(2), Ok(PromptGuess::ProbablyDontKnow));
        assert_eq!(PromptGuess::try_from(3), Ok(PromptGuess::ProbablyKnow));
        assert_eq!(PromptGuess::try_from(4), Ok(PromptGuess::CertainlyKnow));
        assert_eq!(u8::from(PromptGuess::CertainlyKnow), 4);
    }

    #[test]
    fn recall_feedback_round_trips_numeric_values() {
        assert_eq!(RecallFeedback::try_from(1), Ok(RecallFeedback::Again));
        assert_eq!(RecallFeedback::try_from(2), Ok(RecallFeedback::Hard));
        assert_eq!(RecallFeedback::try_from(3), Ok(RecallFeedback::Good));
        assert_eq!(RecallFeedback::try_from(4), Ok(RecallFeedback::Easy));
        assert_eq!(u8::from(RecallFeedback::Easy), 4);
    }

    #[test]
    fn ratings_reject_out_of_range_values() {
        assert_eq!(PromptGuess::try_from(5), Err(RatingValueError::new(5)));
        assert_eq!(RecallFeedback::try_from(5), Err(RatingValueError::new(5)));
    }

    #[test]
    fn rating_value_error_reports_the_invalid_value() {
        let error = RatingValueError::new(9);

        assert_eq!(error.value(), 9);
        assert_eq!(
            error.to_string(),
            "rating value must be between 0 and 4, got 9"
        );
    }

    #[test]
    fn review_pairs_the_pre_response_guess_with_post_response_feedback() {
        let review = Review::new(PromptGuess::ProbablyKnow, RecallFeedback::Easy);

        assert_eq!(review.prompt_guess(), PromptGuess::ProbablyKnow);
        assert_eq!(review.recall_feedback(), RecallFeedback::Easy);
    }
}
