//! Zone types for DNS

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Unique zone identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ZoneId(Uuid);

impl ZoneId {
    /// Create a new random zone ID
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }

    /// Create from existing UUID
    pub fn from_uuid(id: Uuid) -> Self {
        Self(id)
    }

    /// Get the underlying UUID
    pub fn as_uuid(&self) -> &Uuid {
        &self.0
    }
}

impl Default for ZoneId {
    fn default() -> Self {
        Self::new()
    }
}

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

impl std::str::FromStr for ZoneId {
    type Err = uuid::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self(Uuid::parse_str(s)?))
    }
}

/// Validated zone name (DNS domain name)
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ZoneName(String);

impl ZoneName {
    /// Create a new zone name with validation
    pub fn new(name: impl Into<String>) -> Result<Self, &'static str> {
        let name = name.into();

        // Basic DNS name validation
        if name.is_empty() {
            return Err("zone name cannot be empty");
        }

        if name.len() > 253 {
            return Err("zone name cannot exceed 253 characters");
        }

        // Each label must be 1-63 chars
        for label in name.trim_end_matches('.').split('.') {
            if label.is_empty() {
                return Err("zone name cannot have empty labels");
            }
            if label.len() > 63 {
                return Err("zone label cannot exceed 63 characters");
            }
            // Labels must start and end with alphanumeric
            if !label.chars().next().unwrap().is_ascii_alphanumeric() {
                return Err("zone label must start with alphanumeric");
            }
            if !label.chars().last().unwrap().is_ascii_alphanumeric() {
                return Err("zone label must end with alphanumeric");
            }
            // Labels can only contain alphanumeric and hyphens
            if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
                return Err("zone label can only contain alphanumeric and hyphens");
            }
        }

        // Normalize: ensure trailing dot
        let normalized = if name.ends_with('.') {
            name
        } else {
            format!("{}.", name)
        };

        Ok(Self(normalized.to_lowercase()))
    }

    /// Get the zone name as a string slice (with trailing dot)
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Get the zone name without trailing dot
    pub fn without_dot(&self) -> &str {
        self.0.trim_end_matches('.')
    }
}

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

impl AsRef<str> for ZoneName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

/// Zone status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ZoneStatus {
    /// Zone is active and serving queries
    #[default]
    Active,
    /// Zone is being created/provisioned
    Creating,
    /// Zone is disabled (not serving queries)
    Disabled,
    /// Zone is being deleted
    Deleting,
}

/// A DNS zone containing records
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Zone {
    /// Unique zone identifier
    pub id: ZoneId,
    /// Zone name (domain name with trailing dot)
    pub name: ZoneName,
    /// Organization ID (tenant)
    pub org_id: String,
    /// Project ID (scope)
    pub project_id: String,
    /// Zone status
    pub status: ZoneStatus,
    /// SOA serial number
    pub serial: u32,
    /// SOA refresh interval (seconds)
    pub refresh: u32,
    /// SOA retry interval (seconds)
    pub retry: u32,
    /// SOA expire time (seconds)
    pub expire: u32,
    /// SOA minimum TTL (seconds)
    pub minimum: u32,
    /// Primary nameserver
    pub primary_ns: String,
    /// Admin email (SOA rname)
    pub admin_email: String,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
    /// Last modified timestamp
    pub updated_at: DateTime<Utc>,
    /// Record count
    pub record_count: u64,
}

impl Zone {
    /// Create a new zone with defaults
    pub fn new(
        name: ZoneName,
        org_id: impl Into<String>,
        project_id: impl Into<String>,
    ) -> Self {
        let now = Utc::now();
        let serial = now.timestamp() as u32;
        Self {
            id: ZoneId::new(),
            name,
            org_id: org_id.into(),
            project_id: project_id.into(),
            status: ZoneStatus::Active,
            serial,
            refresh: 7200,   // 2 hours
            retry: 3600,     // 1 hour
            expire: 1209600, // 2 weeks
            minimum: 3600,   // 1 hour
            primary_ns: "ns1.flashdns.local.".to_string(),
            admin_email: "hostmaster.flashdns.local.".to_string(),
            created_at: now,
            updated_at: now,
            record_count: 0,
        }
    }

    /// Increment serial number
    pub fn increment_serial(&mut self) {
        self.serial = self.serial.wrapping_add(1);
        self.updated_at = Utc::now();
    }
}

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

    #[test]
    fn test_zone_name_validation() {
        assert!(ZoneName::new("example.com").is_ok());
        assert!(ZoneName::new("example.com.").is_ok());
        assert!(ZoneName::new("sub.example.com").is_ok());
        assert!(ZoneName::new("").is_err()); // empty
        assert!(ZoneName::new("-invalid.com").is_err()); // starts with hyphen
    }

    #[test]
    fn test_zone_name_normalization() {
        let name = ZoneName::new("EXAMPLE.COM").unwrap();
        assert_eq!(name.as_str(), "example.com.");
    }

    #[test]
    fn test_zone_id() {
        let id = ZoneId::new();
        assert!(!id.to_string().is_empty());
    }
}
