//! Semantic validation of fstab entries.
//!
//! Provides checks matching `findmnt --verify` behavior:
//! - Root filesystem must have `passno=1`
//! - No duplicate mount points
//! - Swap entries must use `none` as mount point
//! - Deprecated types and tags emit warnings

use crate::options::MountPermission;
use crate::spec::Spec;
use crate::types::Fstab;
use std::collections::HashSet;

/// Severity level of a validation diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Severity {
    /// A non-critical issue that may not prevent mounting.
    Warning,
    /// A critical issue that should be fixed.
    Error,
}

/// A validation diagnostic, corresponding to `findmnt --verify` output.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Diagnostic {
    /// The severity of the diagnostic (Warning or Error).
    pub severity: Severity,
    /// A human-readable description of the issue.
    pub message: String,
}

impl Fstab {
    /// Validate the fstab and return all diagnostics.
    ///
    /// Performs the following semantic checks:
    /// - Root filesystem must have `passno=1`
    /// - No duplicate mount points (warning)
    /// - Swap entries must have mount point `none`
    /// - Deprecated filesystem types (`ignore`) emit warnings
    /// - Deprecated `ID=` tags emit warnings
    /// - `user`/`users`/`owner` with explicit `exec` override emits warning
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::{Fstab, Entry, Spec, MountPoint, FsType, Options};
    /// let fstab = Fstab {
    ///     entries: vec![
    ///         Entry::new(
    ///             Spec::parse("UUID=root").unwrap(),
    ///             MountPoint::new("/").unwrap(),
    ///             FsType::parse("ext4").unwrap(),
    ///             Options::parse("defaults").unwrap(),
    ///         ),
    ///     ],
    ///     ..Default::default()
    /// };
    /// let diags = fstab.validate();
    /// // Root without passno=1 produces an Error
    /// assert!(diags.iter().any(|d| d.severity == mount_fstab::Severity::Error));
    /// ```
    #[must_use]
    pub fn validate(&self) -> Vec<Diagnostic> {
        let mut diags = Vec::new();
        self.check_root_passno(&mut diags);
        self.check_duplicate_mountpoints(&mut diags);
        self.check_swap_entries(&mut diags);
        self.check_deprecated_types(&mut diags);
        self.check_deprecated_tags(&mut diags);
        self.check_user_permission_conflicts(&mut diags);
        diags
    }

    /// Check that root filesystem has `passno=1`.
    fn check_root_passno(&self, diags: &mut Vec<Diagnostic>) {
        let root_entry = self.root();
        match root_entry {
            None => {
                if let Some(entry) = self.entries.iter().find(|e| e.file.is_root()) {
                    if entry.passno != 1 {
                        diags.push(Diagnostic {
                            severity: Severity::Error,
                            message: format!(
                                "root filesystem ({}) should have passno=1 (currently {})",
                                entry.spec, entry.passno,
                            ),
                        });
                    }
                }
            }
            Some(_) => {
                let roots: Vec<_> = self
                    .entries
                    .iter()
                    .filter(|e| e.passno == 1 && e.file.is_root())
                    .collect();
                if roots.len() > 1 {
                    diags.push(Diagnostic {
                        severity: Severity::Error,
                        message: "multiple root filesystem entries with passno=1".to_owned(),
                    });
                }
            }
        }
    }

    /// Check for duplicate mount points.
    fn check_duplicate_mountpoints(&self, diags: &mut Vec<Diagnostic>) {
        let mut seen = HashSet::new();
        for entry in &self.entries {
            let key = entry.file.as_path().to_string_lossy().to_string();
            if !seen.insert(key.clone()) {
                diags.push(Diagnostic {
                    severity: Severity::Warning,
                    message: format!("duplicate mount point: {key}"),
                });
            }
        }
    }

    /// Check that swap entries use `none` as mount point.
    fn check_swap_entries(&self, diags: &mut Vec<Diagnostic>) {
        for entry in &self.entries {
            if entry.is_swap() && !entry.file.is_swap() {
                diags.push(Diagnostic {
                    severity: Severity::Error,
                    message: format!(
                        "swap entry '{}' must have mount point 'none', got '{}'",
                        entry.spec,
                        entry.file.as_path().display(),
                    ),
                });
            }
        }
    }

    /// Check for deprecated filesystem types.
    fn check_deprecated_types(&self, diags: &mut Vec<Diagnostic>) {
        for entry in &self.entries {
            if entry.vfstype.as_str() == "ignore" {
                diags.push(Diagnostic {
                    severity: Severity::Warning,
                    message: format!(
                        "'ignore' filesystem type is no longer supported since util-linux v2.22 ({})",
                        entry.spec,
                    ),
                });
            }
        }
    }

    /// Check for deprecated `ID=` tags.
    fn check_deprecated_tags(&self, diags: &mut Vec<Diagnostic>) {
        for entry in &self.entries {
            #[allow(deprecated)]
            if matches!(entry.spec, Spec::Id(_)) {
                diags.push(Diagnostic {
                    severity: Severity::Warning,
                    message: "ID= tag is not recommended for generic use".to_owned(),
                });
            }
        }
    }

    /// Check for `user`/`users`/`owner` with explicit `exec` override.
    fn check_user_permission_conflicts(&self, diags: &mut Vec<Diagnostic>) {
        for entry in &self.entries {
            let perm = entry.options.mount_permission();
            match perm {
                MountPermission::User | MountPermission::Users | MountPermission::Owner
                    if entry.options.has("exec") =>
                {
                    let name = match perm {
                        MountPermission::User => "user",
                        MountPermission::Users => "users",
                        MountPermission::Owner => "owner",
                        // outer match guard already filtered Group and None
                        _ => continue,
                    };
                    diags.push(Diagnostic {
                        severity: Severity::Warning,
                        message: format!("'{name}' option with explicit 'exec' override"),
                    });
                }
                _ => {}
            }
        }
    }
}
