use clap::{self, Parser};
use console::style;
use simple_logger;
use std::{
    collections::{HashMap, HashSet},
    future::Future,
    path::PathBuf,
    pin::Pin,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc, Mutex,
    },
    time::{Duration, SystemTime},
};

use hen::{
    automation, benchmark, collection,
    error::{print_error, HenError, HenErrorKind, HenResult},
    parser,
    prompt_generator::{collection_prompt, request_prompt, scan_directory},
    report::{self, BodyReportOptions},
    request,
};

// hen -> prompt for a collection in CWD
// hen <dir> -> prompt for a collections in a specific directory
// hen <file> -> prompt to run a specific request
// hen <file> --all -> run all requests
#[derive(clap::Args, Debug, Clone, Default)]
struct RunArgs {
    path: Option<String>,
    selector: Option<String>,

    #[arg(long)]
    export: bool,

    #[arg(long)]
    benchmark: Option<usize>,

    #[arg(short = 'v', long)]
    verbose: bool,

    #[arg(long = "input", value_parser = parse_input_kv)]
    inputs: Vec<(String, String)>,

    #[arg(long, help = "Execute independent requests concurrently")]
    parallel: bool,

    #[arg(
        long = "max-concurrency",
        default_value_t = 0,
        help = "Limit concurrent requests when running with --parallel"
    )]
    max_concurrency: usize,

    #[arg(
        long = "continue-on-error",
        help = "Do not stop execution after the first request failure"
    )]
    continue_on_error: bool,

    #[arg(
        long,
        help = "Fail instead of prompting for collection or request selection"
    )]
    non_interactive: bool,

    #[arg(
        long,
        value_enum,
        default_value_t = OutputFormat::Text,
        help = "Render output as text, json, ndjson, or junit"
    )]
    output: OutputFormat,
}

#[derive(clap::Args, Debug, Clone)]
struct VerifyArgs {
    path: String,

    #[arg(
        long,
        value_enum,
        default_value_t = OutputFormat::Text,
        help = "Render output as text, json, ndjson, or junit"
    )]
    output: OutputFormat,
}

#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
enum OutputFormat {
    #[default]
    Text,
    Json,
    Ndjson,
    Junit,
}

impl OutputFormat {
    fn is_text(self) -> bool {
        matches!(self, Self::Text)
    }

    fn as_str(self) -> &'static str {
        match self {
            Self::Text => "text",
            Self::Json => "json",
            Self::Ndjson => "ndjson",
            Self::Junit => "junit",
        }
    }
}

#[derive(clap::Subcommand, Debug, Clone)]
enum Command {
    /// Execute a collection or request
    Run(RunArgs),
    /// Parse and validate a collection without executing shell substitutions or HTTP requests
    Verify(VerifyArgs),
}

enum Invocation {
    Run(RunArgs),
    Verify(VerifyArgs),
}

#[derive(clap::Parser, Debug)]
#[command(name = "hen")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "Command line API client.")]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,

    #[command(flatten)]
    run: RunArgs,
}

impl Cli {
    fn into_invocation(self) -> Invocation {
        match self.command {
            Some(Command::Run(args)) => Invocation::Run(args),
            Some(Command::Verify(args)) => Invocation::Verify(args),
            None => Invocation::Run(self.run),
        }
    }
}

struct PromptSession {
    previous_mode: parser::context::PromptMode,
}

impl PromptSession {
    fn configure(args: &RunArgs) -> Self {
        let previous_mode = parser::context::prompt_mode();
        let prompt_inputs: HashMap<String, String> = args.inputs.iter().cloned().collect();
        parser::context::set_prompt_mode(parser::context::PromptMode::NonInteractive);
        parser::context::set_prompt_inputs(prompt_inputs);

        Self { previous_mode }
    }
}

impl Drop for PromptSession {
    fn drop(&mut self) {
        parser::context::set_prompt_inputs(HashMap::new());
        parser::context::set_prompt_mode(self.previous_mode);
    }
}

const PREVIEW_MAX_LINES: usize = 12;
const PREVIEW_MAX_CHARS: usize = 800;
const STRUCTURED_BODY_MAX_CHARS: usize = 4_000;

struct CommandOutcome {
    exit_code: i32,
}

impl CommandOutcome {
    fn success() -> Self {
        Self { exit_code: 0 }
    }

    fn with_exit_code(exit_code: i32) -> Self {
        Self { exit_code }
    }
}

#[derive(Debug, Clone)]
struct ExecutionState {
    records: Vec<request::ExecutionRecord>,
    failures: Vec<request::RequestFailure>,
    execution_failed: bool,
    interrupted: Option<request::InterruptSignal>,
}

#[derive(Debug, Default, Clone)]
struct CollectedExecution {
    records: Arc<Mutex<Vec<request::ExecutionRecord>>>,
    failures: Arc<Mutex<Vec<request::RequestFailure>>>,
}

impl CollectedExecution {
    fn snapshot(&self) -> (Vec<request::ExecutionRecord>, Vec<request::RequestFailure>) {
        let mut records = self.records.lock().unwrap().clone();
        let mut failures = self.failures.lock().unwrap().clone();
        records.sort_by_key(|record| record.index);
        failures.sort_by_key(|failure| failure.index().unwrap_or(usize::MAX));
        (records, failures)
    }
}

type InterruptFuture = Pin<Box<dyn Future<Output = HenResult<request::InterruptSignal>> + Send>>;

fn tracking_observer(
    downstream: Option<request::ExecutionObserver>,
) -> (request::ExecutionObserver, CollectedExecution) {
    let collected = CollectedExecution::default();
    let records = Arc::clone(&collected.records);
    let failures = Arc::clone(&collected.failures);

    let observer: request::ExecutionObserver = Arc::new(move |event| {
        match &event {
            request::ExecutionEvent::RequestCompleted { record } => {
                records.lock().unwrap().push(record.clone());
            }
            request::ExecutionEvent::RequestFailed { failure } => {
                failures.lock().unwrap().push(failure.clone());
            }
            request::ExecutionEvent::AssertionPassed { .. } => {}
        }

        if let Some(callback) = downstream.as_ref() {
            callback(event);
        }
    });

    (observer, collected)
}

fn interrupt_listener() -> HenResult<InterruptFuture> {
    #[cfg(unix)]
    {
        let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
            .map_err(|err| {
                HenError::new(HenErrorKind::Execution, "Failed to install SIGTERM handler")
                    .with_detail(err.to_string())
            })?;

        Ok(Box::pin(async move {
            tokio::select! {
                result = tokio::signal::ctrl_c() => result
                    .map(|_| request::InterruptSignal::Sigint)
                    .map_err(|err| {
                        HenError::new(HenErrorKind::Execution, "Failed while waiting for SIGINT")
                            .with_detail(err.to_string())
                    }),
                signal = terminate.recv() => match signal {
                    Some(()) => Ok(request::InterruptSignal::Sigterm),
                    None => Err(HenError::new(
                        HenErrorKind::Execution,
                        "SIGTERM listener closed unexpectedly",
                    )),
                },
            }
        }))
    }

    #[cfg(not(unix))]
    {
        Ok(Box::pin(async move {
            tokio::signal::ctrl_c()
                .await
                .map(|_| request::InterruptSignal::Sigint)
                .map_err(|err| {
                    HenError::new(HenErrorKind::Execution, "Failed while waiting for Ctrl-C")
                        .with_detail(err.to_string())
                })
        }))
    }
}

async fn execute_plan_with_interrupt(
    requests: &[request::Request],
    plan: &[usize],
    options: request::ExecutionOptions,
    observer: Option<request::ExecutionObserver>,
) -> HenResult<ExecutionState> {
    let (observer, collected) = tracking_observer(observer);
    let execution = request::execute_request_plan_with_observer(
        requests,
        plan,
        options,
        Some(observer),
    );
    let interrupt = interrupt_listener()?;

    tokio::pin!(execution);
    tokio::pin!(interrupt);

    tokio::select! {
        result = &mut execution => {
            let (records, failures, execution_failed) = match result {
                Ok(records) => (records, Vec::new(), false),
                Err(err) => {
                    let (failures, completed) = err.into_parts();
                    (completed, failures, true)
                }
            };

            Ok(ExecutionState {
                records,
                failures,
                execution_failed,
                interrupted: None,
            })
        }
        result = &mut interrupt => {
            let signal = result?;
            let (records, failures) = collected.snapshot();

            Ok(ExecutionState {
                records,
                failures,
                execution_failed: true,
                interrupted: Some(signal),
            })
        }
    }
}

#[tokio::main]
async fn main() {
    let result = match Cli::parse().into_invocation() {
        Invocation::Run(args) => run(args).await,
        Invocation::Verify(args) => verify(args),
    };

    match result {
        Ok(outcome) => {
            if outcome.exit_code != 0 {
                std::process::exit(outcome.exit_code);
            }
        }
        Err(err) => {
            print_error(&err);
            std::process::exit(err.exit_code());
        }
    }
}

async fn run(args: RunArgs) -> HenResult<CommandOutcome> {
    if args.output.is_text() {
        run_text(args).await?;
        return Ok(CommandOutcome::success());
    }

    Ok(run_machine_output(args).await)
}

async fn run_text(args: RunArgs) -> HenResult<()> {
    let _prompt_session = PromptSession::configure(&args);

    if args.verbose {
        simple_logger::init_with_level(log::Level::Debug).map_err(|err| {
            HenError::new(HenErrorKind::Cli, "Failed to initialize logger")
                .with_detail(err.to_string())
        })?;
    }

    log::debug!("Starting hen with args {:?}", args);

    let cwd = std::env::current_dir().map_err(|err| {
        HenError::new(HenErrorKind::Io, "Failed to determine current directory")
            .with_detail(err.to_string())
    })?;

    let collection = load_collection(&args, cwd.clone()).map_err(|err| match err.kind() {
        HenErrorKind::Parse | HenErrorKind::Io => err,
        _ => err.with_detail("While loading collection"),
    })?;

    log::debug!("PARSED COLLECTION\n{:#?}", collection);

    let planner = request::RequestPlanner::new(&collection.requests).map_err(|err| {
        HenError::new(
            HenErrorKind::Planner,
            "Failed to build request dependency graph",
        )
        .with_detail(err.to_string())
    })?;

    let (plan, display_targets, primary_target) =
        resolve_execution_plan(&collection, &planner, &args)?;

    automation::validate_plan_prompt_inputs(&collection.requests, &plan)?;

    if args.export {
        for idx in &display_targets {
            if let Some(request) = collection.requests.get(*idx) {
                println!("{}", request.as_curl());
            }
        }
        return Ok(());
    }

    if let Some(count) = args.benchmark {
        if let Some(target) = primary_target {
            benchmark::benchmark(&collection.requests, &planner, target, count)
                .await
                .map_err(|err| {
                    HenError::new(HenErrorKind::Benchmark, "Benchmark execution failed")
                        .with_detail(err.to_string())
                })?;
        } else {
            return Err(HenError::new(
                HenErrorKind::Benchmark,
                "Benchmark requires selecting a specific request",
            )
            .with_exit_code(2));
        }

        return Ok(());
    }

    let parallel_enabled = args.parallel || args.max_concurrency > 0;
    let max_concurrency = if args.max_concurrency == 0 {
        None
    } else {
        Some(args.max_concurrency)
    };

    let execution_options = request::ExecutionOptions {
        parallel: parallel_enabled,
        max_concurrency,
        continue_on_error: args.continue_on_error,
    };

    log::debug!(
        "Execution options: parallel={}, max_concurrency={:?}, continue_on_error={}",
        execution_options.parallel,
        execution_options.max_concurrency,
        execution_options.continue_on_error
    );

    let display_set: HashSet<usize> = display_targets.iter().copied().collect();
    let display_set_shared = Arc::new(display_set.clone());
    let verbose = args.verbose;

    #[derive(Default)]
    struct StreamPrinterState {
        needs_separator: bool,
    }

    let printer_state = Arc::new(Mutex::new(StreamPrinterState::default()));
    let events_emitted = Arc::new(AtomicBool::new(false));

    let observer: request::ExecutionObserver = Arc::new({
        let display_set = Arc::clone(&display_set_shared);
        let printer_state = Arc::clone(&printer_state);
        let events_emitted = Arc::clone(&events_emitted);
        move |event: request::ExecutionEvent| match event {
            request::ExecutionEvent::RequestCompleted { record } => {
                let mut state = printer_state.lock().unwrap();
                events_emitted.store(true, Ordering::Relaxed);
                if state.needs_separator {
                    println!();
                } else {
                    state.needs_separator = true;
                }
                print_status_line(&record);
                if display_set.contains(&record.index) {
                    print_body_preview(&record, verbose);
                }
            }
            request::ExecutionEvent::RequestFailed { failure } => {
                let mut state = printer_state.lock().unwrap();
                events_emitted.store(true, Ordering::Relaxed);
                if state.needs_separator {
                    println!();
                } else {
                    state.needs_separator = true;
                }
                print_failure_line(&failure);
            }
            request::ExecutionEvent::AssertionPassed { request, assertion } => {
                let _guard = printer_state.lock().unwrap();
                events_emitted.store(true, Ordering::Relaxed);
                println!("✅ [{}] [{}]", request, assertion);
            }
        }
    });

    let execution_result = execute_plan_with_interrupt(
        &collection.requests,
        &plan,
        execution_options,
        Some(observer),
    )
    .await;

    let execution_result = execution_result?;
    let records = execution_result.records;
    let failures = execution_result.failures;
    let execution_failed = execution_result.execution_failed;
    let interrupted = execution_result.interrupted;

    if events_emitted.load(Ordering::Relaxed) {
        println!();
    } else {
        if !records.is_empty() {
            for (idx, record) in records.iter().enumerate() {
                if idx > 0 {
                    println!();
                }
                print_status_line(record);
                if display_set.contains(&record.index) {
                    print_body_preview(record, verbose);
                }
            }
        }

        if !failures.is_empty() {
            if !records.is_empty() {
                println!();
            }
            for failure in &failures {
                print_failure_line(failure);
            }
        }

        if !records.is_empty() || !failures.is_empty() {
            println!();
        }
    }

    print_summary(&records, &failures, interrupted, plan.len());

    if let Some(signal) = interrupted {
        return Err(
            HenError::new(
                HenErrorKind::Execution,
                format!("Execution interrupted by {}", signal.as_str()),
            )
            .with_exit_code(signal.exit_code()),
        );
    }

    if !failures.is_empty() {
        let failure_details = failures
            .iter()
            .map(|failure| failure.to_string())
            .collect::<Vec<_>>()
            .join("\n");

        return Err(
            HenError::new(HenErrorKind::Execution, "One or more requests failed")
                .with_detail(failure_details),
        );
    }

    if execution_failed {
        return Err(HenError::new(
            HenErrorKind::Execution,
            "Execution terminated before completing all requests",
        ));
    }

    Ok(())
}

fn verify(args: VerifyArgs) -> HenResult<CommandOutcome> {
    if args.output.is_text() {
        verify_text(args)?;
        return Ok(CommandOutcome::success());
    }

    Ok(verify_machine_output(args))
}

fn verify_text(args: VerifyArgs) -> HenResult<()> {
    let result = automation::verify_path(PathBuf::from(args.path))?;
    print_verification_result(&result);
    Ok(())
}

async fn run_machine_output(args: RunArgs) -> CommandOutcome {
    let output = args.output;
    let suite_name = args.path.as_deref().unwrap_or("hen run").to_string();

    match build_machine_run_outcome(&args).await {
        Ok(outcome) => {
            print_run_report(output, &outcome);

            let exit_code = if let Some(signal) = outcome.interrupted {
                signal.exit_code()
            } else if outcome.failures.is_empty() && !outcome.execution_failed {
                0
            } else {
                1
            };

            CommandOutcome::with_exit_code(exit_code)
        }
        Err(err) => {
            print_machine_error(output, &suite_name, "run", &err);
            CommandOutcome::with_exit_code(err.exit_code())
        }
    }
}

fn verify_machine_output(args: VerifyArgs) -> CommandOutcome {
    let output = args.output;
    let suite_name = args.path.clone();

    match automation::verify_path(PathBuf::from(args.path)) {
        Ok(result) => {
            print_verification_report(output, &result);
            CommandOutcome::success()
        }
        Err(err) => {
            print_machine_error(output, &suite_name, "verify", &err);
            CommandOutcome::with_exit_code(err.exit_code())
        }
    }
}

async fn build_machine_run_outcome(args: &RunArgs) -> HenResult<automation::RunOutcome> {
    if args.export {
        return Err(HenError::new(
            HenErrorKind::Input,
            format!(
                "--output {} cannot be combined with --export",
                args.output.as_str()
            ),
        )
        .with_exit_code(2));
    }

    if args.benchmark.is_some() {
        return Err(HenError::new(
            HenErrorKind::Input,
            format!(
                "--output {} cannot be combined with --benchmark",
                args.output.as_str()
            ),
        )
        .with_exit_code(2));
    }

    let path = match args.path.as_ref() {
        Some(path) => PathBuf::from(path),
        None => std::env::current_dir().map_err(|err| {
            HenError::new(HenErrorKind::Io, "Failed to determine current directory")
                .with_detail(err.to_string())
        })?,
    };

    let parallel_enabled = args.parallel || args.max_concurrency > 0;
    let max_concurrency = if args.max_concurrency == 0 {
        None
    } else {
        Some(args.max_concurrency)
    };

    let execution_options = request::ExecutionOptions {
        parallel: parallel_enabled,
        max_concurrency,
        continue_on_error: args.continue_on_error,
    };

    let prepared = automation::prepare_run_path(automation::RunRequest {
        path,
        selector: args.selector.clone(),
        inputs: args.inputs.iter().cloned().collect(),
        execution_options: execution_options.clone(),
    })
    .await?;

    let execution = execute_plan_with_interrupt(
        &prepared.requests,
        &prepared.plan,
        execution_options,
        None,
    )
    .await?;

    Ok(automation::finish_run(
        prepared,
        execution.records,
        execution.failures,
        execution.execution_failed,
        execution.interrupted,
    ))
}

fn print_run_report(output: OutputFormat, outcome: &automation::RunOutcome) {
    match output {
        OutputFormat::Text => unreachable!("text output should be handled separately"),
        OutputFormat::Json => print_json(&report::run_outcome_json(
            outcome,
            BodyReportOptions {
                include_body: true,
                max_body_chars: Some(STRUCTURED_BODY_MAX_CHARS),
            },
        )),
        OutputFormat::Ndjson => println!(
            "{}",
            report::run_outcome_ndjson(
                outcome,
                BodyReportOptions {
                    include_body: true,
                    max_body_chars: Some(STRUCTURED_BODY_MAX_CHARS),
                },
            )
        ),
        OutputFormat::Junit => println!("{}", report::run_outcome_junit(outcome)),
    }
}

fn print_verification_report(output: OutputFormat, result: &automation::VerificationResult) {
    match output {
        OutputFormat::Text => unreachable!("text output should be handled separately"),
        OutputFormat::Json => print_json(&report::verification_result_json(result)),
        OutputFormat::Ndjson => println!("{}", report::verification_result_ndjson(result)),
        OutputFormat::Junit => println!("{}", report::verification_result_junit(result)),
    }
}

fn print_machine_error(output: OutputFormat, suite_name: &str, case_name: &str, error: &HenError) {
    match output {
        OutputFormat::Text => unreachable!("text errors should be printed by main"),
        OutputFormat::Json => print_json(&report::hen_error_json(error)),
        OutputFormat::Ndjson => println!("{}", report::hen_error_ndjson(error)),
        OutputFormat::Junit => {
            println!("{}", report::hen_error_junit(suite_name, case_name, error))
        }
    }
}

fn print_json(value: &serde_json::Value) {
    let rendered = serde_json::to_string_pretty(value).expect("json output should serialize");
    println!("{}", rendered);
}

fn load_collection(args: &RunArgs, cwd: PathBuf) -> HenResult<collection::Collection> {
    if args.non_interactive {
        return load_collection_non_interactive(args, cwd);
    }

    let collection = match args.path.as_ref() {
        Some(path) => {
            let path = PathBuf::from(path);
            if path.is_dir() {
                let hen_files = scan_directory(path.clone()).map_err(|err| {
                    err.with_detail(format!("While scanning directory {}", path.display()))
                })?;
                if hen_files.len() == 1 {
                    collection::Collection::new(hen_files[0].clone())
                } else {
                    collection_prompt(path)
                }
            } else {
                collection::Collection::new(path)
            }
        }
        None => collection_prompt(cwd),
    }?;

    Ok(collection)
}

fn load_collection_non_interactive(
    args: &RunArgs,
    cwd: PathBuf,
) -> HenResult<collection::Collection> {
    let path = args.path.as_ref().map(PathBuf::from).unwrap_or(cwd);

    if path.is_dir() {
        let hen_files = scan_directory(path.clone()).map_err(|err| {
            err.with_detail(format!("While scanning directory {}", path.display()))
        })?;

        return match hen_files.len() {
            0 => Err(
                HenError::new(HenErrorKind::Input, "No .hen files found in directory")
                    .with_detail(format!("Directory: {}", path.display()))
                    .with_exit_code(2),
            ),
            1 => collection::Collection::new(hen_files[0].clone()),
            _ => Err(HenError::new(
                HenErrorKind::Input,
                "Directory contains multiple .hen files and cannot be resolved non-interactively",
            )
            .with_detail(format!("Directory: {}", path.display()))
            .with_detail("Provide a specific collection file path instead.")
            .with_exit_code(2)),
        };
    }

    collection::Collection::new(path)
}

fn resolve_execution_plan(
    collection: &collection::Collection,
    planner: &request::RequestPlanner,
    args: &RunArgs,
) -> HenResult<(Vec<usize>, Vec<usize>, Option<usize>)> {
    if args.non_interactive {
        return resolve_execution_plan_non_interactive(
            collection,
            planner,
            args.selector.as_deref(),
        );
    }

    match args.selector.as_deref() {
        Some("all") => {
            let order = planner.order_all();
            Ok((order.clone(), order, None))
        }
        Some(selector) => {
            let idx = selector.parse::<usize>().map_err(|_| {
                HenError::new(HenErrorKind::Input, "Selector must be an integer")
                    .with_detail(format!("Received: {}", selector))
                    .with_exit_code(2)
            })?;

            let order = planner.order_for(idx).map_err(|err| {
                HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                    .with_detail(err.to_string())
            })?;

            Ok((order.clone(), vec![idx], Some(idx)))
        }
        None => {
            if collection.requests.len() == 1 {
                let order = planner.order_for(0).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for the only request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![0], Some(0)))
            } else {
                let selection = request_prompt(collection);
                let order = planner.order_for(selection).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![selection], Some(selection)))
            }
        }
    }
}

fn resolve_execution_plan_non_interactive(
    collection: &collection::Collection,
    planner: &request::RequestPlanner,
    selector: Option<&str>,
) -> HenResult<(Vec<usize>, Vec<usize>, Option<usize>)> {
    match selector {
        Some("all") => {
            let order = planner.order_all();
            Ok((order.clone(), order, None))
        }
        Some(selector) => {
            let idx = selector.parse::<usize>().map_err(|_| {
                HenError::new(HenErrorKind::Input, "Selector must be an integer or 'all'")
                    .with_detail(format!("Received: {}", selector))
                    .with_exit_code(2)
            })?;

            let order = planner.order_for(idx).map_err(|err| {
                HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                    .with_detail(err.to_string())
            })?;

            Ok((order.clone(), vec![idx], Some(idx)))
        }
        None => {
            if collection.requests.len() == 1 {
                let order = planner.order_for(0).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for the only request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![0], Some(0)))
            } else {
                Err(HenError::new(
                    HenErrorKind::Input,
                    "A selector is required when a collection contains multiple requests",
                )
                .with_detail("Provide a request index or use 'all'.")
                .with_exit_code(2))
            }
        }
    }
}

fn parse_input_kv(s: &str) -> Result<(String, String), String> {
    let mut parts = s.splitn(2, '=');
    let key = parts
        .next()
        .map(str::trim)
        .filter(|k| !k.is_empty())
        .ok_or_else(|| "--input expects key=value".to_string())?;
    let value = parts
        .next()
        .map(|v| v.trim().to_string())
        .ok_or_else(|| "--input expects key=value".to_string())?;
    Ok((key.to_string(), value))
}

fn print_verification_result(result: &automation::VerificationResult) {
    println!("{}", style("Verification passed").green().bold());

    if let Some(path) = &result.path {
        println!("{} {}", style("Path:").dim(), path.display());
    }

    if !result.summary.name.trim().is_empty() {
        println!(
            "{} {}",
            style("Collection:").dim(),
            result.summary.name.trim()
        );
    }

    if !result.summary.description.trim().is_empty() {
        println!(
            "{} {}",
            style("Description:").dim(),
            result.summary.description.trim()
        );
    }

    println!("{}", style("Requests").bold());
    for request in &result.summary.requests {
        println!("  [{}] {} {}", request.index, request.method, request.url);
    }

    println!("{}", style("Required inputs").bold());
    if result.required_inputs.is_empty() {
        println!("  {}", style("<none>").dim());
        return;
    }

    for prompt in &result.required_inputs {
        match &prompt.default {
            Some(default) => println!("  - {} (default: {})", prompt.name, default),
            None => println!("  - {}", prompt.name),
        }
    }
}

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

    #[test]
    fn cli_parses_run_subcommand_non_interactive_mode() {
        let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "all", "--non-interactive"])
            .expect("cli should parse");

        let Invocation::Run(args) = cli.into_invocation() else {
            panic!("expected run invocation");
        };
        assert_eq!(args.path.as_deref(), Some("./example.hen"));
        assert_eq!(args.selector.as_deref(), Some("all"));
        assert!(args.non_interactive);
    }

    #[test]
    fn cli_parses_verify_subcommand() {
        let cli =
            Cli::try_parse_from(["hen", "verify", "./example.hen"]).expect("cli should parse");

        let Invocation::Verify(args) = cli.into_invocation() else {
            panic!("expected verify invocation");
        };
        assert_eq!(args.path, "./example.hen");
    }

    #[test]
    fn cli_parses_run_output_format() {
        let cli = Cli::try_parse_from(["hen", "run", "./example.hen", "--output", "json"])
            .expect("cli should parse");

        let Invocation::Run(args) = cli.into_invocation() else {
            panic!("expected run invocation");
        };
        assert_eq!(args.output, OutputFormat::Json);
    }
}

fn print_status_line(record: &request::ExecutionRecord) {
    let success_symbol = style("[ok]").green();
    let index_label = style(format!("#{}", record.index)).dim();
    let description_label = style(&record.description).bold();
    let method_label = style(record.method.as_str()).cyan();
    let url_label = style(&record.url).dim();
    let target_label = format!("{} {}", method_label, url_label);

    let status = record.execution.snapshot.status;
    let status_text = if let Some(reason) = status.canonical_reason() {
        format!("{} {}", status.as_u16(), reason)
    } else {
        status.as_u16().to_string()
    };
    let status_label = match status.as_u16() {
        200..=299 => style(status_text).green(),
        300..=399 => style(status_text).cyan(),
        400..=499 => style(status_text).yellow(),
        500..=599 => style(status_text).red(),
        _ => style(status_text),
    };

    let duration_label = style(format_duration(record.duration)).dim();

    println!(
        "{} {} {} ({}) — {} — {}",
        success_symbol, index_label, description_label, target_label, status_label, duration_label
    );
}

fn print_body_preview(record: &request::ExecutionRecord, verbose: bool) {
    let heading = if verbose { "Body:" } else { "Body preview:" };
    println!("  {}", style(heading).dim());

    let body = record.execution.output.trim();
    if body.is_empty() {
        println!("    {}", style("<empty>").dim());
        return;
    }

    let (preview, truncated) = build_body_output(body, verbose);
    for line in preview.lines() {
        println!("    {}", line);
    }

    if truncated {
        println!(
            "    {}",
            style(format!(
                "... truncated to {} lines / {} chars",
                PREVIEW_MAX_LINES, PREVIEW_MAX_CHARS
            ))
            .dim()
        );
    }
}

fn build_body_output(body: &str, verbose: bool) -> (String, bool) {
    if verbose {
        (body.to_string(), false)
    } else {
        build_preview(body, PREVIEW_MAX_LINES, PREVIEW_MAX_CHARS)
    }
}

fn print_failure_line(failure: &request::RequestFailure) {
    let failure_symbol = style("[x]").red();
    let index_label = match failure.index() {
        Some(idx) => style(format!("#{}", idx)).dim().to_string(),
        None => style("#?").dim().to_string(),
    };
    let request_label = style(failure.request()).bold();

    let detail = match failure.kind() {
        request::RequestFailureKind::Execution { message, .. } => message.clone(),
        request::RequestFailureKind::Dependency { dependency } => {
            format!("skipped: dependency '{}' failed", dependency)
        }
        request::RequestFailureKind::MissingDependency { dependency } => {
            format!("missing dependency '{}'", dependency)
        }
        request::RequestFailureKind::Join { message } => message.clone(),
        request::RequestFailureKind::MapAborted { group, cause } => {
            format!("skipped: '{}' aborted after failure in '{}'", group, cause)
        }
    };

    println!(
        "{} {} {} — {}",
        failure_symbol,
        index_label,
        request_label,
        style(detail).red()
    );
}

fn print_summary(
    records: &[request::ExecutionRecord],
    failures: &[request::RequestFailure],
    interrupted: Option<request::InterruptSignal>,
    planned_count: usize,
) {
    println!("{}", style("Summary").bold());

    let success_text = format!("{} succeeded", records.len());
    let success_label = if records.is_empty() {
        style(success_text).dim()
    } else {
        style(success_text).green()
    };
    println!("  {}", success_label);

    let failure_text = format!("{} failed", failures.len());
    let failure_label = if failures.is_empty() {
        style(failure_text).dim()
    } else {
        style(failure_text).red()
    };
    println!("  {}", failure_label);

    if let Some(total) = total_elapsed(records) {
        println!(
            "  {}",
            style(format!("elapsed {}", format_duration(total))).dim()
        );
    }

    if let Some(slowest) = records.iter().max_by_key(|record| record.duration) {
        println!(
            "  {}",
            style(format!(
                "slowest {} ({})",
                slowest.description,
                format_duration(slowest.duration)
            ))
            .dim()
        );
    }

    if let Some(signal) = interrupted {
        println!(
            "  {}",
            style(format!(
                "interrupted by {} after {} of {} planned requests finished",
                signal.as_str(),
                records.len() + failures.len(),
                planned_count,
            ))
            .yellow()
        );
    }
}

fn build_preview(text: &str, max_lines: usize, max_chars: usize) -> (String, bool) {
    if text.is_empty() {
        return (String::new(), false);
    }

    let mut preview = String::new();
    let mut truncated = false;
    let mut remaining_chars = max_chars;
    let mut consumed_all = true;

    for (idx, line) in text.lines().enumerate() {
        if idx >= max_lines || remaining_chars == 0 {
            truncated = true;
            consumed_all = false;
            break;
        }

        let mut line_segment = String::new();
        for ch in line.chars() {
            if remaining_chars == 0 {
                truncated = true;
                consumed_all = false;
                break;
            }
            line_segment.push(ch);
            remaining_chars -= 1;
        }

        if !preview.is_empty() {
            preview.push('\n');
        }
        preview.push_str(&line_segment);

        if remaining_chars == 0 {
            truncated = true;
            consumed_all = false;
            break;
        }
    }

    if preview.is_empty() {
        let collected: String = text.chars().take(max_chars).collect();
        let total_chars = text.chars().count();
        let collected_chars = collected.chars().count();
        return (collected, collected_chars < total_chars);
    }

    (preview, truncated || !consumed_all)
}

fn format_duration(duration: Duration) -> String {
    if duration.as_secs() >= 1 {
        let secs = duration.as_secs_f64();
        if secs >= 10.0 {
            format!("{:.1}s", secs)
        } else {
            format!("{:.2}s", secs)
        }
    } else if duration.as_millis() >= 1 {
        format!("{} ms", duration.as_millis())
    } else {
        format!("{} us", duration.as_micros())
    }
}

fn total_elapsed(records: &[request::ExecutionRecord]) -> Option<Duration> {
    if records.is_empty() {
        return None;
    }

    let mut earliest: Option<Duration> = None;
    let mut latest: Option<Duration> = None;

    for record in records {
        let start = match record.started_at.duration_since(SystemTime::UNIX_EPOCH) {
            Ok(duration) => duration,
            Err(_) => continue,
        };

        let finish = match record
            .started_at
            .checked_add(record.duration)
            .and_then(|time| time.duration_since(SystemTime::UNIX_EPOCH).ok())
        {
            Some(duration) => duration,
            None => continue,
        };

        earliest = Some(match earliest {
            Some(current) => current.min(start),
            None => start,
        });
        latest = Some(match latest {
            Some(current) => current.max(finish),
            None => finish,
        });
    }

    match (earliest, latest) {
        (Some(start), Some(end)) if end >= start => Some(end - start),
        _ => None,
    }
}

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

    #[test]
    fn build_body_output_truncates_when_not_verbose() {
        let body = (1..=20)
            .map(|idx| format!("line {idx}"))
            .collect::<Vec<_>>()
            .join("\n");

        let (output, truncated) = build_body_output(&body, false);

        assert!(truncated);
        assert!(output.lines().count() <= PREVIEW_MAX_LINES);
    }

    #[test]
    fn build_body_output_keeps_full_text_when_verbose() {
        let body = (1..=20)
            .map(|idx| format!("line {idx}"))
            .collect::<Vec<_>>()
            .join("\n");

        let (output, truncated) = build_body_output(&body, true);

        assert!(!truncated);
        assert_eq!(output, body);
    }
}
