//! Filesystem source specifiers — fstab(5) field 1.
//!
//! Represents the full range of source identifiers supported by fstab(5):
//! - Device paths (`/dev/sda1`, `/dev/disk/by-uuid/...`)
//! - Tag identifiers (`LABEL=`, `UUID=`, `PARTLABEL=`, `PARTUUID=`, `ID=`)
//! - NFS-style network mounts (`host:/path`, `[::1]:/path`)
//! - Keyword pseudo-filesystems (`proc`, `tmpfs`, `none`)

use crate::error::SpecError;
use crate::escape::decode_escapes;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;

/// Try to convert a string into a `Spec`.
///
/// Equivalent to [`Spec::parse`].
impl TryFrom<&str> for Spec {
    type Error = SpecError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        Spec::parse(s)
    }
}

/// Filesystem source identifier — fstab(5) field 1.
///
/// Corresponds to libmount: `fs->source` + `fs->tagname`/`fs->tagval`.
///
/// # Examples
///
/// ```
/// # use mount_fstab::spec::Spec;
/// let spec = Spec::parse("UUID=abc-123").unwrap();
/// assert!(matches!(spec, Spec::Uuid(_)));
/// assert!(spec.is_tag());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Spec {
    /// Block device path: `/dev/sda1`, `/dev/disk/by-uuid/...`
    Device(PathBuf),
    /// Filesystem label: `LABEL=Boot`
    Label(String),
    /// Filesystem UUID: `UUID=3e6be9de-...` (preserves original case)
    Uuid(String),
    /// GPT partition label: `PARTLABEL=System`
    PartLabel(String),
    /// GPT partition UUID: `PARTUUID=...`
    PartUuid(String),
    /// Hardware block device ID (deprecated, per fstab(5) and mount(8)).
    ///
    /// The `ID=` tag is not strictly defined and depends on udev rules or
    /// hardware topology. Prefer `UUID=`, `PARTUUID=`, or `/dev/disk/by-*`
    /// paths instead.
    #[deprecated = "ID= tag is not strictly defined and depends on udev rules/hardware"]
    Id(String),
    /// NFS-style network mount: `host:/path`
    NetworkMount {
        /// Remote host (hostname, IP address, or bracketed IPv6 address).
        host: String,
        /// Exported path on the remote host.
        path: PathBuf,
    },
    /// Pseudo-filesystem keyword: `proc`, `tmpfs`, `sysfs`, `none`, etc.
    Keyword(String),
}

impl Spec {
    /// Parse a decoded spec string into a typed `Spec`.
    ///
    /// This function accepts already-decoded strings. If you need to parse
    /// escape-encoded strings from an fstab file, use `parse_raw` instead
    /// (crate-internal helper that calls [`decode_escapes`]
    /// before parsing).
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::Spec;
    /// // Device path
    /// let spec = Spec::parse("/dev/sda1").unwrap();
    /// assert!(matches!(spec, Spec::Device(_)));
    ///
    /// // Tag-based identifier (case-insensitive tag prefix)
    /// let spec = Spec::parse("uuid=abc-123").unwrap();
    /// assert_eq!(spec, Spec::Uuid("abc-123".into()));
    ///
    /// // Label
    /// let spec = Spec::parse("LABEL=Boot").unwrap();
    /// assert_eq!(spec, Spec::Label("Boot".into()));
    ///
    /// // NFS network mount
    /// let spec = Spec::parse("server:/export").unwrap();
    /// assert!(matches!(spec, Spec::NetworkMount { .. }));
    ///
    /// // Pseudo-filesystem
    /// let spec = Spec::parse("proc").unwrap();
    /// assert!(spec.is_pseudo());
    /// ```
    ///
    /// # Errors
    ///
    /// Returns [`SpecError::Empty`] if the input is empty.
    pub fn parse(raw: &str) -> Result<Self, SpecError> {
        if raw.is_empty() {
            return Err(SpecError::Empty);
        }

        // Case-insensitive tag prefix matching per mount(8).
        let tag_prefixes = ["LABEL=", "UUID=", "PARTLABEL=", "PARTUUID=", "ID="];
        for &prefix in &tag_prefixes {
            if raw
                .get(..prefix.len())
                .is_some_and(|s| s.eq_ignore_ascii_case(prefix))
            {
                let val = &raw[prefix.len()..];
                return match prefix {
                    "LABEL=" => Ok(Spec::Label(val.to_owned())),
                    "UUID=" => Ok(Spec::Uuid(val.to_owned())),
                    "PARTLABEL=" => Ok(Spec::PartLabel(val.to_owned())),
                    "PARTUUID=" => Ok(Spec::PartUuid(val.to_owned())),
                    "ID=" => {
                        #[allow(deprecated)]
                        let result = Ok(Spec::Id(val.to_owned()));
                        #[allow(deprecated)]
                        result
                    }
                    _ => unreachable!(),
                };
            }
        }

        if raw.starts_with('/') {
            return Ok(Spec::Device(PathBuf::from(raw)));
        }

        // Handle IPv6 NFS mounts: `[::1]:/path`
        if raw.starts_with('[') {
            if let Some(bracket_end) = raw.find("]:") {
                let host = &raw[..bracket_end + 1];
                let path = &raw[bracket_end + 2..];
                if !host.is_empty() {
                    return Ok(Spec::NetworkMount {
                        host: host.to_owned(),
                        path: PathBuf::from(path),
                    });
                }
            }
        }

        if let Some(colon) = raw.find(':') {
            let host = &raw[..colon];
            let path = &raw[colon + 1..];
            if !host.is_empty() {
                return Ok(Spec::NetworkMount {
                    host: host.to_owned(),
                    path: PathBuf::from(path),
                });
            }
        }

        Ok(Spec::Keyword(raw.to_owned()))
    }

    /// Parse a raw (escape-encoded) spec string from an fstab line.
    ///
    /// Decodes escape sequences, then calls [`parse`](Self::parse) for tag recognition.
    pub(crate) fn parse_raw(raw: &str) -> Result<Self, SpecError> {
        let decoded = decode_escapes(raw);
        Self::parse(&decoded)
    }

    /// Returns `true` if this is a tag-based identifier
    /// (`LABEL`/`UUID`/`PARTLABEL`/`PARTUUID`/`ID`).
    #[must_use]
    #[allow(deprecated)]
    pub fn is_tag(&self) -> bool {
        matches!(
            self,
            Spec::Label(_) | Spec::Uuid(_) | Spec::PartLabel(_) | Spec::PartUuid(_) | Spec::Id(_)
        )
    }

    /// Returns `true` if this is a pseudo-filesystem (no backing block device).
    #[must_use]
    pub fn is_pseudo(&self) -> bool {
        matches!(self, Spec::Keyword(_))
    }

    /// Returns the tag prefix name for tag-based identifiers.
    ///
    /// Returns `Some("LABEL")`, `Some("UUID")`, `Some("PARTLABEL")`,
    /// `Some("PARTUUID")`, or `Some("ID")` for tag variants, or `None`
    /// for non-tag variants.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::Spec;
    /// let spec = Spec::parse("UUID=abc-123").unwrap();
    /// assert_eq!(spec.tag_name(), Some("UUID"));
    /// assert_eq!(Spec::parse("/dev/sda1").unwrap().tag_name(), None);
    /// ```
    #[must_use]
    #[allow(deprecated)]
    pub fn tag_name(&self) -> Option<&'static str> {
        match self {
            Spec::Label(_) => Some("LABEL"),
            Spec::Uuid(_) => Some("UUID"),
            Spec::PartLabel(_) => Some("PARTLABEL"),
            Spec::PartUuid(_) => Some("PARTUUID"),
            Spec::Id(_) => Some("ID"),
            _ => None,
        }
    }

    /// Returns the tag value for tag-based identifiers.
    ///
    /// Returns `Some(value)` for tag variants (e.g., `"abc-123"` for
    /// `UUID=abc-123`), or `None` for non-tag variants.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::Spec;
    /// let spec = Spec::parse("LABEL=Boot").unwrap();
    /// assert_eq!(spec.tag_value(), Some("Boot"));
    /// assert_eq!(Spec::parse("proc").unwrap().tag_value(), None);
    /// ```
    #[must_use]
    #[allow(deprecated)]
    pub fn tag_value(&self) -> Option<&str> {
        match self {
            Spec::Label(v) | Spec::Uuid(v) | Spec::PartLabel(v) | Spec::PartUuid(v) => Some(v),
            Spec::Id(v) => Some(v),
            _ => None,
        }
    }
}

impl fmt::Display for Spec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Spec::Device(p) => write!(f, "{}", p.display()),
            Spec::Label(v) => write!(f, "LABEL={v}"),
            Spec::Uuid(v) => write!(f, "UUID={v}"),
            Spec::PartLabel(v) => write!(f, "PARTLABEL={v}"),
            Spec::PartUuid(v) => write!(f, "PARTUUID={v}"),
            #[allow(deprecated)]
            Spec::Id(v) => write!(f, "ID={v}"),
            Spec::NetworkMount { host, path } => write!(f, "{host}:{}", path.display()),
            Spec::Keyword(v) => write!(f, "{v}"),
        }
    }
}

impl FromStr for Spec {
    type Err = SpecError;

    /// Parse a string into a `Spec`.
    ///
    /// This is equivalent to [`Spec::parse`].
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Spec::parse(s)
    }
}

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

    #[test]
    fn parse_device_path() {
        let spec = Spec::parse("/dev/sda1").unwrap();
        assert_eq!(spec, Spec::Device(PathBuf::from("/dev/sda1")));
    }

    #[test]
    fn parse_device_by_uuid_path() {
        let spec = Spec::parse("/dev/disk/by-uuid/abc-123").unwrap();
        assert_eq!(
            spec,
            Spec::Device(PathBuf::from("/dev/disk/by-uuid/abc-123"))
        );
    }

    #[test]
    fn parse_label() {
        let spec = Spec::parse("LABEL=Boot").unwrap();
        assert_eq!(spec, Spec::Label("Boot".to_owned()));
    }

    #[test]
    fn parse_label_with_escape() {
        // parse() accepts already-decoded strings; the value is not decoded again.
        let spec = Spec::parse(r"LABEL=My\040Drive").unwrap();
        assert_eq!(spec, Spec::Label(r"My\040Drive".to_owned()));
    }

    #[test]
    fn parse_label_with_space() {
        // When the value is already decoded (e.g., a space), it passes through.
        let spec = Spec::parse("LABEL=My Drive").unwrap();
        assert_eq!(spec, Spec::Label("My Drive".to_owned()));
    }

    #[test]
    fn parse_label_case_insensitive() {
        let spec = Spec::parse("label=Boot").unwrap();
        assert_eq!(spec, Spec::Label("Boot".to_owned()));
    }

    #[test]
    fn parse_uuid_case_insensitive() {
        let spec = Spec::parse("uuid=abc-123").unwrap();
        assert_eq!(spec, Spec::Uuid("abc-123".to_owned()));
    }

    #[test]
    fn parse_partlabel_case_insensitive() {
        let spec = Spec::parse("partlabel=System").unwrap();
        assert_eq!(spec, Spec::PartLabel("System".to_owned()));
    }

    #[test]
    fn parse_partuuid_case_insensitive() {
        let spec = Spec::parse("partuuid=abc-123").unwrap();
        assert_eq!(spec, Spec::PartUuid("abc-123".to_owned()));
    }

    #[test]
    #[allow(deprecated)]
    fn parse_id_case_insensitive() {
        let spec = Spec::parse("id=ata-SAMSUNG_SSD_1234").unwrap();
        assert_eq!(spec, Spec::Id("ata-SAMSUNG_SSD_1234".to_owned()));
    }

    #[test]
    fn parse_uuid() {
        let spec = Spec::parse("UUID=3e6be9de-8139-11d1-9106-a43f08d823a6").unwrap();
        assert_eq!(
            spec,
            Spec::Uuid("3e6be9de-8139-11d1-9106-a43f08d823a6".to_owned())
        );
    }

    #[test]
    fn parse_uuid_fat_uppercase_preserved() {
        let spec = Spec::parse("UUID=A40D-85E7").unwrap();
        assert_eq!(spec, Spec::Uuid("A40D-85E7".to_owned()));
    }

    #[test]
    fn parse_partlabel() {
        let spec = Spec::parse("PARTLABEL=System").unwrap();
        assert_eq!(spec, Spec::PartLabel("System".to_owned()));
    }

    #[test]
    fn parse_partuuid() {
        let spec = Spec::parse("PARTUUID=d091fd20-162a-43e1-ad50-0e3ce36ab051").unwrap();
        assert_eq!(
            spec,
            Spec::PartUuid("d091fd20-162a-43e1-ad50-0e3ce36ab051".to_owned())
        );
    }

    #[test]
    #[allow(deprecated)]
    fn parse_id() {
        let spec = Spec::parse("ID=ata-SAMSUNG_SSD_1234").unwrap();
        assert_eq!(spec, Spec::Id("ata-SAMSUNG_SSD_1234".to_owned()));
    }

    #[test]
    fn parse_nfs_mount() {
        let spec = Spec::parse("server.example.com:/exports/data").unwrap();
        assert_eq!(
            spec,
            Spec::NetworkMount {
                host: "server.example.com".to_owned(),
                path: PathBuf::from("/exports/data"),
            }
        );
    }

    #[test]
    fn parse_nfs_mount_ip() {
        let spec = Spec::parse("192.168.1.1:/nfs/share").unwrap();
        assert_eq!(
            spec,
            Spec::NetworkMount {
                host: "192.168.1.1".to_owned(),
                path: PathBuf::from("/nfs/share"),
            }
        );
    }

    #[test]
    fn parse_keyword_proc() {
        let spec = Spec::parse("proc").unwrap();
        assert_eq!(spec, Spec::Keyword("proc".to_owned()));
    }

    #[test]
    fn parse_keyword_none() {
        let spec = Spec::parse("none").unwrap();
        assert_eq!(spec, Spec::Keyword("none".to_owned()));
    }

    #[test]
    fn parse_keyword_tmpfs() {
        let spec = Spec::parse("tmpfs").unwrap();
        assert_eq!(spec, Spec::Keyword("tmpfs".to_owned()));
    }

    #[test]
    fn parse_empty_is_error() {
        assert!(Spec::parse("").is_err());
    }

    #[test]
    #[allow(deprecated)]
    fn spec_is_tag() {
        assert!(Spec::Label("test".into()).is_tag());
        assert!(Spec::Uuid("test".into()).is_tag());
        assert!(Spec::PartLabel("test".into()).is_tag());
        assert!(Spec::PartUuid("test".into()).is_tag());
        assert!(Spec::Id("test".into()).is_tag());
        assert!(!Spec::Device(PathBuf::from("/dev/sda")).is_tag());
        assert!(!Spec::Keyword("proc".into()).is_tag());
    }

    #[test]
    fn parse_nfs_with_ipv6_brackets() {
        let spec = Spec::parse("[::1]:/nfs/share").unwrap();
        assert_eq!(
            spec,
            Spec::NetworkMount {
                host: "[::1]".into(),
                path: PathBuf::from("/nfs/share"),
            }
        );
    }

    #[test]
    fn parse_label_with_escape_roundtrip() {
        let spec = Spec::parse_raw(r"LABEL=My\040Drive").unwrap();
        assert_eq!(spec, Spec::Label("My Drive".to_owned()));
    }

    #[test]
    fn from_str_works() {
        let spec: Spec = "UUID=abc".parse().unwrap();
        assert_eq!(spec, Spec::Uuid("abc".to_owned()));
    }

    #[test]
    fn parse_emoji_does_not_panic() {
        // Ensure byte-index slicing does not panic on multi-byte UTF-8.
        let spec = Spec::parse("🐈").unwrap();
        assert!(matches!(spec, Spec::Keyword(_)));
    }
}
