use console::style;
use std::time::{Duration, SystemTime};

use hen::{
    automation,
    error::HenError,
    report::{self, BodyReportOptions},
    request,
};

use crate::cli::OutputFormat;

const PREVIEW_MAX_LINES: usize = 12;
const PREVIEW_MAX_CHARS: usize = 800;
pub(crate) const STRUCTURED_BODY_MAX_CHARS: usize = 4_000;

pub(crate) 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)),
    }
}

pub(crate) 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)),
    }
}

pub(crate) 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);
}

pub(crate) 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),
        }
    }
}

pub(crate) 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
        .artifact
        .http_status()
        .unwrap_or(http::StatusCode::OK);
    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
    );
}

pub(crate) fn print_timing_line(phases: &[request::ArtifactTimingPhase]) {
    if let Some(line) = format_timing_line(phases) {
        println!("  {}", style(line).dim());
    }
}

pub(crate) fn format_timing_line(phases: &[request::ArtifactTimingPhase]) -> Option<String> {
    if phases.is_empty() {
        return None;
    }

    Some(format!(
        "timing: {}",
        phases
            .iter()
            .map(|phase| format!("{} {}", phase.name, format_duration(phase.duration)))
            .collect::<Vec<_>>()
            .join(", ")
    ))
}

pub(crate) 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)
    }
}

pub(crate) 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()
    );
}

pub(crate) 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);
    }

    #[test]
    fn format_timing_line_returns_none_for_empty_phases() {
        assert_eq!(format_timing_line(&[]), None);
    }

    #[test]
    fn format_timing_line_formats_named_phases() {
        let phases = vec![
            request::ArtifactTimingPhase::new("dns", Duration::from_millis(3)),
            request::ArtifactTimingPhase::new("responseStart", Duration::from_millis(18)),
            request::ArtifactTimingPhase::new("bodyRead", Duration::from_millis(1)),
        ];

        assert_eq!(
            format_timing_line(&phases),
            Some("timing: dns 3 ms, responseStart 18 ms, bodyRead 1 ms".to_string())
        );
    }
}