//! Offline cryptographic license key verification.
//!
//! # Bounded Context: Licensing
//!
//! Owns key decoding, Ed25519 signature verification, expiry checks,
//! and claims extraction. Has no network dependencies — verification
//! is fully offline using the embedded public key.
//!
//! License keys are Ed25519-signed payloads containing customer info and
//! an expiry date. The binary embeds only the public key; forging a valid
//! key requires the private key, which never leaves the issuing server.
//!
//! # Key format
//!
//! A license key is a base64-encoded blob: `base64(json_payload || 64-byte Ed25519 signature)`.
//! The JSON payload is a [`LicenseClaims`] struct.

use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Length of an Ed25519 signature in bytes.
const SIGNATURE_LEN: usize = 64;

/// Ed25519 public key used to verify license keys.
///
/// Replace this with the real public key bytes generated by `tools/keygen.rs`.
/// This is a placeholder — the binary will not verify any key until a real
/// keypair is generated and this constant is updated.
///
/// Note: must NOT be all zeros (the Ed25519 identity point), which would
/// trivially verify any all-zero signature.
const PUBLIC_KEY_BYTES: [u8; 32] = [
    0x1e, 0xa6, 0x40, 0x7b, 0xc0, 0x82, 0x48, 0x49, 0xda, 0xe7, 0x78, 0x36, 0x58, 0xa3, 0x8e, 0xad,
    0x42, 0xdd, 0x1b, 0x2b, 0xe7, 0x78, 0x49, 0x09, 0x88, 0x03, 0x49, 0xc8, 0xe8, 0x0c, 0x84, 0x69,
];

/// Claims embedded in a signed license key.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseClaims {
    /// Display name of the licensee (person or company).
    pub customer_name: String,
    /// Unique customer identifier (e.g. UUID).
    pub customer_id: String,
    /// ISO 8601 date when the key was issued (YYYY-MM-DD).
    pub issued_at: String,
    /// ISO 8601 date when the key expires (YYYY-MM-DD).
    pub expires_at: String,
}

/// Result of checking a license's expiry against the current date.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExpiryStatus {
    /// License is valid and not expiring soon.
    Valid,
    /// License expires within the given number of days.
    ExpiringSoon { days: i64 },
    /// License has expired.
    Expired,
}

/// Errors that can occur during license verification.
#[derive(Error, Debug)]
pub enum LicenseError {
    #[error("invalid base64 encoding: {0}")]
    InvalidBase64(#[from] base64::DecodeError),

    #[error("key too short to contain a signature")]
    TooShort,

    #[error("invalid signature")]
    InvalidSignature,

    #[error("invalid public key (compile-time key is malformed)")]
    InvalidPublicKey,

    #[error("failed to parse license claims: {0}")]
    InvalidClaims(#[from] serde_json::Error),

    #[error("invalid expiry date format (expected YYYY-MM-DD): {0}")]
    InvalidDate(String),
}

/// Verify a license key string and return the embedded claims.
///
/// Decodes base64 → splits payload from trailing 64-byte signature →
/// verifies Ed25519 signature with the embedded public key →
/// deserializes the JSON payload into [`LicenseClaims`].
pub fn verify_license_key(key: &str) -> Result<LicenseClaims, LicenseError> {
    use base64::Engine;
    let bytes = base64::engine::general_purpose::STANDARD.decode(key.trim())?;

    if bytes.len() <= SIGNATURE_LEN {
        return Err(LicenseError::TooShort);
    }

    let (payload, sig_bytes) = bytes.split_at(bytes.len() - SIGNATURE_LEN);
    let signature = Signature::from_slice(sig_bytes).map_err(|_| LicenseError::InvalidSignature)?;

    let verifying_key =
        VerifyingKey::from_bytes(&PUBLIC_KEY_BYTES).map_err(|_| LicenseError::InvalidPublicKey)?;

    verifying_key
        .verify(payload, &signature)
        .map_err(|_| LicenseError::InvalidSignature)?;

    let claims: LicenseClaims = serde_json::from_slice(payload)?;
    Ok(claims)
}

/// Check whether a license's expiry date has passed or is approaching.
///
/// Returns [`ExpiryStatus::ExpiringSoon`] when ≤ 30 days remain.
pub fn check_expiry(claims: &LicenseClaims) -> Result<ExpiryStatus, LicenseError> {
    let expires = parse_date(&claims.expires_at)?;
    let today = today_ymd();
    let days_remaining = days_between(today, expires);

    if days_remaining < 0 {
        Ok(ExpiryStatus::Expired)
    } else if days_remaining <= 30 {
        Ok(ExpiryStatus::ExpiringSoon {
            days: days_remaining,
        })
    } else {
        Ok(ExpiryStatus::Valid)
    }
}

// ---------------------------------------------------------------------------
// Minimal date helpers (avoids a chrono / time dependency)
// ---------------------------------------------------------------------------

/// (year, month, day) tuple.
type Ymd = (i64, i64, i64);

/// Parse "YYYY-MM-DD" into a (year, month, day) tuple.
fn parse_date(s: &str) -> Result<Ymd, LicenseError> {
    let parts: Vec<&str> = s.split('-').collect();
    if parts.len() != 3 {
        return Err(LicenseError::InvalidDate(s.to_string()));
    }
    let y = parts[0]
        .parse::<i64>()
        .map_err(|_| LicenseError::InvalidDate(s.to_string()))?;
    let m = parts[1]
        .parse::<i64>()
        .map_err(|_| LicenseError::InvalidDate(s.to_string()))?;
    let d = parts[2]
        .parse::<i64>()
        .map_err(|_| LicenseError::InvalidDate(s.to_string()))?;
    Ok((y, m, d))
}

/// Get today's date as (year, month, day) in UTC.
fn today_ymd() -> Ymd {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    epoch_secs_to_ymd(secs)
}

/// Convert Unix epoch seconds to (year, month, day) in UTC.
/// Uses Howard Hinnant's civil_from_days algorithm.
fn epoch_secs_to_ymd(secs: i64) -> Ymd {
    let mut days = secs / 86400;
    if secs < 0 && secs % 86400 != 0 {
        days -= 1;
    }
    days += 719468;
    let era = if days >= 0 { days } else { days - 146096 } / 146097;
    let doe = days - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y, m, d)
}

/// Compute the number of days from `a` to `b` (positive if b is after a).
fn days_between(a: Ymd, b: Ymd) -> i64 {
    ymd_to_epoch_days(b) - ymd_to_epoch_days(a)
}

/// Convert (year, month, day) to days since Unix epoch.
fn ymd_to_epoch_days(date: Ymd) -> i64 {
    let (mut y, m, d) = date;
    if m <= 2 {
        y -= 1;
    }
    let era = if y >= 0 { y } else { y - 399 } / 400;
    let yoe = y - era * 400;
    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146097 + doe - 719468
}

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

    #[test]
    fn parse_valid_date() {
        assert_eq!(parse_date("2026-12-31").unwrap(), (2026, 12, 31));
        assert_eq!(parse_date("2025-01-01").unwrap(), (2025, 1, 1));
    }

    #[test]
    fn parse_invalid_date() {
        assert!(parse_date("not-a-date").is_err());
        assert!(parse_date("2026/12/31").is_err());
        assert!(parse_date("2026-13").is_err());
    }

    #[test]
    fn days_between_same_day() {
        let d = (2026, 2, 13);
        assert_eq!(days_between(d, d), 0);
    }

    #[test]
    fn days_between_one_day() {
        let a = (2026, 2, 13);
        let b = (2026, 2, 14);
        assert_eq!(days_between(a, b), 1);
        assert_eq!(days_between(b, a), -1);
    }

    #[test]
    fn days_between_across_months() {
        let a = (2026, 1, 31);
        let b = (2026, 3, 1);
        // 2026 is not a leap year: Feb has 28 days
        assert_eq!(days_between(a, b), 29);
    }

    #[test]
    fn epoch_round_trip() {
        let ymd = (2026, 2, 13);
        let days = ymd_to_epoch_days(ymd);
        let back = epoch_secs_to_ymd(days * 86400);
        assert_eq!(back, ymd);
    }

    #[test]
    fn expiry_status_valid() {
        let claims = LicenseClaims {
            customer_name: "Test".into(),
            customer_id: "test-id".into(),
            issued_at: "2026-01-01".into(),
            expires_at: "2099-12-31".into(),
        };
        assert_eq!(check_expiry(&claims).unwrap(), ExpiryStatus::Valid);
    }

    #[test]
    fn expiry_status_expired() {
        let claims = LicenseClaims {
            customer_name: "Test".into(),
            customer_id: "test-id".into(),
            issued_at: "2020-01-01".into(),
            expires_at: "2020-12-31".into(),
        };
        assert_eq!(check_expiry(&claims).unwrap(), ExpiryStatus::Expired);
    }

    #[test]
    fn expiry_status_expiring_soon() {
        // Build an expiry date 15 days from now.
        let today = today_ymd();
        let future_days = ymd_to_epoch_days(today) + 15;
        let future = epoch_secs_to_ymd(future_days * 86400);
        let expires_at = format!("{:04}-{:02}-{:02}", future.0, future.1, future.2);

        let claims = LicenseClaims {
            customer_name: "Test".into(),
            customer_id: "test-id".into(),
            issued_at: "2026-01-01".into(),
            expires_at,
        };
        match check_expiry(&claims).unwrap() {
            ExpiryStatus::ExpiringSoon { days } => assert!((0..=30).contains(&days)),
            other => panic!("expected ExpiringSoon, got {other:?}"),
        }
    }

    #[test]
    fn verify_rejects_garbage_input() {
        let result = verify_license_key("not-valid-base64!!!");
        assert!(result.is_err());
    }

    #[test]
    fn verify_rejects_too_short() {
        use base64::Engine;
        let short = base64::engine::general_purpose::STANDARD.encode(b"tiny");
        let result = verify_license_key(&short);
        assert!(matches!(result, Err(LicenseError::TooShort)));
    }

    #[test]
    fn verify_rejects_bad_signature() {
        use base64::Engine;
        let payload = br#"{"customer_name":"Test","customer_id":"x","issued_at":"2026-01-01","expires_at":"2099-12-31"}"#;
        let mut blob = payload.to_vec();
        blob.extend_from_slice(&[0u8; 64]);
        let encoded = base64::engine::general_purpose::STANDARD.encode(&blob);
        let result = verify_license_key(&encoded);
        // Fails at InvalidPublicKey (placeholder zeros) or InvalidSignature
        assert!(result.is_err());
    }
}
