//! Shared vocabulary for the admin bulk-action endpoint (`POST
//! /web/cases/bulk`).
//!
//! The wire shape is `application/x-www-form-urlencoded` with an
//! `action=<token>` field and one or more `case_id=<uuid>` repetitions.
//! Keeping the set of accepted tokens in a single enum lets the server
//! handler match exhaustively and lets tests build request bodies
//! without string literals that can silently drift from the handler's
//! accepted values.

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

/// Which operation a bulk POST applies to every selected case.
///
/// Serialized as lowercase `"close" | "analyze" | "reset"`. The server
/// handler [`BulkAction::from_str`]-parses the raw form value so an
/// unknown token produces a domain error ("Unbekannte Aktion") instead
/// of a serde/extractor error — preserving the HTTP contract across
/// refactors.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BulkAction {
    /// Mark each case closed (write `.closed` marker).
    Close,
    /// Queue each case for LLM analysis.
    Analyze,
    /// Drop derived artefacts (oneliner, document, analysis input) and
    /// revert `.m4a.failed` to `.m4a` so transcription self-heal retries.
    Reset,
}

impl BulkAction {
    /// Lowercase on-the-wire token. Intended for test request bodies
    /// and redirect targets; the server's own deserializer goes through
    /// [`FromStr`] to preserve the exact error shape.
    pub const fn as_str(self) -> &'static str {
        match self {
            BulkAction::Close => "close",
            BulkAction::Analyze => "analyze",
            BulkAction::Reset => "reset",
        }
    }
}

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

/// Returned by [`BulkAction::from_str`] on an unknown token. Carries
/// the offending input so the caller can log it without re-capturing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownBulkAction(pub String);

impl fmt::Display for UnknownBulkAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unknown bulk action: {}", self.0)
    }
}

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

impl FromStr for BulkAction {
    type Err = UnknownBulkAction;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "close" => Ok(BulkAction::Close),
            "analyze" => Ok(BulkAction::Analyze),
            "reset" => Ok(BulkAction::Reset),
            other => Err(UnknownBulkAction(other.to_string())),
        }
    }
}

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

    #[test]
    fn round_trip_lowercase_tokens() {
        for v in [BulkAction::Close, BulkAction::Analyze, BulkAction::Reset] {
            assert_eq!(v, v.as_str().parse().unwrap());
        }
    }

    #[test]
    fn wire_tokens_are_stable() {
        assert_eq!(BulkAction::Close.as_str(), "close");
        assert_eq!(BulkAction::Analyze.as_str(), "analyze");
        assert_eq!(BulkAction::Reset.as_str(), "reset");
    }

    #[test]
    fn unknown_token_reports_input() {
        let err = "foo".parse::<BulkAction>().unwrap_err();
        assert_eq!(err.0, "foo");
    }

    #[test]
    fn serde_matches_wire() {
        let json = serde_json::to_string(&BulkAction::Analyze).unwrap();
        assert_eq!(json, "\"analyze\"");
        let back: BulkAction = serde_json::from_str("\"reset\"").unwrap();
        assert_eq!(back, BulkAction::Reset);
    }
}
