//! ConceptPath and ConceptAlias types for hierarchical subject identifiers.
//!
//! ConceptPath enables hierarchical subjects with scheme-based source tier inference:
//! - `rfc://7519/jwt/audience_validation` → Regulatory (Tier 0)
//! - `code://rust/citadeldb/auth/jwt/aud` → Expert (Tier 3)
//! - `blog://example/post/123` → Anecdotal (Tier 5)
//!
//! ConceptAlias enables cross-scheme entity resolution, allowing code paths to be
//! aliased to authoritative sources for unified querying.

use super::SourceClass;
use rkyv::{Archive, Deserialize, Serialize};
use std::fmt;

/// Error type for ConceptPath parsing failures.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConceptPathError {
    /// The path string is empty.
    Empty,
    /// The scheme is missing or empty.
    MissingScheme,
    /// No segments after the scheme.
    NoSegments,
    /// A segment is empty (e.g., `code://rust//auth`).
    EmptySegment,
    /// The scheme contains invalid characters.
    InvalidScheme(String),
}

impl fmt::Display for ConceptPathError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConceptPathError::Empty => write!(f, "path is empty"),
            ConceptPathError::MissingScheme => write!(f, "missing scheme (expected scheme://...)"),
            ConceptPathError::NoSegments => write!(f, "no segments after scheme"),
            ConceptPathError::EmptySegment => write!(f, "empty segment in path"),
            ConceptPathError::InvalidScheme(s) => write!(f, "invalid scheme: {}", s),
        }
    }
}

impl std::error::Error for ConceptPathError {}

/// A hierarchical subject identifier with scheme.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
#[archive(check_bytes)]
pub struct ConceptPath {
    /// The scheme identifier (e.g., "rfc", "code", "fda").
    pub scheme: String,
    /// The path segments after the scheme.
    pub segments: Vec<String>,
}

impl ConceptPath {
    /// Create a new ConceptPath from scheme and segments.
    pub fn new(scheme: impl Into<String>, segments: Vec<String>) -> Self {
        Self { scheme: scheme.into(), segments }
    }

    /// Parse a ConceptPath from a wire format string.
    pub fn parse(s: &str) -> Result<Self, ConceptPathError> {
        if s.is_empty() {
            return Err(ConceptPathError::Empty);
        }

        if let Some(idx) = s.find("://") {
            let scheme = &s[..idx];
            let rest = &s[idx + 3..];

            if scheme.is_empty() {
                return Err(ConceptPathError::MissingScheme);
            }

            if !scheme.chars().all(|c| c.is_alphanumeric() || c == '_') {
                return Err(ConceptPathError::InvalidScheme(scheme.to_string()));
            }

            if rest.is_empty() {
                return Err(ConceptPathError::NoSegments);
            }

            let segments: Vec<String> = rest.split('/').map(|s| s.to_string()).collect();

            for seg in &segments {
                if seg.is_empty() {
                    return Err(ConceptPathError::EmptySegment);
                }
            }

            Ok(Self { scheme: scheme.to_string(), segments })
        } else {
            Ok(Self { scheme: "custom".to_string(), segments: vec![s.to_string()] })
        }
    }

    /// Convert to wire format string.
    pub fn to_wire_format(&self) -> String {
        format!("{}://{}", self.scheme, self.segments.join("/"))
    }

    /// Get the leaf segment (last segment).
    pub fn leaf(&self) -> &str {
        self.segments.last().map(|s| s.as_str()).unwrap_or("")
    }

    /// Get the parent path (all but the last segment).
    pub fn parent(&self) -> Option<ConceptPath> {
        if self.segments.len() <= 1 {
            None
        } else {
            Some(ConceptPath {
                scheme: self.scheme.clone(),
                segments: self.segments[..self.segments.len() - 1].to_vec(),
            })
        }
    }

    /// Check if this path is a prefix of another path.
    pub fn is_prefix_of(&self, other: &ConceptPath) -> bool {
        if self.scheme != other.scheme {
            return false;
        }
        if self.segments.len() > other.segments.len() {
            return false;
        }
        self.segments.iter().zip(other.segments.iter()).all(|(a, b)| a == b)
    }

    /// Get the default source class based on scheme.
    pub fn default_source_class(&self) -> SourceClass {
        SourceScheme::parse(&self.scheme).default_source_class()
    }
}

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

/// Source scheme enum for tier mapping.
///
/// Maps URL-like schemes to default SourceClass tiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceScheme {
    /// RFC standards (Tier 0 - Regulatory)
    Rfc,
    /// NIST standards (Tier 0 - Regulatory)
    Nist,
    /// FDA regulations (Tier 0 - Regulatory)
    Fda,
    /// SEC filings (Tier 0 - Regulatory)
    Sec,
    /// OWASP security standards (Tier 1 - Clinical)
    Owasp,
    /// PubMed articles (Tier 1 - Clinical)
    Pubmed,
    /// DOI-identified academic papers (Tier 1 - Clinical)
    Doi,
    /// Vendor documentation (Tier 2 - Observational)
    Vendor,
    /// CVE vulnerability database (Tier 2 - Observational)
    Cve,
    /// Internal documentation (Tier 3 - Expert)
    Internal,
    /// Code references (Tier 3 - Expert)
    Code,
    /// Custom/unrecognized scheme (Tier 3 - Expert)
    Custom,
    /// Community wikis (Tier 4 - Community)
    Community,
    /// Wiki pages (Tier 4 - Community)
    Wiki,
    /// Blog posts (Tier 5 - Anecdotal)
    Blog,
    /// Social media (Tier 5 - Anecdotal)
    Social,
}

impl SourceScheme {
    /// Parse a scheme string to SourceScheme.
    pub fn parse(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "rfc" => SourceScheme::Rfc,
            "nist" => SourceScheme::Nist,
            "fda" => SourceScheme::Fda,
            "sec" => SourceScheme::Sec,
            "owasp" => SourceScheme::Owasp,
            "pubmed" => SourceScheme::Pubmed,
            "doi" => SourceScheme::Doi,
            "vendor" => SourceScheme::Vendor,
            "cve" => SourceScheme::Cve,
            "internal" => SourceScheme::Internal,
            "code" => SourceScheme::Code,
            "community" => SourceScheme::Community,
            "wiki" => SourceScheme::Wiki,
            "blog" => SourceScheme::Blog,
            "social" => SourceScheme::Social,
            _ => SourceScheme::Custom,
        }
    }

    /// Get the default SourceClass for this scheme.
    pub fn default_source_class(&self) -> SourceClass {
        match self {
            SourceScheme::Rfc | SourceScheme::Nist | SourceScheme::Fda | SourceScheme::Sec => {
                SourceClass::Regulatory
            }
            SourceScheme::Owasp | SourceScheme::Pubmed | SourceScheme::Doi => SourceClass::Clinical,
            SourceScheme::Vendor | SourceScheme::Cve => SourceClass::Observational,
            SourceScheme::Internal | SourceScheme::Code | SourceScheme::Custom => {
                SourceClass::Expert
            }
            SourceScheme::Community | SourceScheme::Wiki => SourceClass::Community,
            SourceScheme::Blog | SourceScheme::Social => SourceClass::Anecdotal,
        }
    }
}

/// Origin of a concept alias.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[archive(check_bytes)]
pub enum AliasOrigin {
    /// Manually created by a user or admin.
    Manual,
    /// Suggested by the system based on heuristics.
    Suggested,
    /// Created during an entity merge operation.
    Merged,
    /// Auto-detected by conflict detection (e.g., Aphoria tail-path matching).
    AutoDetected,
}

impl fmt::Display for AliasOrigin {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AliasOrigin::Manual => write!(f, "manual"),
            AliasOrigin::Suggested => write!(f, "suggested"),
            AliasOrigin::Merged => write!(f, "merged"),
            AliasOrigin::AutoDetected => write!(f, "auto_detected"),
        }
    }
}

/// A cross-scheme entity alias.
///
/// ConceptAlias links related concepts across different schemes, enabling
/// unified querying. For example, linking a code path to its authoritative RFC.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, PartialEq)]
#[archive(check_bytes)]
pub struct ConceptAlias {
    /// The alias path (points to canonical).
    pub alias: ConceptPath,
    /// The canonical path (authoritative).
    pub canonical: ConceptPath,
    /// The agent ID that created this alias.
    pub created_by: [u8; 32],
    /// Unix timestamp when the alias was created.
    pub created_at: u64,
    /// How this alias was created.
    pub origin: AliasOrigin,
}

impl ConceptAlias {
    /// Create a new ConceptAlias.
    pub fn new(
        alias: ConceptPath,
        canonical: ConceptPath,
        created_by: [u8; 32],
        created_at: u64,
        origin: AliasOrigin,
    ) -> Self {
        Self { alias, canonical, created_by, created_at, origin }
    }
}

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

    #[test]
    fn test_concept_path_parse_full() {
        let path = ConceptPath::parse("rfc://7519/jwt/audience_validation").unwrap();
        assert_eq!(path.scheme, "rfc");
        assert_eq!(path.segments, vec!["7519", "jwt", "audience_validation"]);
    }

    #[test]
    fn test_concept_path_backward_compat() {
        let path = ConceptPath::parse("Tesla_Inc").unwrap();
        assert_eq!(path.scheme, "custom");
        assert_eq!(path.segments, vec!["Tesla_Inc"]);
    }

    #[test]
    fn test_concept_path_roundtrip() {
        let original = "rfc://7519/jwt/aud";
        let path = ConceptPath::parse(original).unwrap();
        assert_eq!(path.to_wire_format(), original);
    }

    #[test]
    fn test_concept_path_scheme_inference_rfc() {
        let path = ConceptPath::parse("rfc://7519/jwt").unwrap();
        assert_eq!(path.default_source_class(), SourceClass::Regulatory);
    }

    #[test]
    fn test_concept_path_scheme_inference_code() {
        let path = ConceptPath::parse("code://rust/auth").unwrap();
        assert_eq!(path.default_source_class(), SourceClass::Expert);
    }

    #[test]
    fn test_concept_alias_new() {
        let alias = ConceptAlias::new(
            ConceptPath::parse("code://rust/auth").unwrap(),
            ConceptPath::parse("rfc://7519").unwrap(),
            [1u8; 32],
            1000,
            AliasOrigin::Manual,
        );
        assert_eq!(alias.alias.scheme, "code");
        assert_eq!(alias.canonical.scheme, "rfc");
    }
}
