use std::time::{Duration, Instant};
use crate::config::{Config, OutputFormat};
use crate::diagnostics;
use super::action::{self, DiagnosticKey};
use super::session::{FinalOutcome, Reporter, RestoreRegistry, Session, DEFAULT_ITERATION_DELAY};
use super::triage::{
actionable_failures, build_plan, hard_block_detected, requires_confirmation,
requires_high_risk_consent, HardBlock, MAX_ITERATIONS,
};
const DRAIN_CAP: Duration = Duration::from_secs(90);
pub async fn run(
config: &Config,
session: &mut Session,
restore: &RestoreRegistry,
) -> FinalOutcome {
let interactive = is_interactive(config);
let reporter = Reporter::new(config);
if interactive {
reporter.header();
}
let baseline = diagnostics::run_all(config).await;
session.record_baseline(baseline.clone());
let mut current = baseline;
if interactive {
let initial_failures = actionable_failures(¤t);
reporter.baseline_summary(initial_failures.len());
}
for iteration in 1..=MAX_ITERATIONS {
if session.wall_clock_exhausted() {
let remaining: Vec<DiagnosticKey> = actionable_failures(¤t).into_iter().collect();
let outcome = FinalOutcome::Timeout(remaining);
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
return outcome;
}
let failures = actionable_failures(¤t);
if failures.is_empty() {
let outcome = FinalOutcome::Fixed;
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
return outcome;
}
if let Some(block) = hard_block_detected(¤t) {
let outcome = FinalOutcome::HardBlock(block);
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
return outcome;
}
if interactive {
reporter.iteration_header(iteration);
}
let registry = action::all_actions();
let plan = build_plan(
&failures,
&session.attempts,
&session.effectiveness,
®istry,
);
if plan.is_empty() {
let remaining: Vec<DiagnosticKey> = failures.into_iter().collect();
let outcome = FinalOutcome::Exhausted(remaining);
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
return outcome;
}
let mut user_declined_confirmation = false;
let mut skipped_for_confirmation = false;
let mut ran_action = false;
for action in &plan {
if session.wall_clock_exhausted() {
break;
}
if requires_confirmation(action, config.auto_confirm_medium_risk) {
if !interactive {
session.record_action(
iteration,
action,
super::action::ActionOutcome::fail(
"Skipped: requires confirmation. Re-run `nd300 fix` in a terminal or use `--yes` for medium-risk actions.",
),
Duration::from_millis(0),
false,
true,
);
skipped_for_confirmation = true;
continue;
}
let approved = if requires_high_risk_consent(action) {
reporter.high_risk_prompt(action)
} else {
reporter.confirmation_prompt(action)
};
if !approved {
reporter.confirmation_declined(action);
session.record_action(
iteration,
action,
super::action::ActionOutcome::fail("User declined the prompt."),
Duration::from_millis(0),
true,
false,
);
user_declined_confirmation = true;
break;
}
}
if interactive {
reporter.announce_action(action);
}
let started = Instant::now();
let outcome = action.apply(config, restore).await;
let duration = started.elapsed();
if interactive {
reporter.finish_action(&outcome, duration);
}
let fatal_env_change = outcome.fatal_environment_change;
session.record_action(iteration, action, outcome, duration, false, false);
ran_action = true;
if action.stabilization > Duration::from_millis(0) {
tokio::time::sleep(action.stabilization).await;
}
if fatal_env_change {
break;
}
}
if user_declined_confirmation || (skipped_for_confirmation && !ran_action) {
let remaining: Vec<DiagnosticKey> = actionable_failures(¤t).into_iter().collect();
let outcome = FinalOutcome::UserDeclined(remaining);
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
return outcome;
}
tokio::time::sleep(DEFAULT_ITERATION_DELAY).await;
let prior_failures = actionable_failures(¤t);
current = diagnostics::run_all(config).await;
let now_failures = actionable_failures(¤t);
session.record_iteration(iteration, current.clone());
session.update_effectiveness(iteration, &prior_failures, &now_failures);
}
let remaining_failures = actionable_failures(¤t);
let remaining: Vec<DiagnosticKey> = remaining_failures.iter().copied().collect();
let outcome = if remaining_failures.is_empty() {
FinalOutcome::Fixed
} else {
let baseline_failures = session
.baseline
.as_ref()
.map(actionable_failures)
.unwrap_or_default();
let any_progress = baseline_failures
.difference(&remaining_failures)
.next()
.is_some();
if any_progress {
FinalOutcome::Partial(remaining)
} else {
FinalOutcome::Exhausted(remaining)
}
};
session.final_outcome = Some(outcome.clone());
if interactive {
reporter.final_verdict(&outcome, None);
}
outcome
}
fn is_interactive(config: &Config) -> bool {
use std::io::IsTerminal;
config.format != OutputFormat::Json && std::io::stdin().is_terminal()
}
pub async fn run_and_finalize(config: &Config) -> i32 {
use futures_util::FutureExt;
if !crate::platform::is_elevated() {
let outcome = FinalOutcome::PreflightFailed(
"The fix flow requires elevated privileges. Run with sudo (Unix) or as Administrator (Windows).".to_string(),
);
if config.format == OutputFormat::Json {
print_json_outcome(&Session::new(), &outcome, None, &[]);
} else {
let reporter = Reporter::new(config);
reporter.final_verdict(&outcome, None);
}
return outcome.exit_code();
}
let is_json = config.format == OutputFormat::Json;
let mut session = Session::new();
let restore = RestoreRegistry::new();
let loop_result = {
let fut = std::panic::AssertUnwindSafe(run(config, &mut session, &restore)).catch_unwind();
tokio::select! {
biased;
_ = tokio::signal::ctrl_c() => None,
r = fut => Some(r),
}
};
let (outcome, panicked) = match loop_result {
Some(Ok(outcome)) => (outcome, false),
Some(Err(_panic)) => (
FinalOutcome::Interrupted(remaining_after_interrupt(&session)),
true,
),
None => {
if !is_json {
println!();
println!(" Interrupted — cleaning up and restoring network state...");
}
(
FinalOutcome::Interrupted(remaining_after_interrupt(&session)),
false,
)
}
};
if panicked && !is_json {
println!();
println!(
" A fatal internal error occurred mid-fix — restoring network state before exiting..."
);
}
let drain_failures = match tokio::time::timeout(DRAIN_CAP, restore.drain()).await {
Ok(failures) => failures,
Err(_) => vec![format!(
"Network-state cleanup did not finish within {}s; some changes may not have been restored.",
DRAIN_CAP.as_secs()
)],
};
if matches!(outcome, FinalOutcome::Interrupted(_)) && !is_json {
let reporter = Reporter::new(config);
reporter.final_verdict(&outcome, None);
}
if !drain_failures.is_empty() && !is_json {
println!();
println!(
" {}",
crate::render::color::yellow("Manual recovery needed:", config)
);
for f in &drain_failures {
println!(" • {}", crate::render::color::yellow(f, config));
}
}
session.final_outcome = Some(outcome.clone());
let report_path =
super::report::save_session_report_with_recovery(&session, &outcome, &drain_failures);
if is_json {
print_json_outcome(&session, &outcome, report_path.as_deref(), &drain_failures);
} else if let Some(path) = &report_path {
println!(
" {} {}",
crate::render::color::dim("Saved report:", config),
crate::render::color::dim(&path.display().to_string(), config),
);
}
let code = outcome.exit_code();
if panicked {
std::process::exit(101);
}
code
}
fn remaining_after_interrupt(session: &Session) -> Vec<DiagnosticKey> {
session
.snapshots
.last()
.map(|s| actionable_failures(&s.results).into_iter().collect())
.unwrap_or_default()
}
fn print_json_outcome(
session: &Session,
outcome: &FinalOutcome,
report_path: Option<&std::path::Path>,
recovery_needed: &[String],
) {
use serde_json::json;
let outcome_label = match outcome {
FinalOutcome::Fixed => "fixed",
FinalOutcome::Partial(_) => "partial",
FinalOutcome::Exhausted(_) => "exhausted",
FinalOutcome::HardBlock(_) => "hard_block",
FinalOutcome::Timeout(_) => "timeout",
FinalOutcome::UserDeclined(_) => "user_declined",
FinalOutcome::PreflightFailed(_) => "preflight_failed",
FinalOutcome::Interrupted(_) => "interrupted",
};
let remaining: Vec<&str> = match outcome {
FinalOutcome::Partial(rs)
| FinalOutcome::Exhausted(rs)
| FinalOutcome::Timeout(rs)
| FinalOutcome::UserDeclined(rs)
| FinalOutcome::Interrupted(rs) => rs.iter().map(|k| diagnostic_key_str(*k)).collect(),
_ => Vec::new(),
};
let actions_json: Vec<_> = session
.action_log
.iter()
.map(|r| {
json!({
"iteration": r.iteration,
"action": format!("{:?}", r.action_id),
"label": r.label,
"ok": r.outcome.ok,
"message": r.outcome.message,
"duration_ms": r.duration.as_millis() as u64,
"user_declined": r.user_declined,
"skipped_no_interaction": r.skipped_no_interaction,
})
})
.collect();
let value = json!({
"action": "fix",
"outcome": outcome_label,
"exit_code": outcome.exit_code(),
"iterations": session.snapshots.len().saturating_sub(1),
"remaining_failures": remaining,
"applied_actions": actions_json,
"elapsed_seconds": session.elapsed().as_secs(),
"report_path": report_path.map(|p| p.display().to_string()),
"interrupted": matches!(outcome, FinalOutcome::Interrupted(_)),
"manual_recovery_needed": recovery_needed,
"preflight_error": match outcome {
FinalOutcome::PreflightFailed(s) => Some(s.clone()),
_ => None,
},
"hard_block": match outcome {
FinalOutcome::HardBlock(b) => Some(hard_block_str(b).to_string()),
_ => None,
},
});
println!(
"{}",
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string())
);
}
fn diagnostic_key_str(k: DiagnosticKey) -> &'static str {
match k {
DiagnosticKey::Adapters => "adapters",
DiagnosticKey::Interfaces => "interfaces",
DiagnosticKey::Gateway => "gateway",
DiagnosticKey::Dns => "dns",
DiagnosticKey::PublicIp => "public_ip",
DiagnosticKey::Latency => "latency",
DiagnosticKey::Ports => "ports",
DiagnosticKey::Speed => "speed",
}
}
fn hard_block_str(b: &HardBlock) -> &'static str {
match b {
HardBlock::CaptivePortal => "captive_portal",
HardBlock::NoPhysicalLink => "no_physical_link",
HardBlock::IspOutage => "isp_outage",
HardBlock::EnterpriseVpnActive(_) => "enterprise_vpn_active",
}
}