//! Endpoint URL newtype with HTTPS enforcement.
//!
//! Provides a validated URL type that ensures HTTPS usage in production
//! while allowing HTTP for localhost development (SEC-001).

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

use serde::{Deserialize, Serialize};

use crate::error::LastIDError;

/// A validated endpoint URL for the Identity Provider.
///
/// URLs must use HTTPS in production. HTTP is only allowed for localhost
/// development environments. This enforces SEC-001 requirements to prevent
/// credential data from being transmitted over insecure connections.
///
/// # Security
///
/// HTTP URLs are vulnerable to man-in-the-middle (MITM) attacks. Identity
/// verification data must always be transmitted over encrypted connections
/// in production environments.
///
/// # Examples
///
/// ```rust
/// use lastid_sdk::types::EndpointUrl;
///
/// // HTTPS URLs are always allowed
/// let url = EndpointUrl::new("https://human.lastid.co").unwrap();
///
/// // HTTP is only allowed for localhost (dev mode)
/// let dev_url = EndpointUrl::new("http://localhost:3000").unwrap();
/// assert!(dev_url.is_localhost());
///
/// // HTTP to remote hosts is rejected
/// assert!(EndpointUrl::new("http://example.com").is_err());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EndpointUrl(String);

impl EndpointUrl {
    /// Creates a new endpoint URL with HTTPS validation.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::InsecureUrl` if URL doesn't use HTTPS
    /// (except localhost for dev).
    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError> {
        let s = value.into();
        Self::validate(&s)?;
        Ok(Self(s))
    }

    /// Returns the URL as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns true if this is a localhost URL (dev mode).
    ///
    /// Localhost URLs are allowed to use HTTP for development convenience.
    #[must_use]
    pub fn is_localhost(&self) -> bool {
        self.0.starts_with("http://localhost")
            || self.0.starts_with("http://127.0.0.1")
            || self.0.starts_with("http://[::1]")
    }

    /// Returns true if this URL uses HTTPS.
    #[must_use]
    pub fn is_https(&self) -> bool {
        self.0.starts_with("https://")
    }

    /// Validates a URL string for security requirements.
    fn validate(s: &str) -> Result<(), LastIDError> {
        // Empty URL
        if s.is_empty() {
            return Err(LastIDError::insecure_url("URL cannot be empty"));
        }

        // HTTPS is always allowed
        if s.starts_with("https://") {
            return Ok(());
        }

        // HTTP is allowed for localhost development
        if s.starts_with("http://localhost")
            || s.starts_with("http://127.0.0.1")
            || s.starts_with("http://[::1]")
        {
            return Ok(());
        }

        // HTTP to any other host is rejected
        if s.starts_with("http://") {
            return Err(LastIDError::insecure_url(s));
        }

        // Invalid scheme
        Err(LastIDError::insecure_url(format!(
            "URL must use https:// or http://localhost: {s}"
        )))
    }

    /// Joins a path to this URL.
    ///
    /// Handles trailing/leading slashes appropriately.
    #[must_use]
    pub fn join(&self, path: &str) -> String {
        let base = self.0.trim_end_matches('/');
        let path = path.trim_start_matches('/');
        format!("{base}/{path}")
    }
}

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

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

impl From<EndpointUrl> for String {
    fn from(url: EndpointUrl) -> Self {
        url.0
    }
}

impl TryFrom<String> for EndpointUrl {
    type Error = LastIDError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl TryFrom<&str> for EndpointUrl {
    type Error = LastIDError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl FromStr for EndpointUrl {
    type Err = LastIDError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(s)
    }
}

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

    #[test]
    fn test_valid_https() {
        let url = EndpointUrl::new("https://human.lastid.co").unwrap();
        assert_eq!(url.as_str(), "https://human.lastid.co");
        assert!(url.is_https());
        assert!(!url.is_localhost());
    }

    #[test]
    fn test_valid_https_with_path() {
        let url = EndpointUrl::new("https://api.example.com/v1").unwrap();
        assert!(url.is_https());
    }

    #[test]
    fn test_valid_localhost() {
        let url = EndpointUrl::new("http://localhost:3000").unwrap();
        assert!(url.is_localhost());
        assert!(!url.is_https());
    }

    #[test]
    fn test_valid_localhost_no_port() {
        let url = EndpointUrl::new("http://localhost").unwrap();
        assert!(url.is_localhost());
    }

    #[test]
    fn test_valid_127_0_0_1() {
        let url = EndpointUrl::new("http://127.0.0.1:8080").unwrap();
        assert!(url.is_localhost());
    }

    #[test]
    fn test_valid_ipv6_localhost() {
        let url = EndpointUrl::new("http://[::1]:3000").unwrap();
        assert!(url.is_localhost());
    }

    #[test]
    fn test_invalid_http_remote() {
        assert!(EndpointUrl::new("http://example.com").is_err());
        assert!(EndpointUrl::new("http://api.lastid.co").is_err());
    }

    #[test]
    fn test_invalid_empty() {
        assert!(EndpointUrl::new("").is_err());
    }

    #[test]
    fn test_invalid_no_scheme() {
        assert!(EndpointUrl::new("example.com").is_err());
    }

    #[test]
    fn test_invalid_ftp() {
        assert!(EndpointUrl::new("ftp://files.example.com").is_err());
    }

    #[test]
    fn test_join() {
        let url = EndpointUrl::new("https://api.example.com").unwrap();
        assert_eq!(url.join("v1/users"), "https://api.example.com/v1/users");

        let url_with_slash = EndpointUrl::new("https://api.example.com/").unwrap();
        assert_eq!(
            url_with_slash.join("/v1/users"),
            "https://api.example.com/v1/users"
        );
    }

    #[test]
    fn test_display() {
        let url = EndpointUrl::new("https://test.com").unwrap();
        assert_eq!(format!("{url}"), "https://test.com");
    }

    #[test]
    fn test_from_str() {
        let url: EndpointUrl = "https://api.example.com".parse().unwrap();
        assert!(url.is_https());
    }

    #[test]
    fn test_try_from_string() {
        let url = EndpointUrl::try_from("https://test.com".to_string()).unwrap();
        assert_eq!(url.as_str(), "https://test.com");
    }

    #[test]
    fn test_into_string() {
        let url = EndpointUrl::new("https://test.com").unwrap();
        let s: String = url.into();
        assert_eq!(s, "https://test.com");
    }

    #[test]
    fn test_serde_roundtrip() {
        let url = EndpointUrl::new("https://human.lastid.co").unwrap();
        let json = serde_json::to_string(&url).unwrap();
        assert_eq!(json, "\"https://human.lastid.co\"");

        let deserialized: EndpointUrl = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, url);
    }

    #[test]
    fn test_serde_invalid_url() {
        let result: Result<EndpointUrl, _> = serde_json::from_str("\"http://remote.com\"");
        assert!(result.is_err());
    }
}
