//! Handlers for authored claims management.
//!
//! These endpoints provide CRUD operations for `.aphoria/claims.toml` plus
//! verification and coverage analysis.

use std::path::{Path, PathBuf};

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

use aphoria::claims_file::ClaimsFile;
use aphoria::{
    compute_coverage, parse_authority_tier, run_scan, verify_claims, AphoriaConfig, AuthoredClaim,
    AuthoredValue, ClaimStatus, ComparisonMode, FileSource, ScanArgs, ScanMode, VerifyReport,
};

use crate::dto::aphoria::{
    AcknowledgeViolationRequest, AcknowledgeViolationResponse, AuditVerdictDto, AuthoredClaimDto,
    AuthoredValueDto, ClaimStatusDto, ComparisonModeDto, CoverageReportResponse, CoverageRequest,
    CoverageSummaryDto, CreateClaimRequest, CreateClaimResponse, DeprecateClaimRequest,
    DeprecateClaimResponse, ListClaimsRequest, ListClaimsResponse, ModuleCoverageDto,
    UpdateClaimRequest, UpdateClaimResponse, VerifyClaimsRequest, VerifyReportResponse,
    VerifyResultDto, VerifySummaryDto,
};
use crate::AppState;

// ============================================================================
// List Claims
// ============================================================================

/// List authored claims from `.aphoria/claims.toml`.
///
/// Returns an empty list (not 404) if the file doesn't exist.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/list",
    request_body = ListClaimsRequest,
    responses(
        (status = 200, description = "Claims retrieved successfully", body = ListClaimsResponse),
        (status = 404, description = "Project path not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn list_claims(
    State(_state): State<AppState>,
    Json(req): Json<ListClaimsRequest>,
) -> Result<Json<ListClaimsResponse>, (StatusCode, String)> {
    info!(project_path = %req.project_path, "Listing claims");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    let claims_path = ClaimsFile::default_path(&project_root);

    // Load claims file (returns empty file if missing)
    let claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    let total = claims_file.len();

    // Apply filters
    let mut filtered = claims_file.claims.clone();

    if let Some(ref category) = req.category {
        filtered.retain(|c| c.category.eq_ignore_ascii_case(category));
    }

    if let Some(ref status) = req.status {
        let status_lower = status.to_lowercase();
        filtered.retain(|c| match c.status {
            ClaimStatus::Active => status_lower == "active",
            ClaimStatus::Deprecated => status_lower == "deprecated",
            ClaimStatus::Superseded => status_lower == "superseded",
        });
    }

    // Convert to DTOs
    let claims_dto: Vec<AuthoredClaimDto> = filtered.into_iter().map(claim_to_dto).collect();

    Ok(Json(ListClaimsResponse { claims: claims_dto, total }))
}

// ============================================================================
// Create Claim
// ============================================================================

/// Create a new authored claim and append to `.aphoria/claims.toml`.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/create",
    request_body = CreateClaimRequest,
    responses(
        (status = 200, description = "Claim created successfully", body = CreateClaimResponse),
        (status = 400, description = "Invalid request"),
        (status = 404, description = "Project path not found"),
        (status = 409, description = "Claim ID already exists"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn create_claim(
    State(_state): State<AppState>,
    Json(req): Json<CreateClaimRequest>,
) -> Result<Json<CreateClaimResponse>, (StatusCode, String)> {
    info!(claim_id = %req.id, project_path = %req.project_path, "Creating claim");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    // Validate authority tier
    let _authority_tier_class = parse_authority_tier(&req.authority_tier)
        .map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid authority tier: {e}")))?;

    // Parse comparison mode
    let comparison = parse_comparison_mode(&req.comparison)?;

    // Parse value
    let value = AuthoredValue::parse(&req.value);

    // Build the claim
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_err(|e| {
            error!(error = %e, "System clock error");
            (StatusCode::INTERNAL_SERVER_ERROR, "System clock error".to_string())
        })?
        .as_secs();
    let now_iso = format_timestamp(now);

    let claim = AuthoredClaim {
        id: req.id.clone(),
        concept_path: req.concept_path,
        predicate: req.predicate,
        value,
        comparison,
        provenance: req.provenance,
        invariant: req.invariant,
        consequence: req.consequence,
        authority_tier: req.authority_tier,
        evidence: req.evidence,
        category: req.category,
        status: ClaimStatus::Active,
        supersedes: None,
        created_by: req.created_by,
        created_at: now_iso,
        updated_at: None,
    };

    // Load existing claims
    let claims_path = ClaimsFile::default_path(&project_root);
    let mut claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    // Check for duplicate ID
    if claims_file.claims.iter().any(|c| c.id == claim.id) {
        return Err((StatusCode::CONFLICT, format!("Claim ID '{}' already exists", claim.id)));
    }

    // Add and save
    claims_file.claims.push(claim.clone());
    claims_file.save(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to save claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save claims: {e}"))
    })?;

    Ok(Json(CreateClaimResponse {
        success: true,
        message: format!("Claim '{}' created successfully", claim.id),
        claim: claim_to_dto(claim),
    }))
}

// ============================================================================
// Update Claim
// ============================================================================

/// Update an existing claim.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/update",
    request_body = UpdateClaimRequest,
    responses(
        (status = 200, description = "Claim updated successfully", body = UpdateClaimResponse),
        (status = 404, description = "Claim not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn update_claim(
    State(_state): State<AppState>,
    Json(req): Json<UpdateClaimRequest>,
) -> Result<Json<UpdateClaimResponse>, (StatusCode, String)> {
    info!(claim_id = %req.claim_id, project_path = %req.project_path, "Updating claim");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    let claims_path = ClaimsFile::default_path(&project_root);
    let mut claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    // Find the claim
    let claim =
        claims_file.claims.iter_mut().find(|c| c.id == req.claim_id).ok_or_else(|| {
            (StatusCode::NOT_FOUND, format!("Claim '{}' not found", req.claim_id))
        })?;

    // Apply updates
    if let Some(provenance) = req.provenance {
        claim.provenance = provenance;
    }
    if let Some(invariant) = req.invariant {
        claim.invariant = invariant;
    }
    if let Some(consequence) = req.consequence {
        claim.consequence = consequence;
    }
    if let Some(evidence) = req.evidence {
        claim.evidence = evidence;
    }

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_err(|e| {
            error!(error = %e, "System clock error");
            (StatusCode::INTERNAL_SERVER_ERROR, "System clock error".to_string())
        })?
        .as_secs();
    claim.updated_at = Some(format_timestamp(now));

    let updated_claim = claim.clone();

    // Save
    claims_file.save(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to save claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save claims: {e}"))
    })?;

    Ok(Json(UpdateClaimResponse {
        success: true,
        message: format!("Claim '{}' updated successfully", req.claim_id),
        claim: claim_to_dto(updated_claim),
    }))
}

// ============================================================================
// Deprecate Claim
// ============================================================================

/// Mark a claim as deprecated.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/deprecate",
    request_body = DeprecateClaimRequest,
    responses(
        (status = 200, description = "Claim deprecated successfully", body = DeprecateClaimResponse),
        (status = 404, description = "Claim not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn deprecate_claim(
    State(_state): State<AppState>,
    Json(req): Json<DeprecateClaimRequest>,
) -> Result<Json<DeprecateClaimResponse>, (StatusCode, String)> {
    info!(claim_id = %req.claim_id, project_path = %req.project_path, "Deprecating claim");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    let claims_path = ClaimsFile::default_path(&project_root);
    let mut claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    // Find the claim
    let claim =
        claims_file.claims.iter_mut().find(|c| c.id == req.claim_id).ok_or_else(|| {
            (StatusCode::NOT_FOUND, format!("Claim '{}' not found", req.claim_id))
        })?;

    claim.status = ClaimStatus::Deprecated;
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_err(|e| {
            error!(error = %e, "System clock error");
            (StatusCode::INTERNAL_SERVER_ERROR, "System clock error".to_string())
        })?
        .as_secs();
    claim.updated_at = Some(format_timestamp(now));

    // Append reason to consequence field for audit trail
    claim.consequence = format!("{}\n\nDeprecated: {}", claim.consequence, req.reason);

    let deprecated_claim = claim.clone();

    // Save
    claims_file.save(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to save claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save claims: {e}"))
    })?;

    Ok(Json(DeprecateClaimResponse {
        success: true,
        message: format!("Claim '{}' deprecated successfully", req.claim_id),
        claim: claim_to_dto(deprecated_claim),
    }))
}

// ============================================================================
// Verify Claims
// ============================================================================

/// Verify authored claims against observations extracted from code.
///
/// Runs a scan, extracts observations, then compares them against claims
/// in `.aphoria/claims.toml` to produce pass/conflict/missing verdicts.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/verify",
    request_body = VerifyClaimsRequest,
    responses(
        (status = 200, description = "Verification complete", body = VerifyReportResponse),
        (status = 404, description = "Project path not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn verify_claims_handler(
    State(_state): State<AppState>,
    Json(req): Json<VerifyClaimsRequest>,
) -> Result<Json<VerifyReportResponse>, (StatusCode, String)> {
    info!(project_path = %req.project_path, "Verifying claims");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    // Load claims
    let claims_path = ClaimsFile::default_path(&project_root);
    let claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    if claims_file.is_empty() {
        // No claims = empty report
        return Ok(Json(VerifyReportResponse {
            results: vec![],
            summary: VerifySummaryDto {
                total_claims: 0,
                pass: 0,
                conflict: 0,
                missing: 0,
                unclaimed: 0,
            },
        }));
    }

    // Run a scan to extract observations
    let config = load_config(&project_root)?;
    let scan_args = ScanArgs {
        path: project_root.clone(),
        format: "json".to_string(),
        exit_code_enabled: false,
        mode: ScanMode::Ephemeral, // Fast, no persistence
        debug: false,
        sync: false,
        strict: false,
        file_source: FileSource::All,
        benchmark: false,
        show_claims: false,
    };

    let scan_result = run_scan(scan_args, &config).await.map_err(|e| {
        error!(error = %e, "Scan failed");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Scan failed: {e}"))
    })?;

    // Run verification
    let report = verify_claims(&claims_file.claims, &scan_result.observations);

    Ok(Json(verify_report_to_dto(report)))
}

// ============================================================================
// Coverage
// ============================================================================

/// Compute coverage metrics for authored claims.
///
/// Shows per-module breakdown of how many observations have claims,
/// how many claims have observations, and coverage percentages.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/coverage",
    request_body = CoverageRequest,
    responses(
        (status = 200, description = "Coverage computed successfully", body = CoverageReportResponse),
        (status = 404, description = "Project path not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn coverage(
    State(_state): State<AppState>,
    Json(req): Json<CoverageRequest>,
) -> Result<Json<CoverageReportResponse>, (StatusCode, String)> {
    info!(project_path = %req.project_path, "Computing coverage");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    // Load claims
    let claims_path = ClaimsFile::default_path(&project_root);
    let claims_file = ClaimsFile::load(&claims_path).map_err(|e| {
        error!(error = %e, "Failed to load claims file");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load claims: {e}"))
    })?;

    // Run a scan to extract observations
    let config = load_config(&project_root)?;
    let scan_args = ScanArgs {
        path: project_root.clone(),
        format: "json".to_string(),
        exit_code_enabled: false,
        mode: ScanMode::Ephemeral,
        debug: false,
        sync: false,
        strict: false,
        file_source: FileSource::All,
        benchmark: false,
        show_claims: false,
    };

    let scan_result = run_scan(scan_args, &config).await.map_err(|e| {
        error!(error = %e, "Scan failed");
        (StatusCode::INTERNAL_SERVER_ERROR, format!("Scan failed: {e}"))
    })?;

    // Compute coverage
    let project_name =
        project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();
    let coverage_report =
        compute_coverage(&claims_file.claims, &scan_result.observations, &project_name);

    Ok(Json(coverage_report_to_dto(coverage_report)))
}

// ============================================================================
// Acknowledge Violation
// ============================================================================

/// Acknowledge a claim violation (adds to `.aphoria/acks.toml`).
///
/// Note: This is a placeholder. Full ACK file implementation is in
/// `applications/aphoria/src/ack_file.rs` but not yet integrated.
#[utoipa::path(
    post,
    path = "/v1/aphoria/claims/acknowledge",
    request_body = AcknowledgeViolationRequest,
    responses(
        (status = 200, description = "Violation acknowledged", body = AcknowledgeViolationResponse),
        (status = 404, description = "Project path not found"),
        (status = 500, description = "Internal server error")
    ),
    tag = "aphoria"
)]
pub async fn acknowledge_violation(
    State(_state): State<AppState>,
    Json(req): Json<AcknowledgeViolationRequest>,
) -> Result<Json<AcknowledgeViolationResponse>, (StatusCode, String)> {
    info!(claim_id = %req.claim_id, project_path = %req.project_path, "Acknowledging violation");

    let project_root = PathBuf::from(&req.project_path);
    if !project_root.exists() {
        return Err((
            StatusCode::NOT_FOUND,
            format!("Project path not found: {}", req.project_path),
        ));
    }

    // TODO: Load AckFile, add acknowledgment, save
    // For now, just return success

    Ok(Json(AcknowledgeViolationResponse {
        success: true,
        message: format!(
            "Violation for claim '{}' acknowledged (Note: ACK file integration pending)",
            req.claim_id
        ),
    }))
}

// ============================================================================
// Conversion Helpers
// ============================================================================

fn claim_to_dto(claim: AuthoredClaim) -> AuthoredClaimDto {
    AuthoredClaimDto {
        id: claim.id,
        concept_path: claim.concept_path,
        predicate: claim.predicate,
        value: authored_value_to_dto(claim.value),
        comparison: comparison_mode_to_dto(claim.comparison),
        provenance: claim.provenance,
        invariant: claim.invariant,
        consequence: claim.consequence,
        authority_tier: claim.authority_tier,
        evidence: claim.evidence,
        category: claim.category,
        status: claim_status_to_dto(claim.status),
        supersedes: claim.supersedes,
        created_by: claim.created_by,
        created_at: claim.created_at,
        updated_at: claim.updated_at,
    }
}

fn authored_value_to_dto(value: AuthoredValue) -> AuthoredValueDto {
    match value {
        AuthoredValue::Bool(b) => AuthoredValueDto::Bool(b),
        AuthoredValue::Number(n) => AuthoredValueDto::Number(n),
        AuthoredValue::Text(s) => AuthoredValueDto::Text(s),
    }
}

fn comparison_mode_to_dto(mode: ComparisonMode) -> ComparisonModeDto {
    match mode {
        ComparisonMode::Equals => ComparisonModeDto::Equals,
        ComparisonMode::NotEquals => ComparisonModeDto::NotEquals,
        ComparisonMode::Present => ComparisonModeDto::Present,
        ComparisonMode::Absent => ComparisonModeDto::Absent,
        ComparisonMode::Contains => ComparisonModeDto::Contains,
        ComparisonMode::NotContains => ComparisonModeDto::NotContains,
    }
}

fn claim_status_to_dto(status: ClaimStatus) -> ClaimStatusDto {
    match status {
        ClaimStatus::Active => ClaimStatusDto::Active,
        ClaimStatus::Deprecated => ClaimStatusDto::Deprecated,
        ClaimStatus::Superseded => ClaimStatusDto::Superseded,
    }
}

fn parse_comparison_mode(s: &str) -> Result<ComparisonMode, (StatusCode, String)> {
    match s.to_lowercase().as_str() {
        "equals" => Ok(ComparisonMode::Equals),
        "not_equals" => Ok(ComparisonMode::NotEquals),
        "present" => Ok(ComparisonMode::Present),
        "absent" => Ok(ComparisonMode::Absent),
        _ => Err((
            StatusCode::BAD_REQUEST,
            format!(
                "Invalid comparison mode '{}'. Expected: equals, not_equals, present, absent",
                s
            ),
        )),
    }
}

fn verify_report_to_dto(report: VerifyReport) -> VerifyReportResponse {
    VerifyReportResponse {
        results: report.results.into_iter().map(verify_result_to_dto).collect(),
        summary: VerifySummaryDto {
            total_claims: report.summary.total_claims,
            pass: report.summary.pass,
            conflict: report.summary.conflict,
            missing: report.summary.missing,
            unclaimed: report.summary.unclaimed,
        },
    }
}

fn verify_result_to_dto(result: aphoria::verify::VerifyResult) -> VerifyResultDto {
    VerifyResultDto {
        claim: result.claim.map(claim_to_dto),
        verdict: audit_verdict_to_dto(result.verdict),
        matching_observation_count: result.matching_observations.len(),
        explanation: result.explanation,
    }
}

fn audit_verdict_to_dto(verdict: aphoria::verify::AuditVerdict) -> AuditVerdictDto {
    use aphoria::verify::AuditVerdict;
    match verdict {
        AuditVerdict::Pass => AuditVerdictDto::Pass,
        AuditVerdict::Conflict => AuditVerdictDto::Conflict,
        AuditVerdict::Missing => AuditVerdictDto::Missing,
        AuditVerdict::Unclaimed => AuditVerdictDto::Unclaimed,
    }
}

fn coverage_report_to_dto(report: aphoria::coverage::CoverageReport) -> CoverageReportResponse {
    CoverageReportResponse {
        project: report.project,
        modules: report.modules.into_iter().map(module_coverage_to_dto).collect(),
        summary: coverage_summary_to_dto(report.summary),
    }
}

fn module_coverage_to_dto(mc: aphoria::coverage::ModuleCoverage) -> ModuleCoverageDto {
    ModuleCoverageDto {
        module_path: mc.module_path,
        files: mc.files,
        observation_count: mc.observation_count,
        claim_count: mc.claim_count,
        claimed_observations: mc.claimed_observations,
        unclaimed_observations: mc.unclaimed_observations,
        missing_claims: mc.missing_claims,
        density: mc.density,
    }
}

fn coverage_summary_to_dto(cs: aphoria::coverage::CoverageSummary) -> CoverageSummaryDto {
    CoverageSummaryDto {
        total_observations: cs.total_observations,
        total_claims: cs.total_claims,
        claimed_percentage: cs.claimed_percentage,
        unclaimed_count: cs.unclaimed_count,
        modules_with_claims: cs.modules_with_claims,
        modules_without_claims: cs.modules_without_claims,
    }
}

fn load_config(project_root: &Path) -> Result<AphoriaConfig, (StatusCode, String)> {
    // Try to load project-local config, fallback to default
    let config_path = project_root.join(".aphoria").join("config.toml");
    if config_path.exists() {
        AphoriaConfig::from_file(&config_path).map_err(|e| {
            error!(error = %e, "Failed to load config");
            (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to load config: {e}"))
        })
    } else {
        Ok(AphoriaConfig::default())
    }
}

/// Format a Unix timestamp as an ISO 8601 string (UTC).
fn format_timestamp(timestamp: u64) -> String {
    // Simple ISO 8601 formatting: YYYY-MM-DDTHH:MM:SSZ
    // This is a minimal implementation; for production, consider using chrono
    let days_since_epoch = timestamp / 86400;
    let secs_today = timestamp % 86400;

    // Compute year, month, day (simplified algorithm)
    let mut year = 1970;
    let mut days_left = days_since_epoch;
    loop {
        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
        if days_left < days_in_year {
            break;
        }
        days_left -= days_in_year;
        year += 1;
    }

    let (month, day) = day_to_month_day(days_left as u32, is_leap_year(year));

    let hour = secs_today / 3600;
    let minute = (secs_today % 3600) / 60;
    let second = secs_today % 60;

    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, month, day, hour, minute, second)
}

fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

fn day_to_month_day(day_of_year: u32, is_leap: bool) -> (u32, u32) {
    let days_in_months = if is_leap {
        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    } else {
        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    };

    let mut days_left = day_of_year;
    for (i, &days_in_month) in days_in_months.iter().enumerate() {
        if days_left < days_in_month {
            return ((i + 1) as u32, days_left + 1);
        }
        days_left -= days_in_month;
    }
    (12, 31) // Fallback
}
