//! Handler for creating epochs.

use axum::{extract::State, http::StatusCode, Json};
use tracing::instrument;

use crate::{
    dto::{CreateEpochRequest, CreateResponse, ErrorResponse},
    error::{ApiError, Result},
    hex,
    state::AppState,
};

use stemedb_core::types::Epoch;
use stemedb_ingest::worker::serialize_epoch;

/// Create a new epoch (paradigm) in the knowledge graph.
///
/// Epochs represent distinct periods of truth or worldviews. Assertions can
/// reference epochs to indicate which paradigm they belong to. Epochs can
/// supersede previous epochs via Invalidation, Temporal, or Refinement.
///
/// # Epoch ID Generation
///
/// The epoch ID is computed as BLAKE3 hash of a canonical string representation:
/// `"{name}:{supersedes_hex}:{timestamp}"`. This ensures:
/// - Deterministic IDs for the same input (same name + timestamp = same ID)
/// - IDs that won't change if the Epoch struct's serialization format changes
/// - Human-readable debugging via the canonical format
///
/// # Validation
///
/// - Name must be non-empty
/// - Supersedes field, if provided, must be a valid 32-byte hex-encoded hash
/// - If supersedes is provided, supersession_type is required
///
/// # Example Request (New Epoch)
///
/// ```json
/// {
///   "name": "GAAP-2024",
///   "start_timestamp": 1704067200
/// }
/// ```
///
/// # Example Request (Superseding an Epoch)
///
/// Use this same endpoint to supersede a previous epoch by providing the
/// `supersedes` field with the hex-encoded ID of the epoch being replaced:
///
/// ```json
/// {
///   "name": "post_fda_label_2024",
///   "supersedes": "a1b2c3d4e5f6...64-hex-chars...",
///   "supersession_type": "Invalidate"
/// }
/// ```
///
/// ## Supersession Types
///
/// - **Invalidate**: Previous epoch was factually incorrect (e.g., FDA withdraws approval)
/// - **Temporal**: Previous epoch was correct at the time but is now outdated (e.g., label update)
/// - **Refinement**: Previous epoch was a simplification; new one is more precise
/// - **RequiresReview**: Flagged for human review before resolution
/// - **Additive**: New epoch adds to (doesn't replace) the previous one
#[utoipa::path(
    post,
    path = "/v1/epoch",
    request_body = CreateEpochRequest,
    responses(
        (status = 201, description = "Epoch created successfully", body = CreateResponse),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    tag = "epochs"
)]
#[instrument(skip(state), fields(name = %req.name))]
pub async fn create_epoch(
    State(state): State<AppState>,
    Json(req): Json<CreateEpochRequest>,
) -> Result<(StatusCode, Json<CreateResponse>)> {
    // Convert DTO to internal Epoch type
    let epoch = dto_to_epoch(req)?;

    // Serialize to WAL format (includes record type header)
    let payload = serialize_epoch(&epoch)
        .map_err(|e| ApiError::Serialization(format!("Failed to serialize epoch: {}", e)))?;

    // The epoch ID is already computed from the content hash during dto_to_epoch
    // For the response, we return this same ID as a hex string
    let epoch_id_hex = ::hex::encode(epoch.id);

    // Append to WAL via group commit buffer
    state.commit_buffer.append(payload).await?;

    let response = CreateResponse { hash: epoch_id_hex, status: "created".to_string() };

    Ok((StatusCode::CREATED, Json(response)))
}

/// Convert CreateEpochRequest DTO to internal Epoch type.
///
/// Generates a content-addressed epoch ID by hashing a canonical string
/// representation: `"{name}:{supersedes_hex}:{timestamp}"`. This differs
/// from assertions (which hash serialized bytes) to provide stable IDs
/// that won't change with struct serialization format changes.
fn dto_to_epoch(req: CreateEpochRequest) -> Result<Epoch> {
    // Validate name is non-empty
    if req.name.trim().is_empty() {
        return Err(ApiError::InvalidRequest("Epoch name cannot be empty".to_string()));
    }

    // Decode supersedes epoch ID if provided
    let supersedes = req.supersedes.map(|h| hex::decode_hash_32(&h)).transpose()?;

    // Convert supersession_type if provided
    let supersession_type = req.supersession_type.map(Into::into);

    // Validate: if supersedes is provided, supersession_type should also be provided
    if supersedes.is_some() && supersession_type.is_none() {
        return Err(ApiError::InvalidRequest(
            "supersession_type is required when supersedes is provided".to_string(),
        ));
    }

    // Get timestamp (use provided or default to now)
    let start_timestamp = req.start_timestamp.unwrap_or_else(|| {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0)
    });

    // Create epoch with placeholder ID (will compute below)
    let mut epoch = Epoch {
        id: [0u8; 32], // Placeholder, computed below
        name: req.name,
        supersedes,
        supersession_type,
        start_timestamp,
        end_timestamp: None, // New epochs are open-ended
    };

    // Compute content-addressed ID by hashing a canonical string representation.
    // Using format "{name}:{supersedes_hex}:{timestamp}" to ensure deterministic
    // IDs that won't change with struct serialization format changes.
    let id_content = format!(
        "{}:{}:{}",
        epoch.name,
        epoch.supersedes.map(::hex::encode).unwrap_or_default(),
        epoch.start_timestamp
    );
    let id_hash = blake3::hash(id_content.as_bytes());
    epoch.id = *id_hash.as_bytes();

    Ok(epoch)
}

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

    #[test]
    fn test_dto_to_epoch_basic() {
        let req = CreateEpochRequest {
            name: "GAAP-2024".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: Some(1704067200),
        };

        let epoch = dto_to_epoch(req).expect("should convert");
        assert_eq!(epoch.name, "GAAP-2024");
        assert!(epoch.supersedes.is_none());
        assert!(epoch.supersession_type.is_none());
        assert_eq!(epoch.start_timestamp, 1704067200);
        assert!(epoch.end_timestamp.is_none());
        // ID should be computed (not all zeros)
        assert_ne!(epoch.id, [0u8; 32]);
    }

    #[test]
    fn test_dto_to_epoch_with_supersedes() {
        let supersedes_hex = "a".repeat(64);
        let req = CreateEpochRequest {
            name: "GAAP-2025".to_string(),
            supersedes: Some(supersedes_hex),
            supersession_type: Some(SupersessionTypeDto::Temporal),
            start_timestamp: Some(1735689600),
        };

        let epoch = dto_to_epoch(req).expect("should convert");
        assert_eq!(epoch.name, "GAAP-2025");
        assert!(epoch.supersedes.is_some());
        assert!(epoch.supersession_type.is_some());
    }

    #[test]
    fn test_dto_to_epoch_empty_name_fails() {
        let req = CreateEpochRequest {
            name: "".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: None,
        };

        let result = dto_to_epoch(req);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("cannot be empty"));
    }

    #[test]
    fn test_dto_to_epoch_whitespace_name_fails() {
        let req = CreateEpochRequest {
            name: "   ".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: None,
        };

        let result = dto_to_epoch(req);
        assert!(result.is_err());
    }

    #[test]
    fn test_dto_to_epoch_supersedes_without_type_fails() {
        let supersedes_hex = "a".repeat(64);
        let req = CreateEpochRequest {
            name: "Test".to_string(),
            supersedes: Some(supersedes_hex),
            supersession_type: None, // Missing!
            start_timestamp: None,
        };

        let result = dto_to_epoch(req);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("supersession_type is required"));
    }

    #[test]
    fn test_dto_to_epoch_invalid_supersedes_hex() {
        let req = CreateEpochRequest {
            name: "Test".to_string(),
            supersedes: Some("invalid".to_string()), // Too short
            supersession_type: Some(SupersessionTypeDto::Invalidate),
            start_timestamp: None,
        };

        let result = dto_to_epoch(req);
        assert!(result.is_err());
    }

    #[test]
    fn test_dto_to_epoch_defaults_timestamp() {
        let req = CreateEpochRequest {
            name: "Now".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: None, // Should default to now
        };

        let epoch = dto_to_epoch(req).expect("should convert");
        // Timestamp should be recent (within last minute)
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);
        assert!(epoch.start_timestamp <= now);
        assert!(epoch.start_timestamp > now - 60);
    }

    #[test]
    fn test_epoch_id_is_deterministic() {
        let req1 = CreateEpochRequest {
            name: "Test".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: Some(1000),
        };
        let req2 = CreateEpochRequest {
            name: "Test".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: Some(1000),
        };

        let epoch1 = dto_to_epoch(req1).expect("should convert");
        let epoch2 = dto_to_epoch(req2).expect("should convert");

        // Same input should produce same ID
        assert_eq!(epoch1.id, epoch2.id);
    }

    #[test]
    fn test_epoch_id_differs_by_name() {
        let req1 = CreateEpochRequest {
            name: "Epoch-A".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: Some(1000),
        };
        let req2 = CreateEpochRequest {
            name: "Epoch-B".to_string(),
            supersedes: None,
            supersession_type: None,
            start_timestamp: Some(1000),
        };

        let epoch1 = dto_to_epoch(req1).expect("should convert");
        let epoch2 = dto_to_epoch(req2).expect("should convert");

        // Different names should produce different IDs
        assert_ne!(epoch1.id, epoch2.id);
    }
}
