use once_cell::sync::Lazy;
use regex::Regex;

use crate::error::PathError;

const PATH_GUIDE: &str = concat!(
    "hint: expected format m/ssh/<principal>/<domain>/<host>[/<bucket>]\n",
    "example: m/ssh/ops:deploy/example.com/bastion.example.com/20240101"
);

#[derive(Clone, Debug)]
pub struct Path {
    canonical: String,
    segments: Vec<String>,
    bucket: Option<String>,
}

static PRINCIPAL_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^[a-z0-9][a-z0-9:_\-\.]*$").expect("principal regex"));
static LABEL_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"^[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?$").expect("label regex"));

fn path_error(message: impl Into<String>) -> PathError {
    PathError::new(format!("{}\n{}", message.into(), PATH_GUIDE))
}

pub fn parse_path(input: &str) -> Result<Path, PathError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(path_error("hdpki: empty derivation path"));
    }

    let parts: Vec<&str> = trimmed.split('/').collect();
    if parts.len() < 4 {
        return Err(path_error(format!(
            "hdpki: path {input:?} too short; expected at least m/ssh/<principal>/<domain>/<host>"
        )));
    }
    if parts.iter().any(|part| part.is_empty()) {
        return Err(path_error(format!(
            "hdpki: empty path segment in {input:?}"
        )));
    }
    if parts[0] != "m" {
        return Err(path_error(format!(
            "hdpki: path must start with m: {input:?}"
        )));
    }
    if parts[1].to_ascii_lowercase() != "ssh" {
        return Err(path_error(format!(
            "hdpki: path must contain ssh scheme: {input:?}"
        )));
    }

    let principal = parts[2].to_ascii_lowercase();
    if !PRINCIPAL_RE.is_match(&principal) {
        return Err(path_error(format!(
            "hdpki: invalid principal {0:?}; use lowercase letters, numbers, and :_-.",
            parts[2]
        )));
    }

    let domain = parts[3].to_ascii_lowercase();
    validate_domain(&domain)?;

    if parts.len() < 5 {
        return Err(path_error(format!(
            "hdpki: missing host segment in {input:?}"
        )));
    }

    let mut host = parts[4].to_ascii_lowercase();
    if host.is_empty() {
        return Err(path_error("hdpki: empty host segment"));
    }
    if host.contains('.') {
        if !host.ends_with(&domain) {
            return Err(path_error(format!(
                "hdpki: host {host:?} must be within domain {domain:?}"
            )));
        }
    } else {
        host = format!("{host}.{domain}");
    }
    validate_domain(&host)
        .map_err(|err| path_error(format!("hdpki: invalid host fqdn {host:?}: {err}")))?;

    let bucket = if parts.len() > 5 {
        if parts.len() > 6 {
            return Err(path_error(format!(
                "hdpki: unexpected additional path segments in {input:?}"
            )));
        }
        let raw_bucket = parts[5];
        let cleaned = raw_bucket.strip_prefix('+').unwrap_or(raw_bucket);
        if cleaned.is_empty() {
            return Err(path_error(format!(
                "hdpki: invalid bucket segment {raw_bucket:?}"
            )));
        }
        cleaned
            .parse::<u64>()
            .map_err(|err| path_error(format!("hdpki: invalid bucket {raw_bucket:?}: {err}")))?;
        Some(cleaned.to_string())
    } else {
        None
    };

    let mut segments = vec![
        "m".to_string(),
        "ssh".to_string(),
        principal,
        domain.clone(),
        host.clone(),
    ];
    if let Some(bucket_value) = bucket.clone() {
        segments.push(bucket_value);
    }

    Ok(Path {
        canonical: segments.join("/"),
        segments,
        bucket,
    })
}

impl Path {
    pub(crate) fn root() -> Self {
        Self {
            canonical: "m".to_string(),
            segments: vec!["m".to_string()],
            bucket: None,
        }
    }

    pub(crate) fn from_segments(segments: Vec<String>) -> Self {
        let bucket = if segments.len() > 5 {
            Some(segments[5].clone())
        } else {
            None
        };
        Self {
            canonical: segments.join("/"),
            segments,
            bucket,
        }
    }

    pub fn canonical(&self) -> &str {
        &self.canonical
    }

    pub fn segments(&self) -> Vec<String> {
        self.segments.clone()
    }

    pub fn prefix_segments(&self, n: usize) -> Option<Vec<String>> {
        if n < 1 || n > self.segments.len() {
            return None;
        }
        Some(self.segments[..n].to_vec())
    }

    pub fn bucket(&self) -> Option<&str> {
        self.bucket.as_deref()
    }
}

fn validate_domain(domain: &str) -> Result<(), PathError> {
    let labels: Vec<&str> = domain.split('.').collect();
    if labels.len() < 2 {
        return Err(path_error(format!(
            "hdpki: {domain:?} must be a fully qualified domain"
        )));
    }
    for label in labels {
        if !LABEL_RE.is_match(label) {
            return Err(path_error(format!("hdpki: invalid dns label {label:?}")));
        }
    }
    Ok(())
}

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

    #[test]
    fn parse_path_canonicalizes() {
        let path = parse_path("m/ssh/User:Admin/Example.COM/Edge1/20240101").unwrap();
        assert_eq!(
            path.canonical(),
            "m/ssh/user:admin/example.com/edge1.example.com/20240101"
        );
        assert_eq!(path.bucket(), Some("20240101"));
    }

    #[test]
    fn parse_path_validates_host() {
        assert!(parse_path("m/ssh/user/example.com/api.other.com").is_err());
    }

    #[test]
    fn parse_path_requires_segments() {
        assert!(parse_path("m/ssh/user").is_err());
    }
}
