use std::fmt::{self, Write as _};

const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
const REVIEW_EXCEPTION_NOTE: &str =
    "reviewed dry-run exception; verify owner, source scope, and expiry before live apply";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
    Ufw,
    Iptables,
    Nftables,
}

impl Backend {
    pub fn all() -> [Self; 3] {
        [Self::Ufw, Self::Iptables, Self::Nftables]
    }

    pub fn next(self) -> Self {
        match self {
            Self::Ufw => Self::Iptables,
            Self::Iptables => Self::Nftables,
            Self::Nftables => Self::Ufw,
        }
    }
}

impl fmt::Display for Backend {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Ufw => f.write_str("ufw"),
            Self::Iptables => f.write_str("iptables"),
            Self::Nftables => f.write_str("nftables"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionMode {
    DryRun,
}

impl fmt::Display for ExecutionMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DryRun => f.write_str("dry-run only"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleAction {
    Allow,
    Deny,
    Reject,
}

impl RuleAction {
    pub fn label(self) -> &'static str {
        match self {
            Self::Allow => "allow",
            Self::Deny => "deny",
            Self::Reject => "reject",
        }
    }

    fn ufw_arg(self) -> &'static str {
        self.label()
    }

    fn netfilter_target(self) -> &'static str {
        match self {
            Self::Allow => "ACCEPT",
            Self::Deny => "DROP",
            Self::Reject => "REJECT",
        }
    }

    fn nft_verdict(self) -> &'static str {
        match self {
            Self::Allow => "accept",
            Self::Deny => "drop",
            Self::Reject => "reject",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
    In,
    Out,
}

impl Direction {
    pub fn label(self) -> &'static str {
        match self {
            Self::In => "in",
            Self::Out => "out",
        }
    }

    fn iptables_chain(self) -> &'static str {
        match self {
            Self::In => "INPUT",
            Self::Out => "OUTPUT",
        }
    }

    fn nft_chain(self) -> &'static str {
        match self {
            Self::In => "input",
            Self::Out => "output",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
    Any,
    Tcp,
    Udp,
}

impl Protocol {
    pub fn label(self) -> &'static str {
        match self {
            Self::Any => "any",
            Self::Tcp => "tcp",
            Self::Udp => "udp",
        }
    }
}

impl fmt::Display for Protocol {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.label())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefaultPolicy {
    Allow,
    Deny,
}

impl DefaultPolicy {
    pub fn next(self) -> Self {
        match self {
            Self::Allow => Self::Deny,
            Self::Deny => Self::Allow,
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::Allow => "allow",
            Self::Deny => "deny",
        }
    }

    fn iptables_target(self) -> &'static str {
        match self {
            Self::Allow => "ACCEPT",
            Self::Deny => "DROP",
        }
    }

    fn nft_policy(self) -> &'static str {
        match self {
            Self::Allow => "accept",
            Self::Deny => "drop",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortSpec {
    Any,
    Single(u16),
    Range(u16, u16),
}

impl PortSpec {
    pub fn ufw_value(self, protocol: Protocol) -> Option<String> {
        let port = match self {
            Self::Any => return None,
            Self::Single(port) => port.to_string(),
            Self::Range(start, end) => format!("{start}:{end}"),
        };

        match protocol {
            Protocol::Any => Some(port),
            Protocol::Tcp | Protocol::Udp => Some(format!("{port}/{protocol}")),
        }
    }

    fn netfilter_value(self) -> Option<String> {
        match self {
            Self::Any => None,
            Self::Single(port) => Some(port.to_string()),
            Self::Range(start, end) => Some(format!("{start}:{end}")),
        }
    }
}

impl fmt::Display for PortSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Any => f.write_str("any"),
            Self::Single(port) => write!(f, "{port}"),
            Self::Range(start, end) => write!(f, "{start}-{end}"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FirewallRule {
    pub id: u32,
    pub enabled: bool,
    pub action: RuleAction,
    pub direction: Direction,
    pub protocol: Protocol,
    pub port: PortSpec,
    pub note: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortCandidate {
    pub name: &'static str,
    pub group: &'static str,
    pub protocol: Protocol,
    pub port: PortSpec,
    pub risk: &'static str,
    pub aliases: &'static [&'static str],
    pub detail: &'static str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortPriority {
    pub index: usize,
    pub name: &'static str,
    pub group: &'static str,
    pub protocol: Protocol,
    pub port: PortSpec,
    pub risk: &'static str,
    pub status: CandidateStatus,
    pub score: u16,
    pub reason: String,
    pub recommendation: &'static str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReviewException {
    pub name: &'static str,
    pub group: &'static str,
    pub protocol: Protocol,
    pub port: PortSpec,
    pub risk: &'static str,
    pub note: &'static str,
}

impl ReviewException {
    fn from_candidate(candidate: &PortCandidate) -> Self {
        Self {
            name: candidate.name,
            group: candidate.group,
            protocol: candidate.protocol,
            port: candidate.port,
            risk: candidate.risk,
            note: REVIEW_EXCEPTION_NOTE,
        }
    }

    fn matches_candidate(&self, candidate: &PortCandidate) -> bool {
        self.protocol == candidate.protocol && self.port == candidate.port
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FirewallProfile {
    Workstation,
    WebServer,
    Developer,
    Lockdown,
}

impl FirewallProfile {
    pub fn all() -> [Self; 4] {
        [
            Self::Workstation,
            Self::WebServer,
            Self::Developer,
            Self::Lockdown,
        ]
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::Workstation => "workstation",
            Self::WebServer => "web-server",
            Self::Developer => "developer",
            Self::Lockdown => "lockdown",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateStatus {
    Blocked,
    Allowed,
    Open,
}

impl CandidateStatus {
    pub fn label(self) -> &'static str {
        match self {
            Self::Blocked => "blocked",
            Self::Allowed => "allowed",
            Self::Open => "open",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateFilter {
    All,
    Named,
    AtRisk,
    Open,
    Blocked,
    Allowed,
}

impl CandidateFilter {
    pub fn next(self) -> Self {
        match self {
            Self::All => Self::Named,
            Self::Named => Self::AtRisk,
            Self::AtRisk => Self::Open,
            Self::Open => Self::Blocked,
            Self::Blocked => Self::Allowed,
            Self::Allowed => Self::All,
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::All => "all",
            Self::Named => "named",
            Self::AtRisk => "at-risk",
            Self::Open => "open",
            Self::Blocked => "blocked",
            Self::Allowed => "allowed",
        }
    }

    fn matches(self, candidate: &PortCandidate, status: CandidateStatus) -> bool {
        match self {
            Self::All => true,
            Self::Named => {
                candidate.aliases.iter().any(|alias| {
                    alias.contains("ssh")
                        || alias.contains("nginx")
                        || alias.contains("postgres")
                        || alias.contains("docker")
                }) || matches!(candidate.name, "SSH" | "HTTP" | "Postgres" | "Docker API")
            }
            Self::AtRisk => matches!(candidate.risk, "high" | "data" | "admin"),
            Self::Open => status == CandidateStatus::Open,
            Self::Blocked => status == CandidateStatus::Blocked,
            Self::Allowed => status == CandidateStatus::Allowed,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannedCommand {
    pub backend: Backend,
    pub program: &'static str,
    pub args: Vec<String>,
    pub reason: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendPlanSummary {
    pub backend: Backend,
    pub commands: usize,
    pub first_command: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProfileImpactSummary {
    pub profile: FirewallProfile,
    pub staged_changes: usize,
    pub resulting_rules: usize,
    pub coverage_percent: u8,
    pub at_risk_open: usize,
    pub management_total: usize,
    pub management_blocked: usize,
    pub management_allowed: usize,
    pub management_open: usize,
    pub management_all_blocked: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileRecommendation {
    pub profile: FirewallProfile,
    pub staged_changes: usize,
    pub resulting_rules: usize,
    pub coverage_percent: u8,
    pub at_risk_open: usize,
    pub management_blocked: usize,
    pub management_allowed: usize,
    pub management_open: usize,
    pub management_all_blocked: bool,
    pub score: i32,
    pub reason: String,
    pub warning: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileChangePreview {
    pub profile: FirewallProfile,
    pub candidate_index: usize,
    pub name: &'static str,
    pub group: &'static str,
    pub protocol: Protocol,
    pub port: PortSpec,
    pub risk: &'static str,
    pub before: CandidateStatus,
    pub after: CandidateStatus,
    pub reason: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GroupCoverageSummary {
    pub group: &'static str,
    pub total: usize,
    pub blocked: usize,
    pub allowed: usize,
    pub open: usize,
    pub at_risk_open: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RiskCoverageSummary {
    pub risk: &'static str,
    pub total: usize,
    pub blocked: usize,
    pub allowed: usize,
    pub open: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ManagementAccessSummary {
    pub total: usize,
    pub blocked: usize,
    pub allowed: usize,
    pub open: usize,
}

impl ManagementAccessSummary {
    pub fn all_blocked(self) -> bool {
        self.total > 0 && self.blocked == self.total
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FirewallStats {
    pub total_rules: usize,
    pub enabled_rules: usize,
    pub allow_rules: usize,
    pub deny_rules: usize,
    pub reject_rules: usize,
    pub inbound_rules: usize,
    pub outbound_rules: usize,
    pub known_ports: usize,
    pub known_blocked: usize,
    pub known_allowed: usize,
    pub known_open: usize,
    pub at_risk_open: usize,
    pub at_risk_allowed: usize,
    pub reviewed_exceptions: usize,
    pub unreviewed_at_risk_allowed: usize,
    pub high_risk_blocks: usize,
    pub plan_commands: usize,
    pub coverage_percent: u8,
    pub risk_percent: u8,
}

impl FirewallStats {
    pub fn readiness_grade(self) -> &'static str {
        match self.risk_percent {
            90..=100 => "A",
            75..=89 => "B",
            50..=74 => "C",
            25..=49 => "D",
            _ => "F",
        }
    }

    pub fn readiness_label(self) -> &'static str {
        match self.risk_percent {
            90..=100 => "ready for review",
            75..=89 => "nearly ready",
            50..=74 => "needs hardening",
            25..=49 => "high risk",
            _ => "unsafe draft",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdvisorSeverity {
    Critical,
    Warning,
    Info,
    Good,
}

impl AdvisorSeverity {
    pub fn label(self) -> &'static str {
        match self {
            Self::Critical => "critical",
            Self::Warning => "warning",
            Self::Info => "info",
            Self::Good => "good",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvisorFinding {
    pub severity: AdvisorSeverity,
    pub title: String,
    pub detail: String,
    pub recommendation: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvisorNextAction {
    pub severity: AdvisorSeverity,
    pub title: String,
    pub recommendation: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReviewOrderStep {
    pub title: String,
    pub detail: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewGateStatus {
    Pass,
    Warn,
    Block,
}

impl ReviewGateStatus {
    pub fn label(self) -> &'static str {
        match self {
            Self::Pass => "pass",
            Self::Warn => "warn",
            Self::Block => "block",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReviewGate {
    pub status: ReviewGateStatus,
    pub title: String,
    pub detail: String,
    pub action: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReviewGateSummary {
    pub pass: usize,
    pub warn: usize,
    pub block: usize,
}

impl ReviewGateSummary {
    fn from_gates(gates: &[ReviewGate]) -> Self {
        let mut summary = Self {
            pass: 0,
            warn: 0,
            block: 0,
        };

        for gate in gates {
            match gate.status {
                ReviewGateStatus::Pass => summary.pass += 1,
                ReviewGateStatus::Warn => summary.warn += 1,
                ReviewGateStatus::Block => summary.block += 1,
            }
        }

        summary
    }
}

impl AdvisorNextAction {
    fn from_findings(findings: &[AdvisorFinding]) -> Self {
        findings
            .first()
            .map(|finding| Self {
                severity: finding.severity,
                title: finding.title.clone(),
                recommendation: finding.recommendation.clone(),
            })
            .unwrap_or_else(|| Self {
                severity: AdvisorSeverity::Good,
                title: "Dry-run plan has no advisor findings".into(),
                recommendation:
                    "Review generated commands before any future live executor is added.".into(),
            })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdvisorSeveritySummary {
    pub critical: usize,
    pub warning: usize,
    pub info: usize,
    pub good: usize,
}

impl AdvisorSeveritySummary {
    pub fn total(self) -> usize {
        self.critical + self.warning + self.info + self.good
    }

    fn from_findings(findings: &[AdvisorFinding]) -> Self {
        let mut summary = Self {
            critical: 0,
            warning: 0,
            info: 0,
            good: 0,
        };

        for finding in findings {
            match finding.severity {
                AdvisorSeverity::Critical => summary.critical += 1,
                AdvisorSeverity::Warning => summary.warning += 1,
                AdvisorSeverity::Info => summary.info += 1,
                AdvisorSeverity::Good => summary.good += 1,
            }
        }

        summary
    }
}

impl PlannedCommand {
    pub fn shell_line(&self) -> String {
        let mut parts = Vec::with_capacity(self.args.len() + 1);
        parts.push(self.program.to_string());
        parts.extend(self.args.iter().map(|arg| shell_quote(arg)));
        parts.join(" ")
    }
}

#[derive(Debug, Clone)]
pub struct FirewallController {
    mode: ExecutionMode,
    backend: Backend,
    default_incoming: DefaultPolicy,
    default_outgoing: DefaultPolicy,
    logging: bool,
    rules: Vec<FirewallRule>,
    candidates: Vec<PortCandidate>,
    review_exceptions: Vec<ReviewException>,
    next_id: u32,
    audit_log: Vec<String>,
    history: Vec<ControllerSnapshot>,
}

#[derive(Debug, Clone)]
struct ControllerSnapshot {
    backend: Backend,
    default_incoming: DefaultPolicy,
    default_outgoing: DefaultPolicy,
    logging: bool,
    rules: Vec<FirewallRule>,
    review_exceptions: Vec<ReviewException>,
    next_id: u32,
}

impl FirewallController {
    pub fn demo() -> Self {
        let mut controller = Self {
            mode: ExecutionMode::DryRun,
            backend: Backend::Ufw,
            default_incoming: DefaultPolicy::Deny,
            default_outgoing: DefaultPolicy::Allow,
            logging: true,
            rules: Vec::new(),
            candidates: default_candidates(),
            review_exceptions: Vec::new(),
            next_id: 1,
            audit_log: vec![
                "Dry-run controller started. No live firewall writes are available.".into(),
            ],
            history: Vec::new(),
        };

        controller.stage_starter_rules();
        controller
    }

    pub fn mode(&self) -> ExecutionMode {
        self.mode
    }

    pub fn backend(&self) -> Backend {
        self.backend
    }

    pub fn default_incoming(&self) -> DefaultPolicy {
        self.default_incoming
    }

    pub fn default_outgoing(&self) -> DefaultPolicy {
        self.default_outgoing
    }

    pub fn logging(&self) -> bool {
        self.logging
    }

    pub fn rules(&self) -> &[FirewallRule] {
        &self.rules
    }

    pub fn candidates(&self) -> &[PortCandidate] {
        &self.candidates
    }

    pub fn review_exceptions(&self) -> &[ReviewException] {
        &self.review_exceptions
    }

    pub fn candidate_status(&self, index: usize) -> Option<CandidateStatus> {
        self.candidates
            .get(index)
            .map(|candidate| self.candidate_status_for(candidate))
    }

    pub fn candidate_has_review_exception(&self, index: usize) -> bool {
        self.candidates
            .get(index)
            .is_some_and(|candidate| self.has_review_exception(candidate))
    }

    pub fn candidate_group(&self, index: usize) -> Option<&'static str> {
        self.candidates.get(index).map(|candidate| candidate.group)
    }

    pub fn candidate_risk(&self, index: usize) -> Option<&'static str> {
        self.candidates.get(index).map(|candidate| candidate.risk)
    }

    pub fn filtered_candidate_indexes(
        &self,
        filter: CandidateFilter,
        group: Option<&str>,
        risk: Option<&str>,
    ) -> Vec<usize> {
        self.candidates
            .iter()
            .enumerate()
            .filter(|(_, candidate)| group.map_or(true, |group| candidate.group == group))
            .filter(|(_, candidate)| risk.map_or(true, |risk| candidate.risk == risk))
            .filter_map(|(index, candidate)| {
                let status = self.candidate_status_for(candidate);
                filter.matches(candidate, status).then_some(index)
            })
            .collect()
    }

    pub fn candidate_groups(&self) -> Vec<&'static str> {
        let mut groups = Vec::new();
        for candidate in &self.candidates {
            if !groups.contains(&candidate.group) {
                groups.push(candidate.group);
            }
        }
        groups
    }

    pub fn candidate_risks(&self) -> Vec<&'static str> {
        let mut risks = Vec::new();
        for candidate in &self.candidates {
            if !risks.contains(&candidate.risk) {
                risks.push(candidate.risk);
            }
        }
        risks
    }

    pub fn group_coverage_summaries(&self) -> Vec<GroupCoverageSummary> {
        self.candidate_groups()
            .into_iter()
            .map(|group| {
                let mut summary = GroupCoverageSummary {
                    group,
                    total: 0,
                    blocked: 0,
                    allowed: 0,
                    open: 0,
                    at_risk_open: 0,
                };

                for candidate in self
                    .candidates
                    .iter()
                    .filter(|candidate| candidate.group == group)
                {
                    summary.total += 1;
                    let status = self.candidate_status_for(candidate);
                    match status {
                        CandidateStatus::Blocked => summary.blocked += 1,
                        CandidateStatus::Allowed => summary.allowed += 1,
                        CandidateStatus::Open => {
                            summary.open += 1;
                            if matches!(candidate.risk, "high" | "data" | "admin") {
                                summary.at_risk_open += 1;
                            }
                        }
                    }
                }

                summary
            })
            .collect()
    }

    pub fn risk_coverage_summaries(&self) -> Vec<RiskCoverageSummary> {
        self.candidate_risks()
            .into_iter()
            .map(|risk| {
                let mut summary = RiskCoverageSummary {
                    risk,
                    total: 0,
                    blocked: 0,
                    allowed: 0,
                    open: 0,
                };

                for candidate in self
                    .candidates
                    .iter()
                    .filter(|candidate| candidate.risk == risk)
                {
                    summary.total += 1;
                    match self.candidate_status_for(candidate) {
                        CandidateStatus::Blocked => summary.blocked += 1,
                        CandidateStatus::Allowed => summary.allowed += 1,
                        CandidateStatus::Open => summary.open += 1,
                    }
                }

                summary
            })
            .collect()
    }

    pub fn management_access_summary(&self) -> ManagementAccessSummary {
        let mut summary = ManagementAccessSummary {
            total: 0,
            blocked: 0,
            allowed: 0,
            open: 0,
        };

        for candidate in self
            .candidates
            .iter()
            .filter(|candidate| matches!(candidate.name, "SSH" | "RDP" | "VNC" | "Kubernetes"))
        {
            summary.total += 1;
            match self.candidate_status_for(candidate) {
                CandidateStatus::Blocked => summary.blocked += 1,
                CandidateStatus::Allowed => summary.allowed += 1,
                CandidateStatus::Open => summary.open += 1,
            }
        }

        summary
    }

    pub fn last_message(&self) -> &str {
        self.audit_log
            .last()
            .map(String::as_str)
            .unwrap_or("No activity yet")
    }

    pub fn audit_log(&self) -> &[String] {
        &self.audit_log
    }

    pub fn record_report_export(&mut self, path: &str) {
        self.audit_log
            .push(format!("Exported dry-run report to {path}."));
    }

    pub fn record_script_export(&mut self, path: &str) {
        self.audit_log
            .push(format!("Exported review-only command script to {path}."));
    }

    pub fn record_manifest_export(&mut self, path: &str) {
        self.audit_log
            .push(format!("Exported review-only JSON manifest to {path}."));
    }

    pub fn record_bundle_export(&mut self) {
        self.audit_log
            .push("Exported review-only report, script, and JSON manifest bundle.".into());
    }

    pub fn history_depth(&self) -> usize {
        self.history.len()
    }

    pub fn stats(&self) -> FirewallStats {
        let total_rules = self.rules.len();
        let enabled_rules = self.rules.iter().filter(|rule| rule.enabled).count();
        let allow_rules = self
            .rules
            .iter()
            .filter(|rule| rule.action == RuleAction::Allow)
            .count();
        let deny_rules = self
            .rules
            .iter()
            .filter(|rule| rule.action == RuleAction::Deny)
            .count();
        let reject_rules = self
            .rules
            .iter()
            .filter(|rule| rule.action == RuleAction::Reject)
            .count();
        let inbound_rules = self
            .rules
            .iter()
            .filter(|rule| rule.direction == Direction::In)
            .count();
        let outbound_rules = self
            .rules
            .iter()
            .filter(|rule| rule.direction == Direction::Out)
            .count();
        let known_ports = self.candidates.len();
        let mut known_blocked = 0;
        let mut known_allowed = 0;
        let mut known_open = 0;
        let mut high_risk_blocks = 0;
        let mut high_risk_total = 0;
        let mut at_risk_open = 0;
        let mut at_risk_allowed = 0usize;
        let mut reviewed_exceptions = 0usize;

        for candidate in &self.candidates {
            let status = self.candidate_status_for(candidate);
            match status {
                CandidateStatus::Blocked => known_blocked += 1,
                CandidateStatus::Allowed => known_allowed += 1,
                CandidateStatus::Open => known_open += 1,
            }

            if matches!(candidate.risk, "high" | "data" | "admin") {
                high_risk_total += 1;
                match status {
                    CandidateStatus::Blocked => high_risk_blocks += 1,
                    CandidateStatus::Open => at_risk_open += 1,
                    CandidateStatus::Allowed => {
                        at_risk_allowed += 1;
                        if self.has_review_exception(candidate) {
                            reviewed_exceptions += 1;
                        }
                    }
                }
            }
        }

        let unreviewed_at_risk_allowed = at_risk_allowed.saturating_sub(reviewed_exceptions);

        let high_risk_total = high_risk_total.max(1);
        let coverage_percent = ((high_risk_blocks * 100) / high_risk_total) as u8;
        let policy_score = match self.default_incoming {
            DefaultPolicy::Deny => 25,
            DefaultPolicy::Allow => 0,
        };
        let logging_score = if self.logging { 10 } else { 0 };
        let rule_score = (enabled_rules * 8).min(45);
        let coverage_score = (coverage_percent as usize / 5).min(20);
        let risk_percent =
            (policy_score + logging_score + rule_score + coverage_score).min(100) as u8;

        FirewallStats {
            total_rules,
            enabled_rules,
            allow_rules,
            deny_rules,
            reject_rules,
            inbound_rules,
            outbound_rules,
            known_ports,
            known_blocked,
            known_allowed,
            known_open,
            at_risk_open,
            at_risk_allowed,
            reviewed_exceptions,
            unreviewed_at_risk_allowed,
            high_risk_blocks,
            plan_commands: self.plan().len(),
            coverage_percent,
            risk_percent,
        }
    }

    pub fn toggle_default_incoming(&mut self) {
        self.save_checkpoint();
        self.default_incoming = self.default_incoming.next();
        self.audit_log.push(format!(
            "Default incoming policy changed to {}.",
            self.default_incoming.label()
        ));
    }

    pub fn toggle_default_outgoing(&mut self) {
        self.save_checkpoint();
        self.default_outgoing = self.default_outgoing.next();
        self.audit_log.push(format!(
            "Default outgoing policy changed to {}.",
            self.default_outgoing.label()
        ));
    }

    pub fn clear_rules(&mut self) -> usize {
        let removed = self.rules.len();
        if removed == 0 {
            self.audit_log.push("No staged rules to clear.".into());
            return 0;
        }

        self.save_checkpoint();
        self.rules.clear();
        self.review_exceptions.clear();
        self.audit_log
            .push(format!("Cleared {removed} staged dry-run rule(s)."));
        removed
    }

    pub fn reset_to_starter_plan(&mut self) -> usize {
        self.save_checkpoint();
        self.backend = Backend::Ufw;
        self.default_incoming = DefaultPolicy::Deny;
        self.default_outgoing = DefaultPolicy::Allow;
        self.logging = true;
        self.rules.clear();
        self.review_exceptions.clear();
        self.next_id = 1;
        self.stage_starter_rules();

        let restored = self.rules.len();
        self.audit_log.push(format!(
            "Restored starter dry-run plan with {restored} staged rule(s)."
        ));
        restored
    }

    pub fn cycle_backend(&mut self) {
        self.save_checkpoint();
        self.backend = self.backend.next();
        self.audit_log
            .push(format!("Preview backend changed to {}.", self.backend));
    }

    pub fn toggle_logging(&mut self) {
        self.save_checkpoint();
        self.logging = !self.logging;
        self.audit_log.push(format!(
            "Planned firewall logging is now {}.",
            if self.logging { "enabled" } else { "disabled" }
        ));
    }

    pub fn toggle_rule(&mut self, index: usize) {
        if index >= self.rules.len() {
            return;
        }
        self.save_checkpoint();
        let Some(rule) = self.rules.get_mut(index) else {
            return;
        };
        rule.enabled = !rule.enabled;
        self.audit_log.push(format!(
            "Rule #{} is now {}.",
            rule.id,
            if rule.enabled { "enabled" } else { "disabled" }
        ));
        self.prune_review_exceptions();
    }

    pub fn remove_rule(&mut self, index: usize) {
        if index >= self.rules.len() {
            return;
        }

        self.save_checkpoint();
        let removed = self.rules.remove(index);
        self.prune_review_exceptions();
        self.audit_log
            .push(format!("Removed staged rule #{}.", removed.id));
    }

    pub fn block_candidate(&mut self, index: usize) {
        let Some(candidate) = self.candidates.get(index).cloned() else {
            return;
        };

        if self.has_rule(
            RuleAction::Deny,
            Direction::In,
            candidate.protocol,
            candidate.port,
        ) {
            self.audit_log.push(format!(
                "Block for {} {} already staged.",
                candidate.name, candidate.port
            ));
            return;
        }

        self.save_checkpoint();
        self.remove_candidate_action(candidate.protocol, candidate.port, RuleAction::Allow);
        self.remove_review_exception_for(&candidate);

        self.stage_rule(
            RuleAction::Deny,
            Direction::In,
            candidate.protocol,
            candidate.port,
            &format!("Block {}", candidate.name),
        );
    }

    pub fn allow_candidate(&mut self, index: usize) {
        let Some(candidate) = self.candidates.get(index).cloned() else {
            return;
        };

        if self.has_rule(
            RuleAction::Allow,
            Direction::In,
            candidate.protocol,
            candidate.port,
        ) {
            self.audit_log.push(format!(
                "Allow for {} {} already staged.",
                candidate.name, candidate.port
            ));
            return;
        }

        self.save_checkpoint();
        self.remove_candidate_blocks(candidate.protocol, candidate.port);

        self.stage_rule(
            RuleAction::Allow,
            Direction::In,
            candidate.protocol,
            candidate.port,
            &format!("Allow {}", candidate.name),
        );
    }

    pub fn toggle_candidate_block(&mut self, index: usize) -> Option<bool> {
        let candidate = self.candidates.get(index).cloned()?;
        self.save_checkpoint();
        if self.candidate_status_for(&candidate) == CandidateStatus::Blocked {
            let removed = self.remove_candidate_blocks(candidate.protocol, candidate.port);
            self.prune_review_exceptions();
            self.audit_log.push(format!(
                "Unblocked {} {} by removing {} staged block rule(s).",
                candidate.name, candidate.port, removed
            ));
            return Some(false);
        }

        self.remove_candidate_action(candidate.protocol, candidate.port, RuleAction::Allow);
        self.remove_review_exception_for(&candidate);
        self.stage_rule(
            RuleAction::Deny,
            Direction::In,
            candidate.protocol,
            candidate.port,
            &format!("Block {}", candidate.name),
        );
        Some(true)
    }

    pub fn toggle_candidate_exception(&mut self, index: usize) -> Option<bool> {
        let candidate = self.candidates.get(index).cloned()?;

        if !matches!(candidate.risk, "high" | "data" | "admin") {
            self.audit_log.push(format!(
                "{} is not classified as a risky exception candidate.",
                candidate.name
            ));
            return None;
        }

        if self.candidate_status_for(&candidate) != CandidateStatus::Allowed {
            self.audit_log.push(format!(
                "Allow {} before recording a reviewed exception.",
                candidate.name
            ));
            return None;
        }

        self.save_checkpoint();
        if self.remove_review_exception_for(&candidate) {
            self.audit_log.push(format!(
                "Removed reviewed exception for {} {}.",
                candidate.name, candidate.port
            ));
            Some(false)
        } else {
            self.review_exceptions
                .push(ReviewException::from_candidate(&candidate));
            self.audit_log.push(format!(
                "Recorded reviewed exception for {} {}.",
                candidate.name, candidate.port
            ));
            Some(true)
        }
    }

    pub fn block_high_risk_candidates(&mut self) -> usize {
        self.save_checkpoint();
        let candidates = self
            .candidates
            .iter()
            .filter(|candidate| matches!(candidate.risk, "high" | "data" | "admin"))
            .cloned()
            .collect::<Vec<_>>();
        let mut staged = 0;

        for candidate in candidates {
            if self.candidate_status_for(&candidate) == CandidateStatus::Blocked {
                continue;
            }
            self.remove_candidate_action(candidate.protocol, candidate.port, RuleAction::Allow);
            self.stage_rule(
                RuleAction::Deny,
                Direction::In,
                candidate.protocol,
                candidate.port,
                &format!("Block {}", candidate.name),
            );
            staged += 1;
        }

        if staged == 0 {
            self.audit_log
                .push("All high-risk known ports already have staged blocks.".into());
        } else {
            self.audit_log
                .push(format!("Hardened {} high-risk known port(s).", staged));
        }
        staged
    }

    pub fn block_candidate_group(&mut self, index: usize) -> usize {
        let Some(group) = self.candidate_group(index) else {
            return 0;
        };
        self.save_checkpoint();
        let candidates = self
            .candidates
            .iter()
            .filter(|candidate| candidate.group == group)
            .cloned()
            .collect::<Vec<_>>();
        let mut staged = 0;

        for candidate in candidates {
            if self.block_candidate_value(&candidate) {
                staged += 1;
            }
        }

        self.audit_log.push(format!(
            "Blocked {} known port(s) in group {group}.",
            staged
        ));
        staged
    }

    pub fn block_candidate_risk(&mut self, index: usize) -> usize {
        let Some(risk) = self.candidate_risk(index) else {
            return 0;
        };
        self.save_checkpoint();
        let candidates = self
            .candidates
            .iter()
            .filter(|candidate| candidate.risk == risk)
            .cloned()
            .collect::<Vec<_>>();
        let mut staged = 0;

        for candidate in candidates {
            if self.block_candidate_value(&candidate) {
                staged += 1;
            }
        }

        self.audit_log.push(format!(
            "Blocked {} known port(s) in risk class {risk}.",
            staged
        ));
        staged
    }

    pub fn block_candidate_indexes(&mut self, indexes: &[usize]) -> usize {
        let candidates = indexes
            .iter()
            .filter_map(|index| self.candidates.get(*index))
            .cloned()
            .collect::<Vec<_>>();

        if candidates.is_empty() {
            return 0;
        }

        self.save_checkpoint();
        let mut staged = 0;

        for candidate in candidates {
            if self.block_candidate_value(&candidate) {
                staged += 1;
            }
        }

        self.audit_log.push(format!(
            "Blocked {} known port(s) from active filter.",
            staged
        ));
        staged
    }

    pub fn allow_candidate_indexes(&mut self, indexes: &[usize]) -> usize {
        let candidates = indexes
            .iter()
            .filter_map(|index| self.candidates.get(*index))
            .cloned()
            .collect::<Vec<_>>();

        if candidates.is_empty() {
            return 0;
        }

        self.save_checkpoint();
        let mut staged = 0;

        for candidate in candidates {
            if self.allow_candidate_value(&candidate) {
                staged += 1;
            }
        }

        self.audit_log.push(format!(
            "Allowed {} known port(s) from active filter.",
            staged
        ));
        staged
    }

    pub fn apply_profile(&mut self, profile: FirewallProfile) -> usize {
        self.save_checkpoint();
        let mut staged = 0;
        let candidates = self.candidates.clone();

        for candidate in &candidates {
            let block = match profile {
                FirewallProfile::Workstation => matches!(
                    candidate.group,
                    "legacy" | "file" | "data" | "admin" | "dev" | "orchestration"
                ),
                FirewallProfile::WebServer => matches!(
                    candidate.group,
                    "legacy" | "file" | "data" | "dev" | "orchestration" | "observability"
                ),
                FirewallProfile::Developer => matches!(
                    candidate.group,
                    "legacy" | "file" | "data" | "orchestration"
                ),
                FirewallProfile::Lockdown => true,
            };

            if block && self.block_candidate_value(candidate) {
                staged += 1;
            }
        }

        if profile == FirewallProfile::WebServer {
            staged += self.allow_candidate_named("HTTP") as usize;
            staged += self.allow_candidate_named("HTTPS") as usize;
        }

        self.audit_log.push(format!(
            "Applied {} dry-run profile with {} staged change(s).",
            profile.label(),
            staged
        ));
        staged
    }

    pub fn apply_recommended_profile(&mut self) -> (FirewallProfile, usize) {
        let profile = self.recommended_profile().profile;
        let staged = self.apply_profile(profile);
        self.audit_log.push(format!(
            "Applied recommended {} dry-run profile.",
            profile.label()
        ));
        (profile, staged)
    }

    pub fn apply_safe_recommended_profile(&mut self) -> Option<(FirewallProfile, usize)> {
        let profile = self.safe_recommended_profile()?.profile;
        let staged = self.apply_profile(profile);
        self.audit_log.push(format!(
            "Applied management-preserving recommended {} dry-run profile.",
            profile.label()
        ));
        Some((profile, staged))
    }

    pub fn apply_advisor_quick_fix(&mut self) -> usize {
        self.save_checkpoint();
        let mut changes = 0;

        if self.default_incoming != DefaultPolicy::Deny {
            self.default_incoming = DefaultPolicy::Deny;
            changes += 1;
        }

        if !self.logging {
            self.logging = true;
            changes += 1;
        }

        let candidates = self
            .candidates
            .iter()
            .filter(|candidate| matches!(candidate.risk, "high" | "data" | "admin"))
            .cloned()
            .collect::<Vec<_>>();
        for candidate in candidates {
            if self.block_candidate_value(&candidate) {
                changes += 1;
            }
        }

        self.audit_log.push(format!(
            "Advisor quick fix staged {} dry-run change(s).",
            changes
        ));
        changes
    }

    pub fn undo(&mut self) -> bool {
        let Some(snapshot) = self.history.pop() else {
            self.audit_log.push("Nothing to undo.".into());
            return false;
        };

        self.backend = snapshot.backend;
        self.default_incoming = snapshot.default_incoming;
        self.default_outgoing = snapshot.default_outgoing;
        self.logging = snapshot.logging;
        self.rules = snapshot.rules;
        self.review_exceptions = snapshot.review_exceptions;
        self.next_id = snapshot.next_id;
        self.audit_log.push("Undid last staged change.".into());
        true
    }

    pub fn advisor_findings(&self) -> Vec<AdvisorFinding> {
        let mut findings = Vec::new();
        let management_access = self.management_access_summary();

        if self.default_incoming == DefaultPolicy::Allow {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Critical,
                title: "Default incoming policy allows traffic".into(),
                detail: "A replacement for ufw should default-deny inbound traffic unless explicitly opened.".into(),
                recommendation: "Press I or use advisor quick fix to stage a default deny incoming policy.".into(),
            });
        }

        if self.default_outgoing == DefaultPolicy::Deny {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Warning,
                title: "Default outgoing policy denies traffic".into(),
                detail: "A strict egress default can break updates, package installs, DNS, and package mirrors unless allow rules are planned.".into(),
                recommendation: "Press O if this host should keep the common allow-outgoing baseline.".into(),
            });
        }

        if !self.logging {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Warning,
                title: "Firewall logging is disabled in the plan".into(),
                detail: "You will have less visibility into rejected traffic.".into(),
                recommendation: "Press l or use advisor quick fix to stage logging.".into(),
            });
        }

        if self.history_depth() > 0 {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Info,
                title: "Undo checkpoints are available".into(),
                detail: format!(
                    "{} in-memory checkpoint(s) can be restored without touching the system.",
                    self.history_depth()
                ),
                recommendation: "Press u to undo the last staged dry-run change.".into(),
            });
        }

        for candidate in &self.candidates {
            let status = self.candidate_status_for(candidate);
            if matches!(candidate.risk, "high" | "data" | "admin")
                && status == CandidateStatus::Open
            {
                findings.push(AdvisorFinding {
                    severity: AdvisorSeverity::Warning,
                    title: format!("{} is not blocked", candidate.name),
                    detail: format!(
                        "{}/{} ({}) is a {} service: {}.",
                        candidate.port, candidate.protocol, candidate.group, candidate.risk, candidate.detail
                    ),
                    recommendation: "Select it in Known Ports and press Enter, or press h to harden all high-risk ports.".into(),
                });
            }

            if matches!(candidate.risk, "high" | "data" | "admin")
                && status == CandidateStatus::Allowed
            {
                if self.has_review_exception(candidate) {
                    findings.push(AdvisorFinding {
                        severity: AdvisorSeverity::Warning,
                        title: format!("{} has a reviewed allow exception", candidate.name),
                        detail: format!(
                            "{}/{} remains staged open as a reviewed {} exception: {}.",
                            candidate.port, candidate.protocol, candidate.risk, REVIEW_EXCEPTION_NOTE
                        ),
                        recommendation: "Verify owner, source scope, and expiry before any future live executor uses this plan.".into(),
                    });
                } else {
                    findings.push(AdvisorFinding {
                        severity: AdvisorSeverity::Critical,
                        title: format!("{} is explicitly allowed", candidate.name),
                        detail: format!(
                            "{}/{} is staged open even though it is classified as {}.",
                            candidate.port, candidate.protocol, candidate.risk
                        ),
                        recommendation: "Block it or press X to record a reviewed exception for an intentional exposure.".into(),
                    });
                }
            }
        }

        if management_access.all_blocked() {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Warning,
                title: "Management access is fully blocked".into(),
                detail: format!(
                    "All {} known management/control-plane access path(s) are blocked in the dry-run plan.",
                    management_access.total
                ),
                recommendation: "Confirm local console, break-glass, or out-of-band access before any future live use.".into(),
            });
        }

        for (left_idx, left) in self.rules.iter().enumerate() {
            if !left.enabled {
                continue;
            }
            for right in self
                .rules
                .iter()
                .skip(left_idx + 1)
                .filter(|rule| rule.enabled)
            {
                let same_target = left.direction == right.direction
                    && left.protocol == right.protocol
                    && left.port == right.port;
                let conflict = matches!(
                    (left.action, right.action),
                    (RuleAction::Allow, RuleAction::Deny | RuleAction::Reject)
                        | (RuleAction::Deny | RuleAction::Reject, RuleAction::Allow)
                );
                if same_target && conflict {
                    findings.push(AdvisorFinding {
                        severity: AdvisorSeverity::Critical,
                        title: format!("Conflicting rules for {}/{}", left.port, left.protocol),
                        detail: format!(
                            "Rule #{} and #{} target the same direction/protocol/port with opposite actions.",
                            left.id, right.id
                        ),
                        recommendation: "Remove or disable the stale rule before applying anything live.".into(),
                    });
                }
            }
        }

        if findings.is_empty() {
            findings.push(AdvisorFinding {
                severity: AdvisorSeverity::Good,
                title: "Dry-run plan has no obvious high-risk gaps".into(),
                detail: "Known high-risk ports are covered and no conflicting staged rules were found.".into(),
                recommendation: "Review the generated commands before any future live executor is added.".into(),
            });
        } else {
            findings.sort_by_key(|finding| match finding.severity {
                AdvisorSeverity::Critical => 0,
                AdvisorSeverity::Warning => 1,
                AdvisorSeverity::Info => 2,
                AdvisorSeverity::Good => 3,
            });
        }

        findings
    }

    pub fn advisor_summary(&self) -> AdvisorSeveritySummary {
        AdvisorSeveritySummary::from_findings(&self.advisor_findings())
    }

    pub fn advisor_next_action(&self) -> AdvisorNextAction {
        AdvisorNextAction::from_findings(&self.advisor_findings())
    }

    pub fn review_gates(&self) -> Vec<ReviewGate> {
        let stats = self.stats();
        let findings = self.advisor_findings();
        let advisor_summary = AdvisorSeveritySummary::from_findings(&findings);
        let backend_summaries = self.backend_plan_summaries();
        let management_access = self.management_access_summary();
        let mut gates = Vec::new();

        gates.push(ReviewGate {
            status: ReviewGateStatus::Pass,
            title: "Dry-run safety lock".into(),
            detail: "No live executor exists; generated firewall commands are review-only.".into(),
            action: "Keep exports in review flow before any future guarded live executor.".into(),
        });

        gates.push(ReviewGate {
            status: if self.default_incoming == DefaultPolicy::Deny {
                ReviewGateStatus::Pass
            } else {
                ReviewGateStatus::Block
            },
            title: "Default incoming policy".into(),
            detail: format!(
                "Planned default incoming policy is {}.",
                self.default_incoming.label()
            ),
            action: if self.default_incoming == DefaultPolicy::Deny {
                "Default-deny inbound baseline is staged.".into()
            } else {
                "Press I or run advisor quick fix before review sign-off.".into()
            },
        });

        gates.push(ReviewGate {
            status: if advisor_summary.critical == 0 {
                ReviewGateStatus::Pass
            } else {
                ReviewGateStatus::Block
            },
            title: "Critical advisor findings".into(),
            detail: format!("{} critical finding(s) remain.", advisor_summary.critical),
            action: if advisor_summary.critical == 0 {
                "No critical advisor findings are currently present.".into()
            } else {
                "Resolve critical advisor findings before review sign-off.".into()
            },
        });

        gates.push(ReviewGate {
            status: if stats.at_risk_open == 0 {
                ReviewGateStatus::Pass
            } else {
                ReviewGateStatus::Warn
            },
            title: "At-risk open known ports".into(),
            detail: format!(
                "{} admin/high/data known port(s) remain open.",
                stats.at_risk_open
            ),
            action: if stats.at_risk_open == 0 {
                "No at-risk known ports remain open.".into()
            } else {
                "Use n, M, h, or Ports filters to block or document the exposure.".into()
            },
        });

        let exception_status = if stats.unreviewed_at_risk_allowed > 0 {
            ReviewGateStatus::Block
        } else if stats.reviewed_exceptions > 0 {
            ReviewGateStatus::Warn
        } else {
            ReviewGateStatus::Pass
        };
        gates.push(ReviewGate {
            status: exception_status,
            title: "Risky allow exceptions".into(),
            detail: format!(
                "{} reviewed exception(s), {} unreviewed risky allow(s).",
                stats.reviewed_exceptions, stats.unreviewed_at_risk_allowed
            ),
            action: match exception_status {
                ReviewGateStatus::Pass => "No risky allow exceptions require review.".into(),
                ReviewGateStatus::Warn => {
                    "Validate owner, source scope, and expiry for each reviewed exception.".into()
                }
                ReviewGateStatus::Block => {
                    "Block the risky allow or press X to record a reviewed exception.".into()
                }
            },
        });

        gates.push(ReviewGate {
            status: if management_access.all_blocked() {
                ReviewGateStatus::Warn
            } else {
                ReviewGateStatus::Pass
            },
            title: "Management access continuity".into(),
            detail: format!(
                "{} management/control-plane path(s): {} blocked, {} allowed, {} open.",
                management_access.total,
                management_access.blocked,
                management_access.allowed,
                management_access.open
            ),
            action: if management_access.all_blocked() {
                "Confirm local console, break-glass, or out-of-band access before any future live use.".into()
            } else {
                "Management access is not fully blocked; review exposure separately.".into()
            },
        });

        let recommended_profile = self.recommended_profile();
        let safe_recommended_profile = self.safe_recommended_profile();
        gates.push(ReviewGate {
            status: if recommended_profile.management_all_blocked {
                ReviewGateStatus::Warn
            } else {
                ReviewGateStatus::Pass
            },
            title: "Recommended profile safety".into(),
            detail: format!(
                "Strongest {} profile would leave management access at {} blocked, {} allowed, {} open.",
                recommended_profile.profile.label(),
                recommended_profile.management_blocked,
                recommended_profile.management_allowed,
                recommended_profile.management_open
            ),
            action: if recommended_profile.management_all_blocked {
                safe_recommended_profile
                    .map(|recommendation| {
                        format!(
                            "Press y for the management-preserving {} profile, or confirm console access before Y.",
                            recommendation.profile.label()
                        )
                    })
                    .unwrap_or_else(|| {
                        "Confirm console, break-glass, or out-of-band access before pressing Y."
                            .into()
                    })
            } else {
                "Strongest recommendation preserves at least one management path.".into()
            },
        });

        gates.push(ReviewGate {
            status: if stats.plan_commands > 0 {
                ReviewGateStatus::Pass
            } else {
                ReviewGateStatus::Warn
            },
            title: "Command preview generated".into(),
            detail: format!(
                "{} command(s) generated for {} preview.",
                stats.plan_commands,
                self.backend()
            ),
            action: if stats.plan_commands > 0 {
                "Review the Plan tab or exported script before any manual use.".into()
            } else {
                "Stage rules or defaults before relying on an empty preview.".into()
            },
        });

        let missing_backend = backend_summaries
            .iter()
            .find(|summary| summary.commands == 0)
            .map(|summary| summary.backend.to_string());
        gates.push(ReviewGate {
            status: if missing_backend.is_none() {
                ReviewGateStatus::Pass
            } else {
                ReviewGateStatus::Warn
            },
            title: "Backend parity preview".into(),
            detail: backend_summaries
                .iter()
                .map(|summary| format!("{} {}", summary.backend, summary.commands))
                .collect::<Vec<_>>()
                .join(" | "),
            action: missing_backend
                .map(|backend| format!("Review why {backend} generated no preview commands."))
                .unwrap_or_else(|| {
                    "UFW, iptables, and nftables previews all produce commands.".into()
                }),
        });

        gates.push(ReviewGate {
            status: ReviewGateStatus::Pass,
            title: "Artifact fingerprint".into(),
            detail: format!("Current review fingerprint is {}.", self.plan_fingerprint()),
            action: "Compare this fingerprint across report, manifest, and script exports.".into(),
        });

        gates
    }

    pub fn review_gate_summary(&self) -> ReviewGateSummary {
        ReviewGateSummary::from_gates(&self.review_gates())
    }

    pub fn next_review_gate(&self) -> ReviewGate {
        self.review_gates()
            .into_iter()
            .find(|gate| gate.status != ReviewGateStatus::Pass)
            .unwrap_or_else(|| ReviewGate {
                status: ReviewGateStatus::Pass,
                title: "All review gates passed".into(),
                detail: "No blocker or warning gates remain in the dry-run review.".into(),
                action: "Export the review bundle and compare fingerprints.".into(),
            })
    }

    pub fn priority_ports(&self, limit: usize) -> Vec<PortPriority> {
        let mut priorities = self
            .candidates
            .iter()
            .enumerate()
            .filter_map(|(index, candidate)| {
                let status = self.candidate_status_for(candidate);
                let (score, reason) = self.priority_score_for(candidate, status)?;
                Some(PortPriority {
                    index,
                    name: candidate.name,
                    group: candidate.group,
                    protocol: candidate.protocol,
                    port: candidate.port,
                    risk: candidate.risk,
                    status,
                    score,
                    reason,
                    recommendation: self.candidate_recommendation_for(candidate, status),
                })
            })
            .collect::<Vec<_>>();

        priorities.sort_by(|left, right| {
            right
                .score
                .cmp(&left.score)
                .then(left.index.cmp(&right.index))
        });
        priorities.truncate(limit);
        priorities
    }

    fn review_order_steps(
        &self,
        findings: &[AdvisorFinding],
        stats: FirewallStats,
        plan_commands: usize,
    ) -> Vec<ReviewOrderStep> {
        let advisor_summary = AdvisorSeveritySummary::from_findings(findings);
        let mut steps = Vec::new();

        if advisor_summary.critical > 0 {
            steps.push(ReviewOrderStep {
                title: "Resolve critical advisor findings".into(),
                detail: format!(
                    "{} critical finding(s) can expose traffic or create conflicting rules.",
                    advisor_summary.critical
                ),
            });
        } else if advisor_summary.warning > 0 {
            steps.push(ReviewOrderStep {
                title: "Review advisor warnings".into(),
                detail: format!(
                    "{} warning finding(s) remain before any future live executor is added.",
                    advisor_summary.warning
                ),
            });
        } else {
            steps.push(ReviewOrderStep {
                title: "Confirm advisor clean state".into(),
                detail: "No critical or warning advisor findings are present.".into(),
            });
        }

        if stats.at_risk_open > 0 {
            steps.push(ReviewOrderStep {
                title: "Close at-risk open known ports".into(),
                detail: format!(
                    "{} admin/high/data known port(s) are still open in the dry-run state.",
                    stats.at_risk_open
                ),
            });
        } else {
            steps.push(ReviewOrderStep {
                title: "Confirm at-risk coverage".into(),
                detail: "No admin/high/data known ports remain open.".into(),
            });
        }

        if stats.unreviewed_at_risk_allowed > 0 {
            steps.push(ReviewOrderStep {
                title: "Document risky allow exceptions".into(),
                detail: format!(
                    "{} admin/high/data allow rule(s) need a reviewed exception or a block.",
                    stats.unreviewed_at_risk_allowed
                ),
            });
        } else if stats.reviewed_exceptions > 0 {
            steps.push(ReviewOrderStep {
                title: "Validate reviewed exceptions".into(),
                detail: format!(
                    "{} reviewed exception(s) still require owner, source scope, and expiry validation.",
                    stats.reviewed_exceptions
                ),
            });
        }

        if stats.allow_rules > 0 {
            steps.push(ReviewOrderStep {
                title: "Validate explicit allow rules".into(),
                detail: format!(
                    "{} allow rule(s) should match intentional service exposure.",
                    stats.allow_rules
                ),
            });
        }

        steps.push(ReviewOrderStep {
            title: "Review command preview".into(),
            detail: format!(
                "{} firewall command(s) are generated for the {} backend preview.",
                plan_commands,
                self.backend()
            ),
        });

        steps.push(ReviewOrderStep {
            title: "Record review fingerprint".into(),
            detail: format!(
                "Use fingerprint {} to compare report, manifest, and script exports.",
                self.plan_fingerprint()
            ),
        });

        steps
    }

    pub fn plan(&self) -> Vec<PlannedCommand> {
        let mut plan = self.base_policy_plan();

        if self.logging {
            plan.extend(self.logging_plan());
        }

        for rule in self.rules.iter().filter(|rule| rule.enabled) {
            plan.extend(self.rule_plan(rule));
        }

        plan
    }

    pub fn plan_for_backend(&self, backend: Backend) -> Vec<PlannedCommand> {
        let mut controller = self.clone();
        controller.backend = backend;
        controller.plan()
    }

    pub fn backend_plan_summaries(&self) -> Vec<BackendPlanSummary> {
        Backend::all()
            .into_iter()
            .map(|backend| {
                let plan = self.plan_for_backend(backend);
                let first_command = plan
                    .first()
                    .map(PlannedCommand::shell_line)
                    .unwrap_or_else(|| "no commands".into());
                BackendPlanSummary {
                    backend,
                    commands: plan.len(),
                    first_command,
                }
            })
            .collect()
    }

    pub fn profile_impact_summaries(&self) -> Vec<ProfileImpactSummary> {
        FirewallProfile::all()
            .into_iter()
            .map(|profile| {
                let mut controller = self.clone();
                let staged_changes = controller.apply_profile(profile);
                let stats = controller.stats();
                let management_access = controller.management_access_summary();
                ProfileImpactSummary {
                    profile,
                    staged_changes,
                    resulting_rules: stats.total_rules,
                    coverage_percent: stats.coverage_percent,
                    at_risk_open: stats.at_risk_open,
                    management_total: management_access.total,
                    management_blocked: management_access.blocked,
                    management_allowed: management_access.allowed,
                    management_open: management_access.open,
                    management_all_blocked: management_access.all_blocked(),
                }
            })
            .collect()
    }

    pub fn recommended_profile(&self) -> ProfileRecommendation {
        let mut impacts = self.profile_impact_summaries();
        impacts.sort_by(|left, right| {
            left.at_risk_open
                .cmp(&right.at_risk_open)
                .then(right.coverage_percent.cmp(&left.coverage_percent))
                .then(left.staged_changes.cmp(&right.staged_changes))
                .then(left.resulting_rules.cmp(&right.resulting_rules))
        });

        let impact = impacts
            .first()
            .copied()
            .expect("built-in profile set is not empty");
        self.profile_recommendation_from_impact(impact)
    }

    pub fn safe_recommended_profile(&self) -> Option<ProfileRecommendation> {
        let mut recommendations = self
            .profile_impact_summaries()
            .into_iter()
            .map(|impact| self.profile_recommendation_from_impact(impact))
            .filter(|recommendation| !recommendation.management_all_blocked)
            .collect::<Vec<_>>();

        recommendations.sort_by(|left, right| {
            left.at_risk_open
                .cmp(&right.at_risk_open)
                .then(right.coverage_percent.cmp(&left.coverage_percent))
                .then(left.staged_changes.cmp(&right.staged_changes))
                .then(left.resulting_rules.cmp(&right.resulting_rules))
        });

        recommendations.into_iter().next()
    }

    fn profile_recommendation_from_impact(
        &self,
        impact: ProfileImpactSummary,
    ) -> ProfileRecommendation {
        let score = profile_recommendation_score(impact);
        let reason = if impact.at_risk_open == 0 {
            format!(
                "eliminates at-risk open ports with {} staged change(s) and {}% coverage",
                impact.staged_changes, impact.coverage_percent
            )
        } else {
            format!(
                "best available profile leaves {} at-risk open port(s) with {}% coverage",
                impact.at_risk_open, impact.coverage_percent
            )
        };
        let warning = impact.management_all_blocked.then(|| {
            format!(
                "{} blocks all {} known management/control-plane access path(s); confirm console or break-glass access before use",
                impact.profile.label(),
                impact.management_total
            )
        });

        ProfileRecommendation {
            profile: impact.profile,
            staged_changes: impact.staged_changes,
            resulting_rules: impact.resulting_rules,
            coverage_percent: impact.coverage_percent,
            at_risk_open: impact.at_risk_open,
            management_blocked: impact.management_blocked,
            management_allowed: impact.management_allowed,
            management_open: impact.management_open,
            management_all_blocked: impact.management_all_blocked,
            score,
            reason,
            warning,
        }
    }

    pub fn profile_change_preview(&self, profile: FirewallProfile) -> Vec<ProfileChangePreview> {
        let mut controller = self.clone();
        controller.apply_profile(profile);

        self.candidates
            .iter()
            .enumerate()
            .filter_map(|(index, candidate)| {
                let before = self.candidate_status_for(candidate);
                let after = controller
                    .candidate_status(index)
                    .expect("candidate index from current catalog exists in clone");

                (before != after).then(|| ProfileChangePreview {
                    profile,
                    candidate_index: index,
                    name: candidate.name,
                    group: candidate.group,
                    protocol: candidate.protocol,
                    port: candidate.port,
                    risk: candidate.risk,
                    before,
                    after,
                    reason: profile_change_reason(profile, candidate, after),
                })
            })
            .collect()
    }

    pub fn recommended_profile_changes(&self) -> Vec<ProfileChangePreview> {
        self.profile_change_preview(self.recommended_profile().profile)
    }

    pub fn safe_recommended_profile_changes(&self) -> Vec<ProfileChangePreview> {
        self.safe_recommended_profile()
            .map(|recommendation| self.profile_change_preview(recommendation.profile))
            .unwrap_or_default()
    }

    pub fn plan_fingerprint(&self) -> String {
        let mut hash = FNV_OFFSET;

        fingerprint_write(&mut hash, "kwall-review-v1");
        fingerprint_write(&mut hash, &self.mode().to_string());
        fingerprint_write(&mut hash, &self.backend().to_string());
        fingerprint_write(&mut hash, self.default_incoming().label());
        fingerprint_write(&mut hash, self.default_outgoing().label());
        fingerprint_write(
            &mut hash,
            if self.logging() {
                "logging-on"
            } else {
                "logging-off"
            },
        );

        for rule in &self.rules {
            fingerprint_write(&mut hash, &rule.id.to_string());
            fingerprint_write(&mut hash, if rule.enabled { "on" } else { "off" });
            fingerprint_write(&mut hash, rule.action.label());
            fingerprint_write(&mut hash, rule.direction.label());
            fingerprint_write(&mut hash, rule.protocol.label());
            fingerprint_write(&mut hash, &rule.port.to_string());
            fingerprint_write(&mut hash, &rule.note);
        }

        for exception in &self.review_exceptions {
            fingerprint_write(&mut hash, exception.name);
            fingerprint_write(&mut hash, exception.group);
            fingerprint_write(&mut hash, exception.protocol.label());
            fingerprint_write(&mut hash, &exception.port.to_string());
            fingerprint_write(&mut hash, exception.risk);
            fingerprint_write(&mut hash, exception.note);
        }

        for command in self.plan() {
            fingerprint_write(&mut hash, &command.backend.to_string());
            fingerprint_write(&mut hash, command.program);
            for arg in &command.args {
                fingerprint_write(&mut hash, arg);
            }
            fingerprint_write(&mut hash, &command.reason);
        }

        format!("{hash:016x}")
    }

    pub fn dry_run_report(&self) -> String {
        let stats = self.stats();
        let findings = self.advisor_findings();
        let advisor_summary = AdvisorSeveritySummary::from_findings(&findings);
        let advisor_next = AdvisorNextAction::from_findings(&findings);
        let plan = self.plan();
        let review_order = self.review_order_steps(&findings, stats, plan.len());
        let review_gates = self.review_gates();
        let review_gate_summary = ReviewGateSummary::from_gates(&review_gates);
        let priority_port_queue = self.priority_ports(5);
        let backend_summaries = self.backend_plan_summaries();
        let profile_impacts = self.profile_impact_summaries();
        let recommended_profile = self.recommended_profile();
        let recommended_profile_changes = self.recommended_profile_changes();
        let safe_recommended_profile = self.safe_recommended_profile();
        let safe_recommended_profile_changes = self.safe_recommended_profile_changes();
        let management_access = self.management_access_summary();
        let group_coverage = self.group_coverage_summaries();
        let risk_coverage = self.risk_coverage_summaries();
        let at_risk_open_ports = self
            .candidates
            .iter()
            .filter(|candidate| {
                matches!(candidate.risk, "high" | "data" | "admin")
                    && self.candidate_status_for(candidate) == CandidateStatus::Open
            })
            .collect::<Vec<_>>();
        let at_risk_allowed_ports = self
            .candidates
            .iter()
            .filter(|candidate| {
                matches!(candidate.risk, "high" | "data" | "admin")
                    && self.candidate_status_for(candidate) == CandidateStatus::Allowed
            })
            .collect::<Vec<_>>();
        let mut report = String::new();

        let _ = writeln!(report, "# Kwall Dry-Run Report");
        let _ = writeln!(report);
        let _ = writeln!(report, "Review-only artifact. No commands were executed.");
        let _ = writeln!(report);

        let _ = writeln!(report, "## Summary");
        let _ = writeln!(report, "- mode: {}", self.mode());
        let _ = writeln!(report, "- backend preview: {}", self.backend());
        let _ = writeln!(
            report,
            "- defaults: incoming {}, outgoing {}",
            self.default_incoming().label(),
            self.default_outgoing().label()
        );
        let _ = writeln!(
            report,
            "- logging: {}",
            if self.logging() {
                "enabled"
            } else {
                "disabled"
            }
        );
        let _ = writeln!(report, "- staged rules: {}", stats.total_rules);
        let _ = writeln!(report, "- active rules: {}", stats.enabled_rules);
        let _ = writeln!(report, "- planned commands: {}", stats.plan_commands);
        let _ = writeln!(
            report,
            "- advisor findings: {} total, {} critical, {} warning, {} info, {} good",
            advisor_summary.total(),
            advisor_summary.critical,
            advisor_summary.warning,
            advisor_summary.info,
            advisor_summary.good
        );
        let _ = writeln!(
            report,
            "- review gates: {} pass, {} warn, {} block",
            review_gate_summary.pass, review_gate_summary.warn, review_gate_summary.block
        );
        let _ = writeln!(
            report,
            "- next advisor action: [{}] {} - {}",
            advisor_next.severity.label(),
            advisor_next.title,
            advisor_next.recommendation
        );
        let _ = writeln!(
            report,
            "- known ports: {} total, {} blocked, {} allowed, {} open, {} at-risk open",
            stats.known_ports,
            stats.known_blocked,
            stats.known_allowed,
            stats.known_open,
            stats.at_risk_open
        );
        let _ = writeln!(
            report,
            "- risky allow exceptions: {} reviewed, {} unreviewed",
            stats.reviewed_exceptions, stats.unreviewed_at_risk_allowed
        );
        let _ = writeln!(
            report,
            "- management access: {} blocked, {} allowed, {} open",
            management_access.blocked, management_access.allowed, management_access.open
        );
        let _ = writeln!(
            report,
            "- recommended profile: {} (score {}, +{} changes, {}% coverage, {} at-risk open)",
            recommended_profile.profile.label(),
            recommended_profile.score,
            recommended_profile.staged_changes,
            recommended_profile.coverage_percent,
            recommended_profile.at_risk_open
        );
        if let Some(warning) = &recommended_profile.warning {
            let _ = writeln!(report, "- recommended profile warning: {warning}");
        }
        if let Some(recommendation) = &safe_recommended_profile {
            let _ = writeln!(
                report,
                "- management-preserving profile: {} (score {}, +{} changes, {}% coverage, {} at-risk open)",
                recommendation.profile.label(),
                recommendation.score,
                recommendation.staged_changes,
                recommendation.coverage_percent,
                recommendation.at_risk_open
            );
        } else {
            let _ = writeln!(
                report,
                "- management-preserving profile: none available without keeping all management paths blocked"
            );
        }
        let _ = writeln!(report, "- review fingerprint: {}", self.plan_fingerprint());
        let _ = writeln!(
            report,
            "- high-risk known-port coverage: {}%",
            stats.coverage_percent
        );
        let _ = writeln!(
            report,
            "- review readiness: {}% ({} / {})",
            stats.risk_percent,
            stats.readiness_grade(),
            stats.readiness_label()
        );
        let _ = writeln!(report);

        let _ = writeln!(report, "## Suggested Review Order");
        for (index, step) in review_order.iter().enumerate() {
            let _ = writeln!(report, "{}. {} - {}", index + 1, step.title, step.detail);
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Review Gates");
        for gate in &review_gates {
            let _ = writeln!(report, "- [{}] {}", gate.status.label(), gate.title);
            let _ = writeln!(report, "  detail: {}", gate.detail);
            let _ = writeln!(report, "  action: {}", gate.action);
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Priority Port Queue");
        if priority_port_queue.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for (index, priority) in priority_port_queue.iter().enumerate() {
                let _ = writeln!(
                    report,
                    "{}. [{}] {} {}/{} {}/{} - {} | score {} | reason: {}",
                    index + 1,
                    priority.status.label(),
                    priority.name,
                    priority.protocol.label(),
                    priority.port,
                    priority.group,
                    priority.risk,
                    priority.recommendation,
                    priority.score,
                    priority.reason
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Reviewed Exceptions");
        if self.review_exceptions().is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for exception in self.review_exceptions() {
                let _ = writeln!(
                    report,
                    "- {} {}/{} {}/{} - {}",
                    exception.name,
                    exception.protocol.label(),
                    exception.port,
                    exception.group,
                    exception.risk,
                    exception.note
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Staged Rules");
        if self.rules.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for rule in &self.rules {
                let _ = writeln!(
                    report,
                    "- #{:02} [{}] {} {} {} {} - {}",
                    rule.id,
                    if rule.enabled { "on" } else { "off" },
                    rule.action.label(),
                    rule.direction.label(),
                    rule.protocol,
                    rule.port,
                    rule.note
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Advisor Findings");
        for finding in findings {
            let _ = writeln!(report, "- [{}] {}", finding.severity.label(), finding.title);
            let _ = writeln!(report, "  detail: {}", finding.detail);
            let _ = writeln!(report, "  recommendation: {}", finding.recommendation);
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Management Access Guard");
        let _ = writeln!(
            report,
            "- {} management/control-plane path(s): {} blocked, {} allowed, {} open",
            management_access.total,
            management_access.blocked,
            management_access.allowed,
            management_access.open
        );
        let _ = writeln!(
            report,
            "- action: {}",
            if management_access.all_blocked() {
                "confirm local console, break-glass, or out-of-band access"
            } else {
                "review exposed management paths separately"
            }
        );
        for candidate in self
            .candidates
            .iter()
            .filter(|candidate| matches!(candidate.name, "SSH" | "RDP" | "VNC" | "Kubernetes"))
        {
            let status = self.candidate_status_for(candidate);
            let _ = writeln!(
                report,
                "- [{}] {} {}/{} {}/{} - {}",
                status.label(),
                candidate.name,
                candidate.protocol.label(),
                candidate.port,
                candidate.group,
                candidate.risk,
                candidate.detail
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Known Port Groups");
        for group in group_coverage {
            let _ = writeln!(
                report,
                "- {}: {} total, {} blocked, {} allowed, {} open, {} at-risk open",
                group.group,
                group.total,
                group.blocked,
                group.allowed,
                group.open,
                group.at_risk_open
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Known Port Risks");
        for risk in risk_coverage {
            let _ = writeln!(
                report,
                "- {}: {} total, {} blocked, {} allowed, {} open",
                risk.risk, risk.total, risk.blocked, risk.allowed, risk.open
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## At-Risk Open Ports");
        if at_risk_open_ports.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for candidate in at_risk_open_ports {
                let _ = writeln!(
                    report,
                    "- {} {}/{} {}/{} - {} | action: {}",
                    candidate.name,
                    candidate.protocol.label(),
                    candidate.port,
                    candidate.group,
                    candidate.risk,
                    candidate.detail,
                    self.candidate_recommendation_for(candidate, CandidateStatus::Open)
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## At-Risk Allowed Ports");
        if at_risk_allowed_ports.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for candidate in at_risk_allowed_ports {
                let _ = writeln!(
                    report,
                    "- {} {}/{} {}/{} - {} | action: {}",
                    candidate.name,
                    candidate.protocol.label(),
                    candidate.port,
                    candidate.group,
                    candidate.risk,
                    candidate.detail,
                    self.candidate_recommendation_for(candidate, CandidateStatus::Allowed)
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Known Port Catalog");
        for candidate in &self.candidates {
            let status = self.candidate_status_for(candidate);
            let _ = writeln!(
                report,
                "- [{}] {} {}/{} {}/{} aliases {} - {} | action: {}",
                status.label(),
                candidate.name,
                candidate.protocol.label(),
                candidate.port,
                candidate.group,
                candidate.risk,
                candidate.aliases.join(", "),
                candidate.detail,
                self.candidate_recommendation_for(candidate, status)
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Command Preview");
        for (index, command) in plan.iter().enumerate() {
            let _ = writeln!(report, "{}. {}", index + 1, command.shell_line());
            let _ = writeln!(report, "   reason: {}", command.reason);
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Backend Comparison");
        for summary in backend_summaries {
            let _ = writeln!(
                report,
                "- {}: {} command(s), first: {}",
                summary.backend, summary.commands, summary.first_command
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Recommended Profile");
        let _ = writeln!(
            report,
            "- strongest hardening: {}: score {}, +{} staged change(s), {} resulting rule(s), {}% coverage, {} at-risk open",
            recommended_profile.profile.label(),
            recommended_profile.score,
            recommended_profile.staged_changes,
            recommended_profile.resulting_rules,
            recommended_profile.coverage_percent,
            recommended_profile.at_risk_open
        );
        let _ = writeln!(report, "  reason: {}", recommended_profile.reason);
        if let Some(warning) = &recommended_profile.warning {
            let _ = writeln!(report, "  warning: {warning}");
        }
        let _ = writeln!(
            report,
            "  management access after profile: {} blocked, {} allowed, {} open",
            recommended_profile.management_blocked,
            recommended_profile.management_allowed,
            recommended_profile.management_open
        );
        if let Some(recommendation) = &safe_recommended_profile {
            let _ = writeln!(
                report,
                "- management-preserving alternative: {}: score {}, +{} staged change(s), {} resulting rule(s), {}% coverage, {} at-risk open",
                recommendation.profile.label(),
                recommendation.score,
                recommendation.staged_changes,
                recommendation.resulting_rules,
                recommendation.coverage_percent,
                recommendation.at_risk_open
            );
            let _ = writeln!(report, "  reason: {}", recommendation.reason);
            if let Some(warning) = &recommendation.warning {
                let _ = writeln!(report, "  warning: {warning}");
            }
            let _ = writeln!(
                report,
                "  management access after profile: {} blocked, {} allowed, {} open",
                recommendation.management_blocked,
                recommendation.management_allowed,
                recommendation.management_open
            );
        } else {
            let _ = writeln!(
                report,
                "- management-preserving alternative: none available from built-in profiles"
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Recommended Profile Changes");
        if recommended_profile_changes.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for change in &recommended_profile_changes {
                let _ = writeln!(
                    report,
                    "- {} {}/{} {}/{}: {} -> {} | {}",
                    change.name,
                    change.protocol.label(),
                    change.port,
                    change.group,
                    change.risk,
                    change.before.label(),
                    change.after.label(),
                    change.reason
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Management-Preserving Profile Changes");
        if safe_recommended_profile_changes.is_empty() {
            let _ = writeln!(report, "- none");
        } else {
            for change in &safe_recommended_profile_changes {
                let _ = writeln!(
                    report,
                    "- {} {}/{} {}/{}: {} -> {} | {}",
                    change.name,
                    change.protocol.label(),
                    change.port,
                    change.group,
                    change.risk,
                    change.before.label(),
                    change.after.label(),
                    change.reason
                );
            }
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Profile Impact Preview");
        for impact in profile_impacts {
            let _ = writeln!(
                report,
                "- {}: {} staged change(s), {} resulting rule(s), {}% coverage, {} at-risk open, management {} blocked/{} allowed/{} open{}",
                impact.profile.label(),
                impact.staged_changes,
                impact.resulting_rules,
                impact.coverage_percent,
                impact.at_risk_open,
                impact.management_blocked,
                impact.management_allowed,
                impact.management_open,
                if impact.management_all_blocked {
                    " (full lockout)"
                } else {
                    ""
                }
            );
        }
        let _ = writeln!(report);

        let _ = writeln!(report, "## Recent Activity");
        for message in self.audit_log.iter().rev().take(12).rev() {
            let _ = writeln!(report, "- {message}");
        }

        report
    }

    pub fn dry_run_script(&self) -> String {
        let plan = self.plan();
        let mut script = String::new();

        let _ = writeln!(script, "#!/bin/sh");
        let _ = writeln!(script, "set -eu");
        let _ = writeln!(script);
        let _ = writeln!(script, "cat <<'KNOTT_FIREWALL_NOTICE'");
        let _ = writeln!(script, "Kwall dry-run preview only.");
        let _ = writeln!(script, "No firewall commands are executed by this file.");
        let _ = writeln!(
            script,
            "Review the commented commands below before any manual use."
        );
        let _ = writeln!(script, "KNOTT_FIREWALL_NOTICE");
        let _ = writeln!(script, "exit 0");
        let _ = writeln!(script);
        let _ = writeln!(script, "# Backend preview: {}", self.backend());
        let _ = writeln!(script, "# Mode: {}", self.mode());
        let _ = writeln!(script, "# Review fingerprint: {}", self.plan_fingerprint());
        let _ = writeln!(
            script,
            "# Defaults: incoming {}, outgoing {}, logging {}",
            self.default_incoming().label(),
            self.default_outgoing().label(),
            if self.logging() { "on" } else { "off" }
        );
        let _ = writeln!(script);
        let _ = writeln!(script, "# Command preview. Lines below are comments.");
        for (index, command) in plan.iter().enumerate() {
            let _ = writeln!(script, "# {:02}. {}", index + 1, command.reason);
            let _ = writeln!(script, "# {}", command.shell_line());
        }

        script
    }

    pub fn dry_run_manifest(&self) -> String {
        let stats = self.stats();
        let findings = self.advisor_findings();
        let advisor_summary = AdvisorSeveritySummary::from_findings(&findings);
        let advisor_next = AdvisorNextAction::from_findings(&findings);
        let plan = self.plan();
        let review_order = self.review_order_steps(&findings, stats, plan.len());
        let review_gates = self.review_gates();
        let review_gate_summary = ReviewGateSummary::from_gates(&review_gates);
        let priority_port_queue = self.priority_ports(5);
        let backend_summaries = self.backend_plan_summaries();
        let profile_impacts = self.profile_impact_summaries();
        let recommended_profile = self.recommended_profile();
        let recommended_profile_changes = self.recommended_profile_changes();
        let safe_recommended_profile = self.safe_recommended_profile();
        let safe_recommended_profile_changes = self.safe_recommended_profile_changes();
        let management_access = self.management_access_summary();
        let group_coverage = self.group_coverage_summaries();
        let risk_coverage = self.risk_coverage_summaries();
        let at_risk_open_ports = self
            .candidates
            .iter()
            .filter(|candidate| {
                matches!(candidate.risk, "high" | "data" | "admin")
                    && self.candidate_status_for(candidate) == CandidateStatus::Open
            })
            .collect::<Vec<_>>();
        let at_risk_allowed_ports = self
            .candidates
            .iter()
            .filter(|candidate| {
                matches!(candidate.risk, "high" | "data" | "admin")
                    && self.candidate_status_for(candidate) == CandidateStatus::Allowed
            })
            .collect::<Vec<_>>();
        let mut manifest = String::new();

        let _ = writeln!(manifest, "{{");
        let _ = writeln!(
            manifest,
            "  \"schema\": {},",
            json_string("knott.firewall.dry-run.v1")
        );
        let _ = writeln!(manifest, "  \"review_only\": true,");
        let _ = writeln!(manifest, "  \"executes_commands\": false,");
        let _ = writeln!(
            manifest,
            "  \"fingerprint\": {},",
            json_string(&self.plan_fingerprint())
        );
        let _ = writeln!(
            manifest,
            "  \"mode\": {},",
            json_string(&self.mode().to_string())
        );
        let _ = writeln!(
            manifest,
            "  \"backend\": {},",
            json_string(&self.backend().to_string())
        );
        let _ = writeln!(manifest, "  \"defaults\": {{");
        let _ = writeln!(
            manifest,
            "    \"incoming\": {},",
            json_string(self.default_incoming().label())
        );
        let _ = writeln!(
            manifest,
            "    \"outgoing\": {}",
            json_string(self.default_outgoing().label())
        );
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"logging\": {},", self.logging());
        let _ = writeln!(manifest, "  \"stats\": {{");
        let _ = writeln!(manifest, "    \"total_rules\": {},", stats.total_rules);
        let _ = writeln!(manifest, "    \"enabled_rules\": {},", stats.enabled_rules);
        let _ = writeln!(manifest, "    \"allow_rules\": {},", stats.allow_rules);
        let _ = writeln!(manifest, "    \"deny_rules\": {},", stats.deny_rules);
        let _ = writeln!(manifest, "    \"reject_rules\": {},", stats.reject_rules);
        let _ = writeln!(manifest, "    \"inbound_rules\": {},", stats.inbound_rules);
        let _ = writeln!(
            manifest,
            "    \"outbound_rules\": {},",
            stats.outbound_rules
        );
        let _ = writeln!(
            manifest,
            "    \"high_risk_blocks\": {},",
            stats.high_risk_blocks
        );
        let _ = writeln!(manifest, "    \"known_ports\": {},", stats.known_ports);
        let _ = writeln!(manifest, "    \"known_blocked\": {},", stats.known_blocked);
        let _ = writeln!(manifest, "    \"known_allowed\": {},", stats.known_allowed);
        let _ = writeln!(manifest, "    \"known_open\": {},", stats.known_open);
        let _ = writeln!(manifest, "    \"at_risk_open\": {},", stats.at_risk_open);
        let _ = writeln!(
            manifest,
            "    \"at_risk_allowed\": {},",
            stats.at_risk_allowed
        );
        let _ = writeln!(
            manifest,
            "    \"reviewed_exceptions\": {},",
            stats.reviewed_exceptions
        );
        let _ = writeln!(
            manifest,
            "    \"unreviewed_at_risk_allowed\": {},",
            stats.unreviewed_at_risk_allowed
        );
        let _ = writeln!(manifest, "    \"plan_commands\": {},", stats.plan_commands);
        let _ = writeln!(
            manifest,
            "    \"coverage_percent\": {},",
            stats.coverage_percent
        );
        let _ = writeln!(manifest, "    \"risk_percent\": {},", stats.risk_percent);
        let _ = writeln!(
            manifest,
            "    \"readiness_grade\": {},",
            json_string(stats.readiness_grade())
        );
        let _ = writeln!(
            manifest,
            "    \"readiness_label\": {}",
            json_string(stats.readiness_label())
        );
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"advisor_next_action\": {{");
        let _ = writeln!(
            manifest,
            "    \"severity\": {},",
            json_string(advisor_next.severity.label())
        );
        let _ = writeln!(
            manifest,
            "    \"title\": {},",
            json_string(&advisor_next.title)
        );
        let _ = writeln!(
            manifest,
            "    \"recommendation\": {}",
            json_string(&advisor_next.recommendation)
        );
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"advisor_summary\": {{");
        let _ = writeln!(manifest, "    \"total\": {},", advisor_summary.total());
        let _ = writeln!(manifest, "    \"critical\": {},", advisor_summary.critical);
        let _ = writeln!(manifest, "    \"warning\": {},", advisor_summary.warning);
        let _ = writeln!(manifest, "    \"info\": {},", advisor_summary.info);
        let _ = writeln!(manifest, "    \"good\": {}", advisor_summary.good);
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"management_access\": {{");
        let _ = writeln!(manifest, "    \"total\": {},", management_access.total);
        let _ = writeln!(manifest, "    \"blocked\": {},", management_access.blocked);
        let _ = writeln!(manifest, "    \"allowed\": {},", management_access.allowed);
        let _ = writeln!(manifest, "    \"open\": {},", management_access.open);
        let _ = writeln!(
            manifest,
            "    \"all_blocked\": {},",
            management_access.all_blocked()
        );
        let _ = writeln!(manifest, "    \"candidates\": [");
        let management_candidates = self
            .candidates
            .iter()
            .filter(|candidate| matches!(candidate.name, "SSH" | "RDP" | "VNC" | "Kubernetes"))
            .collect::<Vec<_>>();
        for (index, candidate) in management_candidates.iter().enumerate() {
            let comma = if index + 1 == management_candidates.len() {
                ""
            } else {
                ","
            };
            let status = self.candidate_status_for(candidate);
            let _ = writeln!(
                manifest,
                "      {{\"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"status\": {}}}{}",
                json_string(candidate.name),
                json_string(candidate.group),
                json_string(candidate.risk),
                json_string(candidate.protocol.label()),
                json_string(&candidate.port.to_string()),
                json_string(status.label()),
                comma
            );
        }
        let _ = writeln!(manifest, "    ]");
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"review_gate_summary\": {{");
        let _ = writeln!(manifest, "    \"pass\": {},", review_gate_summary.pass);
        let _ = writeln!(manifest, "    \"warn\": {},", review_gate_summary.warn);
        let _ = writeln!(manifest, "    \"block\": {}", review_gate_summary.block);
        let _ = writeln!(manifest, "  }},");
        let _ = writeln!(manifest, "  \"review_order\": [");
        for (index, step) in review_order.iter().enumerate() {
            let comma = if index + 1 == review_order.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"title\": {}, \"detail\": {}}}{}",
                index + 1,
                json_string(&step.title),
                json_string(&step.detail),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"review_gates\": [");
        for (index, gate) in review_gates.iter().enumerate() {
            let comma = if index + 1 == review_gates.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"status\": {}, \"title\": {}, \"detail\": {}, \"action\": {}}}{}",
                index + 1,
                json_string(gate.status.label()),
                json_string(&gate.title),
                json_string(&gate.detail),
                json_string(&gate.action),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"priority_port_queue\": [");
        for (index, priority) in priority_port_queue.iter().enumerate() {
            let comma = if index + 1 == priority_port_queue.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"status\": {}, \"candidate_index\": {}, \"score\": {}, \"reason\": {}, \"recommendation\": {}}}{}",
                index + 1,
                json_string(priority.name),
                json_string(priority.group),
                json_string(priority.risk),
                json_string(priority.protocol.label()),
                json_string(&priority.port.to_string()),
                json_string(priority.status.label()),
                priority.index,
                priority.score,
                json_string(&priority.reason),
                json_string(priority.recommendation),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"reviewed_exceptions\": [");
        for (index, exception) in self.review_exceptions().iter().enumerate() {
            let comma = if index + 1 == self.review_exceptions().len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"note\": {}}}{}",
                json_string(exception.name),
                json_string(exception.group),
                json_string(exception.risk),
                json_string(exception.protocol.label()),
                json_string(&exception.port.to_string()),
                json_string(exception.note),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"rules\": [");
        for (index, rule) in self.rules.iter().enumerate() {
            let comma = if index + 1 == self.rules.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"id\": {}, \"enabled\": {}, \"action\": {}, \"direction\": {}, \"protocol\": {}, \"port\": {}, \"note\": {}}}{}",
                rule.id,
                rule.enabled,
                json_string(rule.action.label()),
                json_string(rule.direction.label()),
                json_string(rule.protocol.label()),
                json_string(&rule.port.to_string()),
                json_string(&rule.note),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"known_port_catalog\": [");
        for (index, candidate) in self.candidates.iter().enumerate() {
            let comma = if index + 1 == self.candidates.len() {
                ""
            } else {
                ","
            };
            let status = self.candidate_status_for(candidate);
            let _ = writeln!(
                manifest,
                "    {{\"name\": {}, \"group\": {}, \"risk\": {}, \"aliases\": [{}], \"protocol\": {}, \"port\": {}, \"status\": {}, \"review_exception\": {}, \"detail\": {}, \"recommendation\": {}}}{}",
                json_string(candidate.name),
                json_string(candidate.group),
                json_string(candidate.risk),
                json_array(candidate.aliases),
                json_string(candidate.protocol.label()),
                json_string(&candidate.port.to_string()),
                json_string(status.label()),
                self.has_review_exception(candidate),
                json_string(candidate.detail),
                json_string(self.candidate_recommendation_for(candidate, status)),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"at_risk_open_ports\": [");
        for (index, candidate) in at_risk_open_ports.iter().enumerate() {
            let comma = if index + 1 == at_risk_open_ports.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"status\": {}, \"detail\": {}, \"recommendation\": {}}}{}",
                json_string(candidate.name),
                json_string(candidate.group),
                json_string(candidate.risk),
                json_string(candidate.protocol.label()),
                json_string(&candidate.port.to_string()),
                json_string(CandidateStatus::Open.label()),
                json_string(candidate.detail),
                json_string(self.candidate_recommendation_for(candidate, CandidateStatus::Open)),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"at_risk_allowed_ports\": [");
        for (index, candidate) in at_risk_allowed_ports.iter().enumerate() {
            let comma = if index + 1 == at_risk_allowed_ports.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"status\": {}, \"review_exception\": {}, \"detail\": {}, \"recommendation\": {}}}{}",
                json_string(candidate.name),
                json_string(candidate.group),
                json_string(candidate.risk),
                json_string(candidate.protocol.label()),
                json_string(&candidate.port.to_string()),
                json_string(CandidateStatus::Allowed.label()),
                self.has_review_exception(candidate),
                json_string(candidate.detail),
                json_string(self.candidate_recommendation_for(candidate, CandidateStatus::Allowed)),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"group_coverage\": [");
        for (index, group) in group_coverage.iter().enumerate() {
            let comma = if index + 1 == group_coverage.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"group\": {}, \"total\": {}, \"blocked\": {}, \"allowed\": {}, \"open\": {}, \"at_risk_open\": {}}}{}",
                json_string(group.group),
                group.total,
                group.blocked,
                group.allowed,
                group.open,
                group.at_risk_open,
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"risk_coverage\": [");
        for (index, risk) in risk_coverage.iter().enumerate() {
            let comma = if index + 1 == risk_coverage.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"risk\": {}, \"total\": {}, \"blocked\": {}, \"allowed\": {}, \"open\": {}}}{}",
                json_string(risk.risk),
                risk.total,
                risk.blocked,
                risk.allowed,
                risk.open,
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"backend_comparison\": [");
        for (index, summary) in backend_summaries.iter().enumerate() {
            let comma = if index + 1 == backend_summaries.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"backend\": {}, \"commands\": {}, \"first_command\": {}}}{}",
                json_string(&summary.backend.to_string()),
                summary.commands,
                json_string(&summary.first_command),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"recommended_profile\": {{");
        let _ = writeln!(
            manifest,
            "    \"profile\": {},",
            json_string(recommended_profile.profile.label())
        );
        let _ = writeln!(manifest, "    \"score\": {},", recommended_profile.score);
        let _ = writeln!(
            manifest,
            "    \"staged_changes\": {},",
            recommended_profile.staged_changes
        );
        let _ = writeln!(
            manifest,
            "    \"resulting_rules\": {},",
            recommended_profile.resulting_rules
        );
        let _ = writeln!(
            manifest,
            "    \"coverage_percent\": {},",
            recommended_profile.coverage_percent
        );
        let _ = writeln!(
            manifest,
            "    \"at_risk_open\": {},",
            recommended_profile.at_risk_open
        );
        let _ = writeln!(
            manifest,
            "    \"management_blocked\": {},",
            recommended_profile.management_blocked
        );
        let _ = writeln!(
            manifest,
            "    \"management_allowed\": {},",
            recommended_profile.management_allowed
        );
        let _ = writeln!(
            manifest,
            "    \"management_open\": {},",
            recommended_profile.management_open
        );
        let _ = writeln!(
            manifest,
            "    \"management_all_blocked\": {},",
            recommended_profile.management_all_blocked
        );
        let warning = recommended_profile
            .warning
            .as_deref()
            .map(json_string)
            .unwrap_or_else(|| "null".into());
        let _ = writeln!(manifest, "    \"warning\": {},", warning);
        let _ = writeln!(
            manifest,
            "    \"reason\": {}",
            json_string(&recommended_profile.reason)
        );
        let _ = writeln!(manifest, "  }},");
        if let Some(recommendation) = &safe_recommended_profile {
            let _ = writeln!(manifest, "  \"safe_recommended_profile\": {{");
            let _ = writeln!(
                manifest,
                "    \"profile\": {},",
                json_string(recommendation.profile.label())
            );
            let _ = writeln!(manifest, "    \"score\": {},", recommendation.score);
            let _ = writeln!(
                manifest,
                "    \"staged_changes\": {},",
                recommendation.staged_changes
            );
            let _ = writeln!(
                manifest,
                "    \"resulting_rules\": {},",
                recommendation.resulting_rules
            );
            let _ = writeln!(
                manifest,
                "    \"coverage_percent\": {},",
                recommendation.coverage_percent
            );
            let _ = writeln!(
                manifest,
                "    \"at_risk_open\": {},",
                recommendation.at_risk_open
            );
            let _ = writeln!(
                manifest,
                "    \"management_blocked\": {},",
                recommendation.management_blocked
            );
            let _ = writeln!(
                manifest,
                "    \"management_allowed\": {},",
                recommendation.management_allowed
            );
            let _ = writeln!(
                manifest,
                "    \"management_open\": {},",
                recommendation.management_open
            );
            let _ = writeln!(
                manifest,
                "    \"management_all_blocked\": {},",
                recommendation.management_all_blocked
            );
            let warning = recommendation
                .warning
                .as_deref()
                .map(json_string)
                .unwrap_or_else(|| "null".into());
            let _ = writeln!(manifest, "    \"warning\": {},", warning);
            let _ = writeln!(
                manifest,
                "    \"reason\": {}",
                json_string(&recommendation.reason)
            );
            let _ = writeln!(manifest, "  }},");
        } else {
            let _ = writeln!(manifest, "  \"safe_recommended_profile\": null,");
        }
        let _ = writeln!(manifest, "  \"recommended_profile_changes\": [");
        for (index, change) in recommended_profile_changes.iter().enumerate() {
            let comma = if index + 1 == recommended_profile_changes.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"candidate_index\": {}, \"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"before\": {}, \"after\": {}, \"reason\": {}}}{}",
                index + 1,
                change.candidate_index,
                json_string(change.name),
                json_string(change.group),
                json_string(change.risk),
                json_string(change.protocol.label()),
                json_string(&change.port.to_string()),
                json_string(change.before.label()),
                json_string(change.after.label()),
                json_string(&change.reason),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"safe_recommended_profile_changes\": [");
        for (index, change) in safe_recommended_profile_changes.iter().enumerate() {
            let comma = if index + 1 == safe_recommended_profile_changes.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"candidate_index\": {}, \"name\": {}, \"group\": {}, \"risk\": {}, \"protocol\": {}, \"port\": {}, \"before\": {}, \"after\": {}, \"reason\": {}}}{}",
                index + 1,
                change.candidate_index,
                json_string(change.name),
                json_string(change.group),
                json_string(change.risk),
                json_string(change.protocol.label()),
                json_string(&change.port.to_string()),
                json_string(change.before.label()),
                json_string(change.after.label()),
                json_string(&change.reason),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"profile_impacts\": [");
        for (index, impact) in profile_impacts.iter().enumerate() {
            let comma = if index + 1 == profile_impacts.len() {
                ""
            } else {
                ","
            };
            let _ = writeln!(
                manifest,
                "    {{\"profile\": {}, \"staged_changes\": {}, \"resulting_rules\": {}, \"coverage_percent\": {}, \"at_risk_open\": {}, \"management_total\": {}, \"management_blocked\": {}, \"management_allowed\": {}, \"management_open\": {}, \"management_all_blocked\": {}}}{}",
                json_string(impact.profile.label()),
                impact.staged_changes,
                impact.resulting_rules,
                impact.coverage_percent,
                impact.at_risk_open,
                impact.management_total,
                impact.management_blocked,
                impact.management_allowed,
                impact.management_open,
                impact.management_all_blocked,
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"advisor_findings\": [");
        for (index, finding) in findings.iter().enumerate() {
            let comma = if index + 1 == findings.len() { "" } else { "," };
            let _ = writeln!(
                manifest,
                "    {{\"severity\": {}, \"title\": {}, \"detail\": {}, \"recommendation\": {}}}{}",
                json_string(finding.severity.label()),
                json_string(&finding.title),
                json_string(&finding.detail),
                json_string(&finding.recommendation),
                comma
            );
        }
        let _ = writeln!(manifest, "  ],");
        let _ = writeln!(manifest, "  \"commands\": [");
        for (index, command) in plan.iter().enumerate() {
            let comma = if index + 1 == plan.len() { "" } else { "," };
            let args = command
                .args
                .iter()
                .map(|arg| json_string(arg))
                .collect::<Vec<_>>()
                .join(", ");
            let _ = writeln!(
                manifest,
                "    {{\"index\": {}, \"backend\": {}, \"program\": {}, \"args\": [{}], \"shell\": {}, \"reason\": {}}}{}",
                index + 1,
                json_string(&command.backend.to_string()),
                json_string(command.program),
                args,
                json_string(&command.shell_line()),
                json_string(&command.reason),
                comma
            );
        }
        let _ = writeln!(manifest, "  ]");
        let _ = writeln!(manifest, "}}");

        manifest
    }

    fn stage_rule(
        &mut self,
        action: RuleAction,
        direction: Direction,
        protocol: Protocol,
        port: PortSpec,
        note: &str,
    ) {
        let rule = FirewallRule {
            id: self.next_id,
            enabled: true,
            action,
            direction,
            protocol,
            port,
            note: note.to_string(),
        };
        self.next_id += 1;
        self.audit_log.push(format!(
            "Staged {} {} {} {}.",
            action.label(),
            direction.label(),
            protocol.label(),
            port
        ));
        self.rules.push(rule);
    }

    fn stage_starter_rules(&mut self) {
        self.stage_rule(
            RuleAction::Deny,
            Direction::In,
            Protocol::Tcp,
            PortSpec::Single(23),
            "Block Telnet by default",
        );
        self.stage_rule(
            RuleAction::Deny,
            Direction::In,
            Protocol::Tcp,
            PortSpec::Single(445),
            "Block SMB exposure",
        );
        self.stage_rule(
            RuleAction::Reject,
            Direction::Out,
            Protocol::Tcp,
            PortSpec::Single(25),
            "Reject outbound SMTP by default",
        );
    }

    fn save_checkpoint(&mut self) {
        self.history.push(ControllerSnapshot {
            backend: self.backend,
            default_incoming: self.default_incoming,
            default_outgoing: self.default_outgoing,
            logging: self.logging,
            rules: self.rules.clone(),
            review_exceptions: self.review_exceptions.clone(),
            next_id: self.next_id,
        });
        if self.history.len() > 64 {
            self.history.remove(0);
        }
    }

    fn has_rule(
        &self,
        action: RuleAction,
        direction: Direction,
        protocol: Protocol,
        port: PortSpec,
    ) -> bool {
        self.rules.iter().any(|rule| {
            rule.action == action
                && rule.direction == direction
                && rule.protocol == protocol
                && rule.port == port
        })
    }

    fn candidate_status_for(&self, candidate: &PortCandidate) -> CandidateStatus {
        if self.rules.iter().any(|rule| {
            rule.enabled
                && rule.direction == Direction::In
                && matches!(rule.action, RuleAction::Deny | RuleAction::Reject)
                && rule.protocol == candidate.protocol
                && rule.port == candidate.port
        }) {
            return CandidateStatus::Blocked;
        }

        if self.rules.iter().any(|rule| {
            rule.enabled
                && rule.direction == Direction::In
                && rule.action == RuleAction::Allow
                && rule.protocol == candidate.protocol
                && rule.port == candidate.port
        }) {
            return CandidateStatus::Allowed;
        }

        CandidateStatus::Open
    }

    fn has_review_exception(&self, candidate: &PortCandidate) -> bool {
        self.review_exceptions
            .iter()
            .any(|exception| exception.matches_candidate(candidate))
    }

    fn remove_review_exception_for(&mut self, candidate: &PortCandidate) -> bool {
        let before = self.review_exceptions.len();
        self.review_exceptions
            .retain(|exception| !exception.matches_candidate(candidate));
        before != self.review_exceptions.len()
    }

    fn prune_review_exceptions(&mut self) {
        let allowed_targets = self
            .candidates
            .iter()
            .filter(|candidate| {
                matches!(candidate.risk, "high" | "data" | "admin")
                    && self.candidate_status_for(candidate) == CandidateStatus::Allowed
            })
            .map(|candidate| (candidate.protocol, candidate.port))
            .collect::<Vec<_>>();

        self.review_exceptions.retain(|exception| {
            allowed_targets
                .iter()
                .any(|(protocol, port)| exception.protocol == *protocol && exception.port == *port)
        });
    }

    fn candidate_recommendation_for(
        &self,
        candidate: &PortCandidate,
        status: CandidateStatus,
    ) -> &'static str {
        let at_risk = matches!(candidate.risk, "high" | "data" | "admin");

        match (at_risk, status) {
            (true, CandidateStatus::Allowed) if self.has_review_exception(candidate) => {
                "reviewed exception recorded; verify scope and expiry"
            }
            (true, CandidateStatus::Allowed) => "block unless intentionally exposed",
            (true, CandidateStatus::Open) => "block or document an exception",
            (_, CandidateStatus::Blocked) => "covered by block",
            (false, CandidateStatus::Allowed) => "confirm service exposure is intentional",
            (false, CandidateStatus::Open) => "review if this service should be exposed",
        }
    }

    fn priority_score_for(
        &self,
        candidate: &PortCandidate,
        status: CandidateStatus,
    ) -> Option<(u16, String)> {
        if !matches!(candidate.risk, "high" | "data" | "admin")
            || status == CandidateStatus::Blocked
            || (status == CandidateStatus::Allowed && self.has_review_exception(candidate))
        {
            return None;
        }

        let mut score = 0;
        let mut reasons = Vec::new();

        match status {
            CandidateStatus::Allowed => {
                score += 120;
                reasons.push("unreviewed explicit allow");
            }
            CandidateStatus::Open => {
                score += 70;
                reasons.push("open known service");
            }
            CandidateStatus::Blocked => {}
        }

        match candidate.risk {
            "admin" => {
                score += 35;
                reasons.push("admin-control risk");
            }
            "data" => {
                score += 30;
                reasons.push("data exposure risk");
            }
            "high" => {
                score += 25;
                reasons.push("high-risk service");
            }
            _ => {}
        }

        match candidate.group {
            "admin" => {
                score += 15;
                reasons.push("admin group");
            }
            "data" => {
                score += 12;
                reasons.push("data group");
            }
            "file" | "legacy" | "orchestration" => {
                score += 10;
                reasons.push("broad exposure group");
            }
            _ => {}
        }

        if matches!(candidate.port, PortSpec::Range(_, _)) {
            score += 8;
            reasons.push("port range");
        }

        Some((score, reasons.join("; ")))
    }

    fn block_candidate_value(&mut self, candidate: &PortCandidate) -> bool {
        if self.candidate_status_for(candidate) == CandidateStatus::Blocked {
            return false;
        }

        self.remove_candidate_action(candidate.protocol, candidate.port, RuleAction::Allow);
        self.remove_review_exception_for(candidate);
        self.stage_rule(
            RuleAction::Deny,
            Direction::In,
            candidate.protocol,
            candidate.port,
            &format!("Block {}", candidate.name),
        );
        true
    }

    fn allow_candidate_named(&mut self, name: &str) -> bool {
        let Some(candidate) = self
            .candidates
            .iter()
            .find(|candidate| candidate.name == name)
            .cloned()
        else {
            return false;
        };

        if self.candidate_status_for(&candidate) == CandidateStatus::Allowed {
            return false;
        }

        self.allow_candidate_value(&candidate)
    }

    fn allow_candidate_value(&mut self, candidate: &PortCandidate) -> bool {
        if self.candidate_status_for(candidate) == CandidateStatus::Allowed {
            return false;
        }

        self.remove_candidate_blocks(candidate.protocol, candidate.port);
        self.stage_rule(
            RuleAction::Allow,
            Direction::In,
            candidate.protocol,
            candidate.port,
            &format!("Allow {}", candidate.name),
        );
        true
    }

    fn remove_candidate_blocks(&mut self, protocol: Protocol, port: PortSpec) -> usize {
        let before = self.rules.len();
        self.rules.retain(|rule| {
            !(rule.direction == Direction::In
                && matches!(rule.action, RuleAction::Deny | RuleAction::Reject)
                && rule.protocol == protocol
                && rule.port == port)
        });
        before.saturating_sub(self.rules.len())
    }

    fn remove_candidate_action(
        &mut self,
        protocol: Protocol,
        port: PortSpec,
        action: RuleAction,
    ) -> usize {
        let before = self.rules.len();
        self.rules.retain(|rule| {
            !(rule.direction == Direction::In
                && rule.action == action
                && rule.protocol == protocol
                && rule.port == port)
        });
        before.saturating_sub(self.rules.len())
    }

    fn base_policy_plan(&self) -> Vec<PlannedCommand> {
        match self.backend {
            Backend::Ufw => vec![
                PlannedCommand {
                    backend: self.backend,
                    program: "ufw",
                    args: vec![
                        "default".into(),
                        self.default_incoming.label().into(),
                        "incoming".into(),
                    ],
                    reason: "default incoming policy".into(),
                },
                PlannedCommand {
                    backend: self.backend,
                    program: "ufw",
                    args: vec![
                        "default".into(),
                        self.default_outgoing.label().into(),
                        "outgoing".into(),
                    ],
                    reason: "default outgoing policy".into(),
                },
            ],
            Backend::Iptables => vec![
                PlannedCommand {
                    backend: self.backend,
                    program: "iptables",
                    args: vec![
                        "-P".into(),
                        "INPUT".into(),
                        self.default_incoming.iptables_target().into(),
                    ],
                    reason: "default incoming policy".into(),
                },
                PlannedCommand {
                    backend: self.backend,
                    program: "iptables",
                    args: vec![
                        "-P".into(),
                        "OUTPUT".into(),
                        self.default_outgoing.iptables_target().into(),
                    ],
                    reason: "default outgoing policy".into(),
                },
            ],
            Backend::Nftables => vec![
                PlannedCommand {
                    backend: self.backend,
                    program: "nft",
                    args: vec![
                        "add".into(),
                        "chain".into(),
                        "inet".into(),
                        "filter".into(),
                        "input".into(),
                        "{ type filter hook input priority 0; policy".into(),
                        self.default_incoming.nft_policy().into(),
                        "; }".into(),
                    ],
                    reason: "default incoming policy".into(),
                },
                PlannedCommand {
                    backend: self.backend,
                    program: "nft",
                    args: vec![
                        "add".into(),
                        "chain".into(),
                        "inet".into(),
                        "filter".into(),
                        "output".into(),
                        "{ type filter hook output priority 0; policy".into(),
                        self.default_outgoing.nft_policy().into(),
                        "; }".into(),
                    ],
                    reason: "default outgoing policy".into(),
                },
            ],
        }
    }

    fn logging_plan(&self) -> Vec<PlannedCommand> {
        match self.backend {
            Backend::Ufw => vec![PlannedCommand {
                backend: self.backend,
                program: "ufw",
                args: vec!["logging".into(), "on".into()],
                reason: "enable firewall logging".into(),
            }],
            Backend::Iptables => vec![PlannedCommand {
                backend: self.backend,
                program: "iptables",
                args: vec![
                    "-A".into(),
                    "INPUT".into(),
                    "-m".into(),
                    "limit".into(),
                    "--limit".into(),
                    "6/min".into(),
                    "-j".into(),
                    "LOG".into(),
                    "--log-prefix".into(),
                    "knott-fw denied: ".into(),
                ],
                reason: "rate-limited deny logging".into(),
            }],
            Backend::Nftables => vec![PlannedCommand {
                backend: self.backend,
                program: "nft",
                args: vec![
                    "add".into(),
                    "rule".into(),
                    "inet".into(),
                    "filter".into(),
                    "input".into(),
                    "limit".into(),
                    "rate".into(),
                    "6/minute".into(),
                    "log".into(),
                    "prefix".into(),
                    "knott-fw denied: ".into(),
                ],
                reason: "rate-limited deny logging".into(),
            }],
        }
    }

    fn rule_plan(&self, rule: &FirewallRule) -> Vec<PlannedCommand> {
        match self.backend {
            Backend::Ufw => self.ufw_rule_plan(rule),
            Backend::Iptables => self.iptables_rule_plan(rule),
            Backend::Nftables => self.nft_rule_plan(rule),
        }
    }

    fn ufw_rule_plan(&self, rule: &FirewallRule) -> Vec<PlannedCommand> {
        let mut args = vec![rule.action.ufw_arg().into(), rule.direction.label().into()];
        if let Some(port) = rule.port.ufw_value(rule.protocol) {
            args.push(port);
        }

        vec![PlannedCommand {
            backend: self.backend,
            program: "ufw",
            args,
            reason: rule.note.clone(),
        }]
    }

    fn iptables_rule_plan(&self, rule: &FirewallRule) -> Vec<PlannedCommand> {
        let protocols: &[Protocol] = match rule.protocol {
            Protocol::Any if rule.port != PortSpec::Any => &[Protocol::Tcp, Protocol::Udp],
            Protocol::Any => &[Protocol::Any],
            Protocol::Tcp => &[Protocol::Tcp],
            Protocol::Udp => &[Protocol::Udp],
        };

        protocols
            .iter()
            .map(|protocol| {
                let mut args = vec!["-A".into(), rule.direction.iptables_chain().into()];
                if *protocol != Protocol::Any {
                    args.push("-p".into());
                    args.push(protocol.label().into());
                }
                if let Some(port) = rule.port.netfilter_value() {
                    args.push("--dport".into());
                    args.push(port);
                }
                args.push("-j".into());
                args.push(rule.action.netfilter_target().into());

                PlannedCommand {
                    backend: self.backend,
                    program: "iptables",
                    args,
                    reason: rule.note.clone(),
                }
            })
            .collect()
    }

    fn nft_rule_plan(&self, rule: &FirewallRule) -> Vec<PlannedCommand> {
        let protocols: &[Protocol] = match rule.protocol {
            Protocol::Any if rule.port != PortSpec::Any => &[Protocol::Tcp, Protocol::Udp],
            Protocol::Any => &[Protocol::Any],
            Protocol::Tcp => &[Protocol::Tcp],
            Protocol::Udp => &[Protocol::Udp],
        };

        protocols
            .iter()
            .map(|protocol| {
                let mut args = vec![
                    "add".into(),
                    "rule".into(),
                    "inet".into(),
                    "filter".into(),
                    rule.direction.nft_chain().into(),
                ];
                if *protocol != Protocol::Any {
                    args.push(protocol.label().into());
                }
                if let Some(port) = rule.port.netfilter_value() {
                    args.push("dport".into());
                    args.push(port);
                }
                args.push(rule.action.nft_verdict().into());

                PlannedCommand {
                    backend: self.backend,
                    program: "nft",
                    args,
                    reason: rule.note.clone(),
                }
            })
            .collect()
    }
}

fn default_candidates() -> Vec<PortCandidate> {
    vec![
        PortCandidate {
            name: "SSH",
            group: "admin",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(22),
            risk: "admin",
            aliases: &["openssh", "sshd", "secure shell"],
            detail: "remote shell access",
        },
        PortCandidate {
            name: "Telnet",
            group: "legacy",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(23),
            risk: "high",
            aliases: &["telnetd", "plaintext shell"],
            detail: "plaintext remote shell",
        },
        PortCandidate {
            name: "SMTP",
            group: "mail",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(25),
            risk: "mail",
            aliases: &["mail", "postfix", "sendmail"],
            detail: "mail transfer exposure",
        },
        PortCandidate {
            name: "DNS",
            group: "infra",
            protocol: Protocol::Udp,
            port: PortSpec::Single(53),
            risk: "infra",
            aliases: &["domain", "bind", "named"],
            detail: "resolver amplification risk",
        },
        PortCandidate {
            name: "HTTP",
            group: "web",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(80),
            risk: "web",
            aliases: &["www", "nginx", "apache"],
            detail: "public web traffic",
        },
        PortCandidate {
            name: "HTTPS",
            group: "web",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(443),
            risk: "web",
            aliases: &["tls", "ssl", "https-alt"],
            detail: "public TLS traffic",
        },
        PortCandidate {
            name: "SMB",
            group: "file",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(445),
            risk: "high",
            aliases: &["samba", "cifs", "windows share"],
            detail: "Windows file sharing",
        },
        PortCandidate {
            name: "NFS",
            group: "file",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(2049),
            risk: "high",
            aliases: &["nfsd", "rpc mount"],
            detail: "Unix file sharing",
        },
        PortCandidate {
            name: "RDP",
            group: "admin",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(3389),
            risk: "admin",
            aliases: &["remote desktop", "termservice"],
            detail: "remote desktop access",
        },
        PortCandidate {
            name: "VNC",
            group: "admin",
            protocol: Protocol::Tcp,
            port: PortSpec::Range(5900, 5999),
            risk: "admin",
            aliases: &["remote framebuffer", "screen sharing"],
            detail: "remote desktop range",
        },
        PortCandidate {
            name: "MySQL",
            group: "data",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(3306),
            risk: "data",
            aliases: &["mariadb", "mysqld"],
            detail: "database listener",
        },
        PortCandidate {
            name: "Postgres",
            group: "data",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(5432),
            risk: "data",
            aliases: &["postgresql", "postgres"],
            detail: "database listener",
        },
        PortCandidate {
            name: "Redis",
            group: "data",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(6379),
            risk: "data",
            aliases: &["redis-server", "cache"],
            detail: "cache/database listener",
        },
        PortCandidate {
            name: "MongoDB",
            group: "data",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(27017),
            risk: "data",
            aliases: &["mongod", "document db"],
            detail: "document database listener",
        },
        PortCandidate {
            name: "Elasticsearch",
            group: "data",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(9200),
            risk: "data",
            aliases: &["elastic", "opensearch", "search api"],
            detail: "search database API",
        },
        PortCandidate {
            name: "Docker API",
            group: "orchestration",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(2375),
            risk: "high",
            aliases: &["dockerd", "container api"],
            detail: "unencrypted Docker API",
        },
        PortCandidate {
            name: "Kubernetes",
            group: "orchestration",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(6443),
            risk: "admin",
            aliases: &["k8s", "apiserver", "control plane"],
            detail: "cluster control plane",
        },
        PortCandidate {
            name: "Prometheus",
            group: "observability",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(9090),
            risk: "infra",
            aliases: &["metrics", "prom"],
            detail: "metrics database UI/API",
        },
        PortCandidate {
            name: "Grafana",
            group: "observability",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(3000),
            risk: "infra",
            aliases: &["dashboard", "grafana-server"],
            detail: "dashboard web UI",
        },
        PortCandidate {
            name: "Jupyter",
            group: "dev",
            protocol: Protocol::Tcp,
            port: PortSpec::Single(8888),
            risk: "dev",
            aliases: &["notebook", "jupyterlab"],
            detail: "notebook execution server",
        },
        PortCandidate {
            name: "Dev Range",
            group: "dev",
            protocol: Protocol::Tcp,
            port: PortSpec::Range(3000, 3999),
            risk: "dev",
            aliases: &["node dev", "vite", "rails dev"],
            detail: "local app servers",
        },
    ]
}

fn shell_quote(value: &str) -> String {
    if value.bytes().all(|byte| {
        byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'/' | b':' | b'=')
    }) {
        return value.to_string();
    }

    format!("'{}'", value.replace('\'', "'\\''"))
}

fn json_string(value: &str) -> String {
    let mut escaped = String::with_capacity(value.len() + 2);
    escaped.push('"');
    for character in value.chars() {
        match character {
            '"' => escaped.push_str("\\\""),
            '\\' => escaped.push_str("\\\\"),
            '\n' => escaped.push_str("\\n"),
            '\r' => escaped.push_str("\\r"),
            '\t' => escaped.push_str("\\t"),
            character if character.is_control() => {
                let _ = write!(escaped, "\\u{:04x}", character as u32);
            }
            character => escaped.push(character),
        }
    }
    escaped.push('"');
    escaped
}

fn json_array(values: &[&str]) -> String {
    values
        .iter()
        .map(|value| json_string(value))
        .collect::<Vec<_>>()
        .join(", ")
}

fn profile_recommendation_score(impact: ProfileImpactSummary) -> i32 {
    let lockout_penalty = if impact.management_all_blocked { 25 } else { 0 };

    i32::from(impact.coverage_percent) * 2
        - impact.at_risk_open as i32 * 40
        - impact.staged_changes as i32
        - lockout_penalty
}

fn profile_change_reason(
    profile: FirewallProfile,
    candidate: &PortCandidate,
    after: CandidateStatus,
) -> String {
    let action = match after {
        CandidateStatus::Blocked => "blocks",
        CandidateStatus::Allowed => "allows",
        CandidateStatus::Open => "leaves open",
    };

    format!(
        "{} profile {} {} service exposure",
        profile.label(),
        action,
        candidate.group
    )
}

fn fingerprint_write(hash: &mut u64, value: &str) {
    for byte in value.len().to_le_bytes() {
        *hash ^= u64::from(byte);
        *hash = hash.wrapping_mul(FNV_PRIME);
    }

    for byte in value.bytes() {
        *hash ^= u64::from(byte);
        *hash = hash.wrapping_mul(FNV_PRIME);
    }
}

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

    #[test]
    fn dry_run_report_contains_review_context_and_plan() {
        let controller = FirewallController::demo();

        let report = controller.dry_run_report();

        assert!(report.contains("Review-only artifact. No commands were executed."));
        assert!(report.contains("## Suggested Review Order"));
        assert!(report.contains("## Review Gates"));
        assert!(report.contains("## Priority Port Queue"));
        assert!(report.contains("## Reviewed Exceptions"));
        assert!(report.contains("## Management Access Guard"));
        assert!(report.contains("## Advisor Findings"));
        assert!(report.contains("## Known Port Groups"));
        assert!(report.contains("## Known Port Risks"));
        assert!(report.contains("## At-Risk Open Ports"));
        assert!(report.contains("## At-Risk Allowed Ports"));
        assert!(report.contains("## Known Port Catalog"));
        assert!(report.contains("## Backend Comparison"));
        assert!(report.contains("## Recommended Profile"));
        assert!(report.contains("## Recommended Profile Changes"));
        assert!(report.contains("## Management-Preserving Profile Changes"));
        assert!(report.contains("## Profile Impact Preview"));
        assert!(report.contains("- advisor findings: "));
        assert!(report.contains("- next advisor action: "));
        assert!(report.contains("1. Review advisor warnings - 11 warning finding(s) remain"));
        assert!(
            report.contains("2. Close at-risk open known ports - 11 admin/high/data known port(s)")
        );
        assert!(report
            .contains("1. [open] VNC tcp/5900-5999 admin/admin - block or document an exception"));
        assert!(report.contains(
            "1. [open] VNC tcp/5900-5999 admin/admin - block or document an exception | score 128 | reason: open known service; admin-control risk; admin group; port range"
        ));
        assert!(report.contains("- known ports: "));
        assert!(report.contains("- high: 4 total, 2 blocked, 0 allowed, 2 open"));
        assert!(report.contains("- review readiness: 62% (C / needs hardening)"));
        assert!(report.contains("- review gates: 8 pass, 2 warn, 0 block"));
        assert!(report.contains("- [warn] Recommended profile safety"));
        assert!(report.contains(
            "  action: Press y for the management-preserving developer profile, or confirm console access before Y."
        ));
        assert!(report.contains("- risky allow exceptions: 0 reviewed, 0 unreviewed"));
        assert!(report.contains("- management access: 0 blocked, 0 allowed, 4 open"));
        assert!(report.contains(
            "- recommended profile: workstation (score 162, +13 changes, 100% coverage, 0 at-risk open)"
        ));
        assert!(report.contains(
            "- recommended profile warning: workstation blocks all 4 known management/control-plane access path(s); confirm console or break-glass access before use"
        ));
        assert!(report.contains(
            "- management-preserving profile: developer (score 24, +8 changes, 76% coverage, 3 at-risk open)"
        ));
        assert!(report.contains(
            "- strongest hardening: workstation: score 162, +13 staged change(s), 16 resulting rule(s), 100% coverage, 0 at-risk open"
        ));
        assert!(report.contains("  management access after profile: 4 blocked, 0 allowed, 0 open"));
        assert!(report.contains(
            "- management-preserving alternative: developer: score 24, +8 staged change(s), 11 resulting rule(s), 76% coverage, 3 at-risk open"
        ));
        assert!(report.contains("  management access after profile: 1 blocked, 0 allowed, 3 open"));
        assert!(report.contains(
            "- SSH tcp/22 admin/admin: open -> blocked | workstation profile blocks admin service exposure"
        ));
        assert!(report.contains(
            "- NFS tcp/2049 file/high: open -> blocked | developer profile blocks file service exposure"
        ));
        assert!(report.contains(
            "- workstation: 13 staged change(s), 16 resulting rule(s), 100% coverage, 0 at-risk open, management 4 blocked/0 allowed/0 open (full lockout)"
        ));
        assert!(report.contains(
            "- developer: 8 staged change(s), 11 resulting rule(s), 76% coverage, 3 at-risk open, management 1 blocked/0 allowed/3 open"
        ));
        assert!(
            report.contains("- 4 management/control-plane path(s): 0 blocked, 0 allowed, 4 open")
        );
        assert!(report
            .contains("- [open] Kubernetes tcp/6443 orchestration/admin - cluster control plane"));
        assert!(report.contains("- [warn] At-risk open known ports"));
        assert!(report.contains("- SSH tcp/22 admin/admin - remote shell access"));
        assert!(report.contains("- MySQL tcp/3306 data/data - database listener"));
        assert!(report.contains(
            "- [blocked] Telnet tcp/23 legacy/high aliases telnetd, plaintext shell - plaintext remote shell"
        ));
        assert!(report.contains(
            "- [open] SSH tcp/22 admin/admin aliases openssh, sshd, secure shell - remote shell access"
        ));
        assert!(report.contains("action: block or document an exception"));
        assert!(report.contains("action: covered by block"));
        assert!(report.contains("- review fingerprint: "));
        assert!(report.contains("ufw default deny incoming"));
        assert!(report.contains("iptables:"));
    }

    #[test]
    fn dry_run_script_exits_before_commented_commands() {
        let controller = FirewallController::demo();

        let script = controller.dry_run_script();

        assert!(script.contains("No firewall commands are executed by this file."));
        assert!(script.contains("exit 0\n\n# Backend preview: ufw"));
        assert!(script.contains("# Review fingerprint: "));
        assert!(script.contains("# ufw default deny incoming"));
        assert!(!script
            .lines()
            .any(|line| line == "ufw default deny incoming"));
    }

    #[test]
    fn dry_run_manifest_contains_review_only_machine_readable_plan() {
        let controller = FirewallController::demo();
        let manifest = controller.dry_run_manifest();

        assert!(manifest.contains("\"schema\": \"knott.firewall.dry-run.v1\""));
        assert!(manifest.contains("\"review_only\": true"));
        assert!(manifest.contains("\"executes_commands\": false"));
        assert!(manifest.contains("\"advisor_next_action\": {"));
        assert!(manifest.contains("\"recommendation\": "));
        assert!(manifest.contains("\"advisor_summary\": {"));
        assert!(manifest.contains("\"critical\": "));
        assert!(manifest.contains("\"review_gate_summary\": {"));
        assert!(manifest.contains("\"pass\": 8,"));
        assert!(manifest.contains("\"warn\": 2,"));
        assert!(manifest.contains("\"block\": 0"));
        assert!(manifest.contains("\"management_access\": {"));
        assert!(manifest.contains("\"total\": 4,"));
        assert!(manifest.contains("\"blocked\": 0,"));
        assert!(manifest.contains("\"open\": 4,"));
        assert!(manifest.contains("\"all_blocked\": false,"));
        assert!(manifest.contains(
            "\"name\": \"Kubernetes\", \"group\": \"orchestration\", \"risk\": \"admin\", \"protocol\": \"tcp\", \"port\": \"6443\", \"status\": \"open\""
        ));
        assert!(manifest.contains("\"review_gates\": ["));
        assert!(manifest.contains("\"status\": \"warn\", \"title\": \"At-risk open known ports\""));
        assert!(
            manifest.contains("\"status\": \"warn\", \"title\": \"Recommended profile safety\"")
        );
        assert!(manifest.contains(
            "\"action\": \"Press y for the management-preserving developer profile, or confirm console access before Y.\""
        ));
        assert!(manifest.contains("\"review_order\": ["));
        assert!(manifest.contains(
            "\"index\": 1, \"title\": \"Review advisor warnings\", \"detail\": \"11 warning finding(s) remain"
        ));
        assert!(manifest.contains("\"title\": \"Close at-risk open known ports\""));
        assert!(manifest.contains("\"priority_port_queue\": ["));
        assert!(manifest.contains("\"reviewed_exceptions\": ["));
        assert!(manifest.contains("\"reviewed_exceptions\": 0"));
        assert!(manifest.contains(
            "\"index\": 1, \"name\": \"VNC\", \"group\": \"admin\", \"risk\": \"admin\", \"protocol\": \"tcp\", \"port\": \"5900-5999\", \"status\": \"open\""
        ));
        assert!(manifest.contains("\"score\": 128"));
        assert!(manifest.contains(
            "\"reason\": \"open known service; admin-control risk; admin group; port range\""
        ));
        assert!(manifest.contains("\"known_ports\": "));
        assert!(manifest.contains("\"readiness_grade\": \"C\""));
        assert!(manifest.contains("\"readiness_label\": \"needs hardening\""));
        assert!(manifest.contains("\"at_risk_open\": "));
        assert!(manifest.contains(&format!(
            "\"fingerprint\": \"{}\"",
            controller.plan_fingerprint()
        )));
        assert!(manifest.contains("\"commands\": ["));
        assert!(manifest.contains("\"known_port_catalog\": ["));
        assert!(manifest.contains("\"name\": \"SSH\""));
        assert!(manifest.contains("\"detail\": \"remote shell access\""));
        assert!(manifest.contains("\"aliases\": [\"openssh\", \"sshd\", \"secure shell\"]"));
        assert!(manifest.contains("\"recommendation\": \"block or document an exception\""));
        assert!(manifest.contains("\"recommendation\": \"covered by block\""));
        assert!(manifest.contains(
            "\"name\": \"Telnet\", \"group\": \"legacy\", \"risk\": \"high\", \"aliases\": [\"telnetd\", \"plaintext shell\"], \"protocol\": \"tcp\", \"port\": \"23\", \"status\": \"blocked\""
        ));
        assert!(manifest.contains("\"at_risk_open_ports\": ["));
        assert!(manifest.contains(
            "\"name\": \"SSH\", \"group\": \"admin\", \"risk\": \"admin\", \"protocol\": \"tcp\", \"port\": \"22\", \"status\": \"open\""
        ));
        assert!(manifest.contains("\"at_risk_allowed_ports\": ["));
        assert!(manifest.contains("\"group_coverage\": ["));
        assert!(manifest.contains("\"group\": \"admin\""));
        assert!(manifest.contains("\"risk_coverage\": ["));
        assert!(manifest.contains("\"risk\": \"admin\""));
        assert!(manifest.contains("\"backend_comparison\": ["));
        assert!(manifest.contains("\"recommended_profile\": {"));
        assert!(manifest.contains("\"profile\": \"workstation\""));
        assert!(manifest.contains("\"score\": 162"));
        assert!(manifest.contains("\"staged_changes\": 13"));
        assert!(manifest.contains("\"resulting_rules\": 16"));
        assert!(manifest.contains("\"management_blocked\": 4"));
        assert!(manifest.contains("\"management_allowed\": 0"));
        assert!(manifest.contains("\"management_open\": 0"));
        assert!(manifest.contains("\"management_all_blocked\": true"));
        assert!(manifest.contains(
            "\"warning\": \"workstation blocks all 4 known management/control-plane access path(s); confirm console or break-glass access before use\""
        ));
        assert!(manifest.contains(
            "\"reason\": \"eliminates at-risk open ports with 13 staged change(s) and 100% coverage\""
        ));
        assert!(manifest.contains("\"safe_recommended_profile\": {"));
        assert!(manifest.contains("\"profile\": \"developer\""));
        assert!(manifest.contains("\"score\": 24"));
        assert!(manifest.contains("\"staged_changes\": 8"));
        assert!(manifest.contains("\"resulting_rules\": 11"));
        assert!(manifest.contains("\"coverage_percent\": 76"));
        assert!(manifest.contains("\"at_risk_open\": 3"));
        assert!(manifest.contains("\"management_blocked\": 1"));
        assert!(manifest.contains("\"management_open\": 3"));
        assert!(manifest.contains("\"management_all_blocked\": false"));
        assert!(manifest.contains(
            "\"reason\": \"best available profile leaves 3 at-risk open port(s) with 76% coverage\""
        ));
        assert!(manifest.contains("\"recommended_profile_changes\": ["));
        assert!(manifest.contains(
            "\"index\": 1, \"candidate_index\": 0, \"name\": \"SSH\", \"group\": \"admin\", \"risk\": \"admin\", \"protocol\": \"tcp\", \"port\": \"22\", \"before\": \"open\", \"after\": \"blocked\""
        ));
        assert!(
            manifest.contains("\"reason\": \"workstation profile blocks admin service exposure\"")
        );
        assert!(manifest.contains("\"safe_recommended_profile_changes\": ["));
        assert!(manifest.contains(
            "\"index\": 1, \"candidate_index\": 7, \"name\": \"NFS\", \"group\": \"file\", \"risk\": \"high\", \"protocol\": \"tcp\", \"port\": \"2049\", \"before\": \"open\", \"after\": \"blocked\""
        ));
        assert!(manifest.contains("\"reason\": \"developer profile blocks file service exposure\""));
        assert!(manifest.contains("\"profile_impacts\": ["));
        assert!(manifest.contains("\"profile\": \"lockdown\""));
        assert!(manifest.contains(
            "\"profile\": \"developer\", \"staged_changes\": 8, \"resulting_rules\": 11, \"coverage_percent\": 76, \"at_risk_open\": 3, \"management_total\": 4, \"management_blocked\": 1, \"management_allowed\": 0, \"management_open\": 3, \"management_all_blocked\": false"
        ));
        assert!(manifest.contains(
            "\"profile\": \"workstation\", \"staged_changes\": 13, \"resulting_rules\": 16, \"coverage_percent\": 100, \"at_risk_open\": 0, \"management_total\": 4, \"management_blocked\": 4, \"management_allowed\": 0, \"management_open\": 0, \"management_all_blocked\": true"
        ));
        assert!(manifest.contains("\"backend\": \"nftables\""));
        assert!(manifest.contains("\"args\": [\"default\", \"deny\", \"incoming\"]"));
        assert!(manifest.contains("\"shell\": \"ufw default deny incoming\""));
    }

    #[test]
    fn dry_run_exports_surface_at_risk_allowed_ports() {
        let mut controller = FirewallController::demo();
        let mysql = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == "MySQL")
            .expect("known test candidate");

        controller.allow_candidate(mysql);

        let report = controller.dry_run_report();
        let manifest = controller.dry_run_manifest();

        assert!(report.contains("## At-Risk Allowed Ports"));
        assert!(report.contains("- MySQL tcp/3306 data/data - database listener"));
        assert!(report.contains(
            "- [allowed] MySQL tcp/3306 data/data aliases mariadb, mysqld - database listener"
        ));
        assert!(report.contains("action: block unless intentionally exposed"));
        assert!(manifest.contains(
            "\"name\": \"MySQL\", \"group\": \"data\", \"risk\": \"data\", \"protocol\": \"tcp\", \"port\": \"3306\", \"status\": \"allowed\""
        ));
        assert!(manifest.contains(
            "\"index\": 1, \"name\": \"MySQL\", \"group\": \"data\", \"risk\": \"data\", \"protocol\": \"tcp\", \"port\": \"3306\", \"status\": \"allowed\""
        ));
        assert!(manifest.contains("\"score\": 162"));
        assert!(manifest
            .contains("\"reason\": \"unreviewed explicit allow; data exposure risk; data group\""));
        assert!(manifest.contains("\"recommendation\": \"block unless intentionally exposed\""));
    }

    #[test]
    fn priority_ports_are_scored_ranked_and_skip_reviewed_exceptions() {
        let mut controller = FirewallController::demo();
        let initial = controller.priority_ports(3);

        assert_eq!(initial[0].name, "VNC");
        assert_eq!(initial[0].status, CandidateStatus::Open);
        assert_eq!(initial[0].score, 128);
        assert_eq!(
            initial[0].reason,
            "open known service; admin-control risk; admin group; port range"
        );

        let mysql = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == "MySQL")
            .expect("known test candidate");
        controller.allow_candidate(mysql);
        let exposed = controller.priority_ports(2);
        assert_eq!(exposed[0].name, "MySQL");
        assert_eq!(exposed[0].status, CandidateStatus::Allowed);
        assert_eq!(exposed[0].score, 162);
        assert_eq!(
            exposed[0].reason,
            "unreviewed explicit allow; data exposure risk; data group"
        );

        assert_eq!(controller.toggle_candidate_exception(mysql), Some(true));
        let reviewed = controller.priority_ports(2);
        assert_eq!(reviewed[0].name, "VNC");
        assert!(reviewed.iter().all(|priority| priority.name != "MySQL"));

        controller.apply_advisor_quick_fix();
        assert!(controller.priority_ports(1).is_empty());
    }

    #[test]
    fn reviewed_exceptions_are_undoable_and_exported() {
        let mut controller = FirewallController::demo();
        let mysql = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == "MySQL")
            .expect("known test candidate");

        controller.allow_candidate(mysql);
        let allowed_fingerprint = controller.plan_fingerprint();
        assert_eq!(controller.stats().at_risk_allowed, 1);
        assert_eq!(controller.stats().unreviewed_at_risk_allowed, 1);
        assert_eq!(controller.advisor_summary().critical, 1);

        assert_eq!(controller.toggle_candidate_exception(mysql), Some(true));
        assert!(controller.candidate_has_review_exception(mysql));
        assert_ne!(allowed_fingerprint, controller.plan_fingerprint());

        let stats = controller.stats();
        assert_eq!(stats.at_risk_allowed, 1);
        assert_eq!(stats.reviewed_exceptions, 1);
        assert_eq!(stats.unreviewed_at_risk_allowed, 0);
        assert_eq!(controller.review_exceptions().len(), 1);
        assert_eq!(controller.advisor_summary().critical, 0);
        assert!(controller.advisor_findings().iter().any(|finding| {
            finding.title == "MySQL has a reviewed allow exception"
                && finding.recommendation.contains("source scope")
        }));

        let report = controller.dry_run_report();
        let manifest = controller.dry_run_manifest();
        assert!(report.contains("## Reviewed Exceptions"));
        assert!(report.contains(
            "- MySQL tcp/3306 data/data - reviewed dry-run exception; verify owner, source scope, and expiry before live apply"
        ));
        assert!(report.contains("action: reviewed exception recorded; verify scope and expiry"));
        assert!(manifest.contains(
            "\"name\": \"MySQL\", \"group\": \"data\", \"risk\": \"data\", \"protocol\": \"tcp\", \"port\": \"3306\", \"note\": \"reviewed dry-run exception; verify owner, source scope, and expiry before live apply\""
        ));
        assert!(manifest.contains(
            "\"name\": \"MySQL\", \"group\": \"data\", \"risk\": \"data\", \"protocol\": \"tcp\", \"port\": \"3306\", \"status\": \"allowed\", \"review_exception\": true"
        ));

        controller.block_candidate(mysql);
        assert_eq!(
            controller.candidate_status(mysql),
            Some(CandidateStatus::Blocked)
        );
        assert!(!controller.candidate_has_review_exception(mysql));
        assert_eq!(controller.review_exceptions().len(), 0);

        assert!(controller.undo());
        assert_eq!(
            controller.candidate_status(mysql),
            Some(CandidateStatus::Allowed)
        );
        assert!(controller.candidate_has_review_exception(mysql));

        assert!(controller.undo());
        assert_eq!(
            controller.candidate_status(mysql),
            Some(CandidateStatus::Allowed)
        );
        assert!(!controller.candidate_has_review_exception(mysql));
        assert_eq!(controller.plan_fingerprint(), allowed_fingerprint);
    }

    #[test]
    fn review_gates_track_blockers_warnings_and_clean_state() {
        let mut controller = FirewallController::demo();
        let summary = controller.review_gate_summary();

        assert_eq!(summary.pass, 8);
        assert_eq!(summary.warn, 2);
        assert_eq!(summary.block, 0);
        assert_eq!(
            controller.next_review_gate().title,
            "At-risk open known ports"
        );
        assert!(controller.review_gates().iter().any(|gate| {
            gate.title == "Recommended profile safety"
                && gate.status == ReviewGateStatus::Warn
                && gate.action.contains("Press y")
        }));

        let mysql = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == "MySQL")
            .expect("known test candidate");
        controller.allow_candidate(mysql);
        let exposed_gates = controller.review_gates();
        assert!(exposed_gates.iter().any(|gate| {
            gate.title == "Critical advisor findings" && gate.status == ReviewGateStatus::Block
        }));
        assert!(exposed_gates.iter().any(|gate| {
            gate.title == "Risky allow exceptions" && gate.status == ReviewGateStatus::Block
        }));

        assert_eq!(controller.toggle_candidate_exception(mysql), Some(true));
        let reviewed_gates = controller.review_gates();
        assert!(reviewed_gates.iter().any(|gate| {
            gate.title == "Risky allow exceptions" && gate.status == ReviewGateStatus::Warn
        }));
        assert_eq!(controller.review_gate_summary().block, 0);

        controller.apply_advisor_quick_fix();
        let clean = controller.review_gate_summary();
        assert_eq!(clean.block, 0);
        assert_eq!(clean.warn, 2);
        assert_eq!(
            controller.next_review_gate().title,
            "Management access continuity"
        );
    }

    #[test]
    fn management_access_summary_warns_after_full_admin_lockdown() {
        let mut controller = FirewallController::demo();
        let initial = controller.management_access_summary();

        assert_eq!(initial.total, 4);
        assert_eq!(initial.blocked, 0);
        assert_eq!(initial.open, 4);
        assert!(!initial.all_blocked());

        controller.apply_advisor_quick_fix();
        let locked = controller.management_access_summary();

        assert_eq!(locked.total, 4);
        assert_eq!(locked.blocked, 4);
        assert_eq!(locked.open, 0);
        assert!(locked.all_blocked());
        assert!(controller.advisor_findings().iter().any(|finding| {
            finding.title == "Management access is fully blocked"
                && finding.recommendation.contains("break-glass")
        }));
        assert!(controller.review_gates().iter().any(|gate| {
            gate.title == "Management access continuity" && gate.status == ReviewGateStatus::Warn
        }));
    }

    #[test]
    fn dry_run_artifacts_share_review_fingerprint() {
        let controller = FirewallController::demo();
        let fingerprint = controller.plan_fingerprint();
        let report = controller.dry_run_report();
        let script = controller.dry_run_script();
        let manifest = controller.dry_run_manifest();

        assert!(report.contains(&format!("review fingerprint: {fingerprint}")));
        assert!(script.contains(&format!("# Review fingerprint: {fingerprint}")));
        assert!(manifest.contains(&format!("\"fingerprint\": \"{fingerprint}\"")));
    }

    #[test]
    fn backend_plan_summaries_compare_without_changing_selected_backend() {
        let mut controller = FirewallController::demo();
        controller.cycle_backend();
        let selected_backend = controller.backend();

        let summaries = controller.backend_plan_summaries();

        assert_eq!(controller.backend(), selected_backend);
        assert_eq!(summaries.len(), Backend::all().len());
        assert!(summaries
            .iter()
            .any(|summary| summary.backend == Backend::Ufw
                && summary.first_command == "ufw default deny incoming"));
        assert!(summaries
            .iter()
            .any(|summary| summary.backend == Backend::Iptables
                && summary.first_command == "iptables -P INPUT DROP"));
        assert!(summaries
            .iter()
            .any(|summary| summary.backend == Backend::Nftables
                && summary
                    .first_command
                    .starts_with("nft add chain inet filter input")));
    }

    #[test]
    fn profile_impact_summaries_compare_without_changing_current_plan() {
        let mut controller = FirewallController::demo();
        controller.cycle_backend();
        let before_backend = controller.backend();
        let before_fingerprint = controller.plan_fingerprint();
        let before_history = controller.history_depth();

        let impacts = controller.profile_impact_summaries();

        assert_eq!(controller.backend(), before_backend);
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.history_depth(), before_history);
        assert_eq!(impacts.len(), FirewallProfile::all().len());
        assert!(impacts
            .iter()
            .all(|impact| impact.resulting_rules >= controller.rules().len()));
        let developer = impacts
            .iter()
            .find(|impact| impact.profile == FirewallProfile::Developer)
            .expect("developer profile impact");
        assert_eq!(developer.management_total, 4);
        assert_eq!(developer.management_blocked, 1);
        assert_eq!(developer.management_allowed, 0);
        assert_eq!(developer.management_open, 3);
        assert!(!developer.management_all_blocked);
        assert!(impacts.iter().any(|impact| {
            impact.profile == FirewallProfile::Lockdown
                && impact.coverage_percent == 100
                && impact.at_risk_open == 0
                && impact.management_blocked == 4
                && impact.management_open == 0
                && impact.management_all_blocked
        }));
    }

    #[test]
    fn recommended_profile_selects_strongest_least_change_hardening_without_mutation() {
        let mut controller = FirewallController::demo();
        controller.cycle_backend();
        let before_backend = controller.backend();
        let before_fingerprint = controller.plan_fingerprint();
        let before_history = controller.history_depth();

        let recommendation = controller.recommended_profile();

        assert_eq!(recommendation.profile, FirewallProfile::Workstation);
        assert_eq!(recommendation.staged_changes, 13);
        assert_eq!(recommendation.resulting_rules, 16);
        assert_eq!(recommendation.coverage_percent, 100);
        assert_eq!(recommendation.at_risk_open, 0);
        assert_eq!(recommendation.management_blocked, 4);
        assert_eq!(recommendation.management_allowed, 0);
        assert_eq!(recommendation.management_open, 0);
        assert!(recommendation.management_all_blocked);
        assert_eq!(recommendation.score, 162);
        assert_eq!(
            recommendation.reason,
            "eliminates at-risk open ports with 13 staged change(s) and 100% coverage"
        );
        assert_eq!(
            recommendation.warning.as_deref(),
            Some(
                "workstation blocks all 4 known management/control-plane access path(s); confirm console or break-glass access before use"
            )
        );
        assert_eq!(controller.backend(), before_backend);
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.history_depth(), before_history);
    }

    #[test]
    fn safe_recommended_profile_preserves_management_access_without_mutation() {
        let mut controller = FirewallController::demo();
        controller.cycle_backend();
        let before_backend = controller.backend();
        let before_fingerprint = controller.plan_fingerprint();
        let before_history = controller.history_depth();

        let recommendation = controller
            .safe_recommended_profile()
            .expect("demo plan has a management-preserving profile");
        let changes = controller.safe_recommended_profile_changes();

        assert_eq!(recommendation.profile, FirewallProfile::Developer);
        assert_eq!(recommendation.staged_changes, 8);
        assert_eq!(recommendation.resulting_rules, 11);
        assert_eq!(recommendation.coverage_percent, 76);
        assert_eq!(recommendation.at_risk_open, 3);
        assert_eq!(recommendation.management_blocked, 1);
        assert_eq!(recommendation.management_allowed, 0);
        assert_eq!(recommendation.management_open, 3);
        assert!(!recommendation.management_all_blocked);
        assert_eq!(recommendation.score, 24);
        assert_eq!(
            recommendation.reason,
            "best available profile leaves 3 at-risk open port(s) with 76% coverage"
        );
        assert_eq!(recommendation.warning, None);
        assert_eq!(changes.len(), 8);
        assert_eq!(changes[0].profile, FirewallProfile::Developer);
        assert_eq!(changes[0].candidate_index, 7);
        assert_eq!(changes[0].name, "NFS");
        assert_eq!(changes[0].before, CandidateStatus::Open);
        assert_eq!(changes[0].after, CandidateStatus::Blocked);
        assert_eq!(controller.backend(), before_backend);
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.history_depth(), before_history);

        let mut locked = controller.clone();
        locked.apply_profile(FirewallProfile::Lockdown);
        assert_eq!(locked.safe_recommended_profile(), None);
    }

    #[test]
    fn recommended_profile_changes_preview_lists_deltas_without_mutation() {
        let controller = FirewallController::demo();
        let before_fingerprint = controller.plan_fingerprint();
        let before_history = controller.history_depth();

        let changes = controller.recommended_profile_changes();

        assert_eq!(changes.len(), 13);
        assert_eq!(changes[0].profile, FirewallProfile::Workstation);
        assert_eq!(changes[0].candidate_index, 0);
        assert_eq!(changes[0].name, "SSH");
        assert_eq!(changes[0].before, CandidateStatus::Open);
        assert_eq!(changes[0].after, CandidateStatus::Blocked);
        assert_eq!(
            changes[0].reason,
            "workstation profile blocks admin service exposure"
        );
        assert!(changes.iter().any(|change| {
            change.name == "MySQL"
                && change.before == CandidateStatus::Open
                && change.after == CandidateStatus::Blocked
        }));
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.history_depth(), before_history);
    }

    #[test]
    fn apply_recommended_profile_stages_selected_profile_and_is_undoable() {
        let mut controller = FirewallController::demo();
        let before_fingerprint = controller.plan_fingerprint();

        let (profile, staged) = controller.apply_recommended_profile();

        assert_eq!(profile, FirewallProfile::Workstation);
        assert_eq!(staged, 13);
        assert_eq!(controller.stats().at_risk_open, 0);
        assert_eq!(
            candidate_status_named(&controller, "SSH"),
            CandidateStatus::Blocked
        );
        assert_eq!(
            candidate_status_named(&controller, "MySQL"),
            CandidateStatus::Blocked
        );
        assert!(controller
            .audit_log()
            .iter()
            .any(|message| message == "Applied recommended workstation dry-run profile."));

        assert!(controller.undo());
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.stats().at_risk_open, 11);
    }

    #[test]
    fn apply_safe_recommended_profile_stages_management_preserving_profile_and_is_undoable() {
        let mut controller = FirewallController::demo();
        let before_fingerprint = controller.plan_fingerprint();

        let (profile, staged) = controller
            .apply_safe_recommended_profile()
            .expect("demo plan has a management-preserving profile");

        assert_eq!(profile, FirewallProfile::Developer);
        assert_eq!(staged, 8);
        assert_eq!(controller.stats().at_risk_open, 3);
        assert_eq!(
            candidate_status_named(&controller, "SSH"),
            CandidateStatus::Open
        );
        assert_eq!(
            candidate_status_named(&controller, "Kubernetes"),
            CandidateStatus::Blocked
        );
        assert_eq!(
            candidate_status_named(&controller, "MySQL"),
            CandidateStatus::Blocked
        );
        assert!(controller.audit_log().iter().any(|message| {
            message == "Applied management-preserving recommended developer dry-run profile."
        }));

        assert!(controller.undo());
        assert_eq!(controller.plan_fingerprint(), before_fingerprint);
        assert_eq!(controller.stats().at_risk_open, 11);
    }

    #[test]
    fn apply_safe_recommended_profile_returns_none_when_all_profiles_lock_management() {
        let mut controller = FirewallController::demo();
        controller.apply_profile(FirewallProfile::Lockdown);
        let locked_fingerprint = controller.plan_fingerprint();
        let locked_history = controller.history_depth();

        assert_eq!(controller.apply_safe_recommended_profile(), None);
        assert_eq!(controller.plan_fingerprint(), locked_fingerprint);
        assert_eq!(controller.history_depth(), locked_history);
    }

    #[test]
    fn json_string_escapes_manifest_values() {
        assert_eq!(
            json_string("quoted \"rule\"\\path\nnext"),
            "\"quoted \\\"rule\\\"\\\\path\\nnext\""
        );
    }

    #[test]
    fn plan_fingerprint_is_stable_and_changes_with_review_state() {
        let mut controller = FirewallController::demo();
        let initial = controller.plan_fingerprint();

        assert_eq!(initial.len(), 16);
        assert!(initial
            .chars()
            .all(|character| character.is_ascii_hexdigit()));
        assert_eq!(initial, controller.plan_fingerprint());

        controller.toggle_logging();

        assert_ne!(initial, controller.plan_fingerprint());
    }

    #[test]
    fn default_policy_toggles_update_plan_and_undo() {
        let mut controller = FirewallController::demo();

        controller.toggle_default_incoming();
        controller.toggle_default_outgoing();

        assert_eq!(controller.default_incoming(), DefaultPolicy::Allow);
        assert_eq!(controller.default_outgoing(), DefaultPolicy::Deny);
        assert!(controller
            .plan()
            .iter()
            .any(|command| command.shell_line() == "ufw default allow incoming"));
        assert!(controller
            .advisor_findings()
            .iter()
            .any(|finding| finding.title == "Default outgoing policy denies traffic"));

        assert!(controller.undo());
        assert_eq!(controller.default_incoming(), DefaultPolicy::Allow);
        assert_eq!(controller.default_outgoing(), DefaultPolicy::Allow);
        assert!(controller.undo());
        assert_eq!(controller.default_incoming(), DefaultPolicy::Deny);
        assert_eq!(controller.default_outgoing(), DefaultPolicy::Allow);
    }

    #[test]
    fn known_port_stats_track_coverage_counts() {
        let mut controller = FirewallController::demo();
        let stats = controller.stats();

        assert_eq!(stats.known_ports, controller.candidates().len());
        assert_eq!(
            stats.known_blocked + stats.known_allowed + stats.known_open,
            stats.known_ports
        );
        assert_eq!(stats.known_blocked, 2);
        assert_eq!(stats.known_allowed, 0);
        assert_eq!(stats.at_risk_open, 11);
        assert_eq!(stats.at_risk_allowed, 0);
        assert_eq!(stats.reviewed_exceptions, 0);
        assert_eq!(stats.unreviewed_at_risk_allowed, 0);
        assert_eq!(stats.readiness_grade(), "C");
        assert_eq!(stats.readiness_label(), "needs hardening");

        controller.apply_advisor_quick_fix();
        let hardened = controller.stats();

        assert_eq!(hardened.at_risk_open, 0);
        assert!(hardened.known_blocked > stats.known_blocked);
        assert!(hardened.coverage_percent > stats.coverage_percent);
        assert_eq!(hardened.readiness_grade(), "A");
        assert_eq!(hardened.readiness_label(), "ready for review");
    }

    #[test]
    fn advisor_summary_counts_findings_by_severity() {
        let mut controller = FirewallController::demo();
        let summary = controller.advisor_summary();

        assert_eq!(summary.total(), controller.advisor_findings().len());
        assert_eq!(summary.critical, 0);
        assert_eq!(summary.warning, controller.stats().at_risk_open);
        assert_eq!(summary.info, 0);
        assert_eq!(summary.good, 0);

        controller.toggle_default_incoming();
        let exposed = controller.advisor_summary();

        assert!(exposed.critical > summary.critical);
        assert_eq!(exposed.total(), controller.advisor_findings().len());
    }

    #[test]
    fn advisor_next_action_tracks_highest_priority_finding() {
        let mut controller = FirewallController::demo();
        let next = controller.advisor_next_action();

        assert_eq!(next.severity, AdvisorSeverity::Warning);
        assert!(next.recommendation.contains("Known Ports"));

        controller.toggle_default_incoming();
        let exposed = controller.advisor_next_action();

        assert_eq!(exposed.severity, AdvisorSeverity::Critical);
        assert_eq!(exposed.title, "Default incoming policy allows traffic");
    }

    #[test]
    fn clear_rules_is_undoable() {
        let mut controller = FirewallController::demo();
        let initial_rules = controller.rules().len();

        assert_eq!(controller.clear_rules(), initial_rules);
        assert!(controller.rules().is_empty());
        assert!(controller.undo());
        assert_eq!(controller.rules().len(), initial_rules);
    }

    #[test]
    fn starter_plan_reset_is_undoable() {
        let mut controller = FirewallController::demo();
        controller.apply_profile(FirewallProfile::Lockdown);
        controller.cycle_backend();
        controller.toggle_default_outgoing();
        let before_reset = controller.plan_fingerprint();
        let before_count = controller.rules().len();

        let restored = controller.reset_to_starter_plan();

        assert_eq!(restored, 3);
        assert_eq!(controller.backend(), Backend::Ufw);
        assert_eq!(controller.default_incoming(), DefaultPolicy::Deny);
        assert_eq!(controller.default_outgoing(), DefaultPolicy::Allow);
        assert!(controller.logging());
        assert_eq!(controller.rules().len(), 3);
        assert_ne!(before_reset, controller.plan_fingerprint());

        assert!(controller.undo());
        assert_eq!(controller.rules().len(), before_count);
        assert_eq!(controller.plan_fingerprint(), before_reset);
    }

    #[test]
    fn candidate_filters_select_expected_ports() {
        let mut controller = FirewallController::demo();

        assert_eq!(
            controller
                .filtered_candidate_indexes(CandidateFilter::All, None, None)
                .len(),
            controller.candidates().len()
        );
        assert!(controller
            .filtered_candidate_indexes(CandidateFilter::Blocked, None, None)
            .iter()
            .any(|index| controller.candidates()[*index].name == "Telnet"));
        assert!(controller
            .filtered_candidate_indexes(CandidateFilter::AtRisk, None, None)
            .iter()
            .any(|index| controller.candidates()[*index].name == "SSH"));
        let named = controller.filtered_candidate_indexes(CandidateFilter::Named, None, None);
        assert!(named
            .iter()
            .any(|index| controller.candidates()[*index].name == "Postgres"));
        assert!(named
            .iter()
            .any(|index| controller.candidates()[*index].name == "Docker API"));
        assert!(named
            .iter()
            .any(|index| controller.candidates()[*index].aliases.contains(&"nginx")));
        assert!(controller
            .filtered_candidate_indexes(CandidateFilter::Open, None, None)
            .iter()
            .any(|index| controller.candidates()[*index].name == "HTTP"));

        controller.allow_candidate_named("HTTPS");
        assert!(controller
            .filtered_candidate_indexes(CandidateFilter::Allowed, None, None)
            .iter()
            .any(|index| controller.candidates()[*index].name == "HTTPS"));
    }

    #[test]
    fn group_filters_select_catalog_groups() {
        let controller = FirewallController::demo();
        let groups = controller.candidate_groups();

        assert_eq!(groups.first(), Some(&"admin"));
        assert!(groups.contains(&"web"));
        assert!(groups.contains(&"data"));

        let web_candidates =
            controller.filtered_candidate_indexes(CandidateFilter::All, Some("web"), None);
        assert_eq!(web_candidates.len(), 2);
        assert!(web_candidates
            .iter()
            .all(|index| controller.candidates()[*index].group == "web"));
    }

    #[test]
    fn risk_filters_select_catalog_risks() {
        let controller = FirewallController::demo();
        let risks = controller.candidate_risks();

        assert_eq!(risks.first(), Some(&"admin"));
        assert!(risks.contains(&"high"));
        assert!(risks.contains(&"data"));

        let data_candidates =
            controller.filtered_candidate_indexes(CandidateFilter::All, None, Some("data"));
        assert_eq!(data_candidates.len(), 5);
        assert!(data_candidates
            .iter()
            .all(|index| controller.candidates()[*index].risk == "data"));

        let high_file_candidates =
            controller.filtered_candidate_indexes(CandidateFilter::All, Some("file"), Some("high"));
        assert_eq!(high_file_candidates.len(), 2);
        assert!(high_file_candidates
            .iter()
            .all(|index| controller.candidates()[*index].group == "file"
                && controller.candidates()[*index].risk == "high"));

        let high_web_candidates =
            controller.filtered_candidate_indexes(CandidateFilter::All, Some("web"), Some("high"));
        assert!(high_web_candidates.is_empty());
    }

    #[test]
    fn group_coverage_summaries_count_known_port_statuses() {
        let mut controller = FirewallController::demo();
        let coverage = controller.group_coverage_summaries();

        let admin = group_coverage_named(&coverage, "admin");
        assert_eq!(admin.total, 3);
        assert_eq!(admin.blocked, 0);
        assert_eq!(admin.open, 3);
        assert_eq!(admin.at_risk_open, 3);

        let file = group_coverage_named(&coverage, "file");
        assert_eq!(file.total, 2);
        assert_eq!(file.blocked, 1);
        assert_eq!(file.open, 1);
        assert_eq!(file.at_risk_open, 1);

        controller.apply_advisor_quick_fix();
        let hardened = controller.group_coverage_summaries();
        assert!(hardened.iter().all(|group| group.at_risk_open == 0));
    }

    #[test]
    fn risk_coverage_summaries_count_known_port_statuses() {
        let mut controller = FirewallController::demo();
        let coverage = controller.risk_coverage_summaries();

        assert_eq!(coverage.len(), 7);

        let high = risk_coverage_named(&coverage, "high");
        assert_eq!(high.total, 4);
        assert_eq!(high.blocked, 2);
        assert_eq!(high.allowed, 0);
        assert_eq!(high.open, 2);

        let data = risk_coverage_named(&coverage, "data");
        assert_eq!(data.total, 5);
        assert_eq!(data.blocked, 0);
        assert_eq!(data.allowed, 0);
        assert_eq!(data.open, 5);

        controller.apply_advisor_quick_fix();
        let hardened = controller.risk_coverage_summaries();
        assert_eq!(risk_coverage_named(&hardened, "high").open, 0);
        assert_eq!(risk_coverage_named(&hardened, "data").open, 0);
        assert_eq!(risk_coverage_named(&hardened, "admin").open, 0);
        assert_eq!(risk_coverage_named(&hardened, "web").open, 2);
    }

    #[test]
    fn block_candidate_risk_blocks_matching_risk_and_is_undoable() {
        let mut controller = FirewallController::demo();
        let mysql = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == "MySQL")
            .expect("known test candidate");

        let staged = controller.block_candidate_risk(mysql);

        assert_eq!(staged, 5);
        let coverage = controller.risk_coverage_summaries();
        let data = risk_coverage_named(&coverage, "data");
        assert_eq!(data.blocked, 5);
        assert_eq!(data.open, 0);
        assert_eq!(risk_coverage_named(&coverage, "web").open, 2);
        assert!(controller
            .audit_log()
            .iter()
            .any(|message| message == "Blocked 5 known port(s) in risk class data."));

        assert!(controller.undo());
        let restored = controller.risk_coverage_summaries();
        assert_eq!(risk_coverage_named(&restored, "data").blocked, 0);
        assert_eq!(risk_coverage_named(&restored, "data").open, 5);
    }

    #[test]
    fn block_candidate_indexes_blocks_active_filter_and_is_undoable() {
        let mut controller = FirewallController::demo();
        let filtered = controller.filtered_candidate_indexes(
            CandidateFilter::Open,
            Some("data"),
            Some("data"),
        );

        assert_eq!(filtered.len(), 5);

        let staged = controller.block_candidate_indexes(&filtered);

        assert_eq!(staged, 5);
        let coverage = controller.risk_coverage_summaries();
        assert_eq!(risk_coverage_named(&coverage, "data").blocked, 5);
        assert_eq!(risk_coverage_named(&coverage, "data").open, 0);
        assert_eq!(risk_coverage_named(&coverage, "web").open, 2);
        assert!(controller
            .audit_log()
            .iter()
            .any(|message| message == "Blocked 5 known port(s) from active filter."));

        assert!(controller.undo());
        let restored = controller.risk_coverage_summaries();
        assert_eq!(risk_coverage_named(&restored, "data").blocked, 0);
        assert_eq!(risk_coverage_named(&restored, "data").open, 5);
    }

    #[test]
    fn allow_candidate_indexes_allows_active_filter_and_is_undoable() {
        let mut controller = FirewallController::demo();
        let filtered =
            controller.filtered_candidate_indexes(CandidateFilter::Open, Some("web"), Some("web"));

        assert_eq!(filtered.len(), 2);

        let staged = controller.allow_candidate_indexes(&filtered);

        assert_eq!(staged, 2);
        let coverage = controller.risk_coverage_summaries();
        let web = risk_coverage_named(&coverage, "web");
        assert_eq!(web.allowed, 2);
        assert_eq!(web.open, 0);
        assert_eq!(
            candidate_status_named(&controller, "HTTP"),
            CandidateStatus::Allowed
        );
        assert_eq!(
            candidate_status_named(&controller, "HTTPS"),
            CandidateStatus::Allowed
        );
        assert!(controller
            .audit_log()
            .iter()
            .any(|message| message == "Allowed 2 known port(s) from active filter."));

        assert!(controller.undo());
        let restored = controller.risk_coverage_summaries();
        assert_eq!(risk_coverage_named(&restored, "web").allowed, 0);
        assert_eq!(risk_coverage_named(&restored, "web").open, 2);
    }

    #[test]
    fn developer_profile_blocks_dev_risk_dependencies_without_admin_shells() {
        let mut controller = FirewallController::demo();
        controller.clear_rules();

        let staged = controller.apply_profile(FirewallProfile::Developer);

        assert!(staged > 0);
        assert_eq!(
            candidate_status_named(&controller, "Docker API"),
            CandidateStatus::Blocked
        );
        assert_eq!(
            candidate_status_named(&controller, "MySQL"),
            CandidateStatus::Blocked
        );
        assert_eq!(
            candidate_status_named(&controller, "SSH"),
            CandidateStatus::Open
        );
        assert_eq!(
            candidate_status_named(&controller, "Jupyter"),
            CandidateStatus::Open
        );
    }

    #[test]
    fn advisor_quick_fix_restores_default_deny_incoming() {
        let mut controller = FirewallController::demo();
        controller.toggle_default_incoming();

        let changes = controller.apply_advisor_quick_fix();

        assert!(changes > 0);
        assert_eq!(controller.default_incoming(), DefaultPolicy::Deny);
    }

    fn candidate_status_named(controller: &FirewallController, name: &str) -> CandidateStatus {
        let index = controller
            .candidates()
            .iter()
            .position(|candidate| candidate.name == name)
            .expect("known test candidate");
        controller
            .candidate_status(index)
            .expect("candidate status for known index")
    }

    fn group_coverage_named<'a>(
        coverage: &'a [GroupCoverageSummary],
        group: &str,
    ) -> &'a GroupCoverageSummary {
        coverage
            .iter()
            .find(|summary| summary.group == group)
            .expect("known test group")
    }

    fn risk_coverage_named<'a>(
        coverage: &'a [RiskCoverageSummary],
        risk: &str,
    ) -> &'a RiskCoverageSummary {
        coverage
            .iter()
            .find(|summary| summary.risk == risk)
            .expect("known test risk")
    }
}
