#![cfg_attr(coverage, allow(dead_code))]

#[cfg(any(not(coverage), test))]
use anyhow::{Context, Result, bail};
#[cfg(not(coverage))]
use serde::Serialize;
#[cfg(any(not(coverage), test))]
use std::collections::BTreeSet;
#[cfg(any(not(coverage), test))]
use std::path::PathBuf;

#[cfg(not(coverage))]
use lesavka_client::sync_probe::analyze::analyze_capture;
#[cfg(any(not(coverage), test))]
use lesavka_client::sync_probe::analyze::{
    SyncAnalysisOptions, SyncAnalysisReport, SyncAnalysisVerdict, SyncCalibrationRecommendation,
};

#[cfg(not(coverage))]
#[derive(Serialize)]
struct SyncAnalyzeOutput<'a> {
    #[serde(flatten)]
    report: &'a SyncAnalysisReport,
    #[serde(skip_serializing_if = "Option::is_none")]
    signature_coverage: Option<SignatureCoverage>,
    calibration: SyncCalibrationRecommendation,
    verdict: SyncAnalysisVerdict,
}

#[cfg(any(not(coverage), test))]
#[cfg_attr(not(coverage), derive(Serialize))]
struct SignatureCoverage {
    expected_event_count: usize,
    expected_codes: Vec<u32>,
    paired_event_count: usize,
    paired_server_event_ids: Vec<usize>,
    paired_codes: Vec<u32>,
    missing_event_ids: Vec<usize>,
    missing_codes: Vec<u32>,
    unknown_pair_identity_count: usize,
}

#[cfg(not(coverage))]
/// Keeps `main` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn main() -> Result<()> {
    let args = parse_args(std::env::args().skip(1))?;
    let report = analyze_capture(&args.capture_path, &args.options)
        .with_context(|| format!("analyzing sync capture {}", args.capture_path.display()))?;
    let calibration = report.calibration_recommendation();
    let verdict = report.verdict();
    let signature_coverage = signature_coverage(&args.options.event_width_codes, &report);
    let human_report = format_human_report(
        &args.capture_path,
        &report,
        signature_coverage.as_ref(),
        &calibration,
        &verdict,
    );
    let output = SyncAnalyzeOutput {
        report: &report,
        signature_coverage,
        calibration,
        verdict,
    };

    if let Some(report_dir) = &args.report_dir {
        write_report_dir(report_dir, &human_report, &output)?;
    }

    if args.emit_json {
        println!(
            "{}",
            serde_json::to_string_pretty(&output).context("serializing JSON report")?
        );
    } else {
        print!("{human_report}");
    }

    Ok(())
}

#[cfg(any(not(coverage), test))]
#[derive(Debug, PartialEq)]
struct AnalyzeArgs {
    capture_path: PathBuf,
    emit_json: bool,
    report_dir: Option<PathBuf>,
    options: SyncAnalysisOptions,
}

#[cfg(any(not(coverage), test))]
/// Keeps `parse_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_args<I, S>(args: I) -> Result<AnalyzeArgs>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
    if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
        println!(
            "Usage: lesavka-sync-analyze <capture.mkv> [--json] [--report-dir <dir>] [--event-width-codes 1,2,1,3] [--analysis-window-s START:END]"
        );
        std::process::exit(0);
    }

    let mut emit_json = false;
    let mut report_dir = None::<PathBuf>;
    let mut capture_path = None::<PathBuf>;
    let mut options = SyncAnalysisOptions::default();
    let mut iter = args.into_iter();
    while let Some(arg) = iter.next() {
        if arg == "--json" {
            emit_json = true;
            continue;
        }
        if arg == "--report-dir" {
            let Some(dir) = iter.next() else {
                bail!("--report-dir requires a directory");
            };
            report_dir = Some(PathBuf::from(dir));
            continue;
        }
        if let Some(dir) = arg.strip_prefix("--report-dir=") {
            if dir.is_empty() {
                bail!("--report-dir requires a directory");
            }
            report_dir = Some(PathBuf::from(dir));
            continue;
        }
        if arg == "--event-width-codes" {
            let Some(raw_codes) = iter.next() else {
                bail!("--event-width-codes requires a comma-separated list");
            };
            options.event_width_codes = parse_event_width_codes(&raw_codes)?;
            continue;
        }
        if let Some(raw_codes) = arg.strip_prefix("--event-width-codes=") {
            if raw_codes.is_empty() {
                bail!("--event-width-codes requires a comma-separated list");
            }
            options.event_width_codes = parse_event_width_codes(raw_codes)?;
            continue;
        }
        if arg == "--analysis-window-s" {
            let Some(raw_window) = iter.next() else {
                bail!("--analysis-window-s requires START:END seconds");
            };
            parse_analysis_window(&raw_window, &mut options)?;
            continue;
        }
        if let Some(raw_window) = arg.strip_prefix("--analysis-window-s=") {
            if raw_window.is_empty() {
                bail!("--analysis-window-s requires START:END seconds");
            }
            parse_analysis_window(raw_window, &mut options)?;
            continue;
        }
        if arg == "--analysis-start-s" {
            let Some(raw_start) = iter.next() else {
                bail!("--analysis-start-s requires seconds");
            };
            options.analysis_start_s = Some(parse_analysis_seconds(&raw_start, "analysis start")?);
            continue;
        }
        if let Some(raw_start) = arg.strip_prefix("--analysis-start-s=") {
            if raw_start.is_empty() {
                bail!("--analysis-start-s requires seconds");
            }
            options.analysis_start_s = Some(parse_analysis_seconds(raw_start, "analysis start")?);
            continue;
        }
        if arg == "--analysis-end-s" {
            let Some(raw_end) = iter.next() else {
                bail!("--analysis-end-s requires seconds");
            };
            options.analysis_end_s = Some(parse_analysis_seconds(&raw_end, "analysis end")?);
            continue;
        }
        if let Some(raw_end) = arg.strip_prefix("--analysis-end-s=") {
            if raw_end.is_empty() {
                bail!("--analysis-end-s requires seconds");
            }
            options.analysis_end_s = Some(parse_analysis_seconds(raw_end, "analysis end")?);
            continue;
        }
        if capture_path.is_some() {
            bail!("unexpected extra argument `{arg}`");
        }
        capture_path = Some(PathBuf::from(arg));
    }

    let capture_path = capture_path.context("capture path is required")?;
    Ok(AnalyzeArgs {
        capture_path,
        emit_json,
        report_dir,
        options,
    })
}

#[cfg(any(not(coverage), test))]
/// Keeps `parse_event_width_codes` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_event_width_codes(raw: &str) -> Result<Vec<u32>> {
    let codes = raw
        .split(',')
        .filter_map(|part| {
            let trimmed = part.trim();
            (!trimmed.is_empty()).then_some(trimmed)
        })
        .map(|part| {
            let code = part
                .parse::<u32>()
                .with_context(|| format!("parsing event width code `{part}`"))?;
            if code == 0 {
                bail!("event width codes must be positive");
            }
            Ok(code)
        })
        .collect::<Result<Vec<_>>>()?;
    if codes.is_empty() {
        bail!("event width code list must not be empty");
    }
    Ok(codes)
}

#[cfg(any(not(coverage), test))]
/// Keeps `parse_analysis_window` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_analysis_window(raw: &str, options: &mut SyncAnalysisOptions) -> Result<()> {
    let Some((start, end)) = raw.split_once(':') else {
        bail!("--analysis-window-s requires START:END seconds");
    };
    options.analysis_start_s = if start.trim().is_empty() {
        None
    } else {
        Some(parse_analysis_seconds(start, "analysis start")?)
    };
    options.analysis_end_s = if end.trim().is_empty() {
        None
    } else {
        Some(parse_analysis_seconds(end, "analysis end")?)
    };
    Ok(())
}

#[cfg(any(not(coverage), test))]
/// Keeps `parse_analysis_seconds` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_analysis_seconds(raw: &str, label: &str) -> Result<f64> {
    let value = raw
        .trim()
        .parse::<f64>()
        .with_context(|| format!("parsing {label} `{raw}`"))?;
    if !value.is_finite() || value < 0.0 {
        bail!("{label} must be a finite non-negative timestamp");
    }
    Ok(value)
}

include!("lesavka_sync_analyze/human_report.rs");
#[cfg(any(not(coverage), test))]
/// Keeps `signature_coverage` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn signature_coverage(
    expected_codes: &[u32],
    report: &SyncAnalysisReport,
) -> Option<SignatureCoverage> {
    if expected_codes.is_empty() {
        return None;
    }
    let paired_server_event_ids = report
        .paired_events
        .iter()
        .filter_map(|event| event.server_event_id)
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect::<Vec<_>>();
    let paired_codes = paired_server_event_ids
        .iter()
        .filter_map(|index| expected_codes.get(*index).copied())
        .collect::<Vec<_>>();
    let missing_event_ids = expected_codes
        .iter()
        .enumerate()
        .filter_map(|(index, _)| (!paired_server_event_ids.contains(&index)).then_some(index))
        .collect::<Vec<_>>();
    let missing_codes = missing_event_ids
        .iter()
        .filter_map(|index| expected_codes.get(*index).copied())
        .collect::<Vec<_>>();
    let unknown_pair_identity_count = report
        .paired_events
        .iter()
        .filter(|event| event.server_event_id.is_none() || event.event_code.is_none())
        .count();
    Some(SignatureCoverage {
        expected_event_count: expected_codes.len(),
        expected_codes: expected_codes.to_vec(),
        paired_event_count: paired_server_event_ids.len(),
        paired_server_event_ids,
        paired_codes,
        missing_event_ids,
        missing_codes,
        unknown_pair_identity_count,
    })
}

#[cfg(any(not(coverage), test))]
/// Keeps `format_signature_coverage` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn format_signature_coverage(coverage: Option<&SignatureCoverage>) -> String {
    let Some(coverage) = coverage else {
        return String::new();
    };
    let missing_ids = format_usize_list(&coverage.missing_event_ids);
    let missing_codes = format_u32_list(&coverage.missing_codes);
    format!(
        "- expected coded signatures: {}\n- paired coded signatures: {}/{}\n- missing paired signature ids: {}\n- missing paired signature codes: {}\n- paired signatures without identity: {}\n",
        coverage.expected_event_count,
        coverage.paired_event_count,
        coverage.expected_event_count,
        missing_ids,
        missing_codes,
        coverage.unknown_pair_identity_count
    )
}

#[cfg(any(not(coverage), test))]
fn unpaired_video_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
    unpaired_onsets(
        &report.video_onsets_s,
        &report
            .paired_events
            .iter()
            .map(|event| event.video_time_s)
            .collect::<Vec<_>>(),
    )
}

#[cfg(any(not(coverage), test))]
fn unpaired_audio_onsets(report: &SyncAnalysisReport) -> Vec<f64> {
    unpaired_onsets(
        &report.audio_onsets_s,
        &report
            .paired_events
            .iter()
            .map(|event| event.audio_time_s)
            .collect::<Vec<_>>(),
    )
}

#[cfg(any(not(coverage), test))]
/// Keeps `unpaired_onsets` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn unpaired_onsets(all_onsets: &[f64], paired_onsets: &[f64]) -> Vec<f64> {
    const SAME_ONSET_EPSILON_S: f64 = 0.000_001;
    all_onsets
        .iter()
        .copied()
        .filter(|onset| {
            !paired_onsets
                .iter()
                .any(|paired| (paired - onset).abs() <= SAME_ONSET_EPSILON_S)
        })
        .collect()
}

#[cfg(any(not(coverage), test))]
/// Keeps `format_onset_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn format_onset_list(onsets: &[f64]) -> String {
    const MAX_PRINTED_ONSETS: usize = 12;
    if onsets.is_empty() {
        return "none".to_string();
    }
    let mut formatted = onsets
        .iter()
        .take(MAX_PRINTED_ONSETS)
        .map(|onset| format!("{onset:.3}s"))
        .collect::<Vec<_>>();
    if onsets.len() > MAX_PRINTED_ONSETS {
        formatted.push(format!("...+{}", onsets.len() - MAX_PRINTED_ONSETS));
    }
    formatted.join(", ")
}

#[cfg(any(not(coverage), test))]
/// Keeps `format_usize_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn format_usize_list(values: &[usize]) -> String {
    if values.is_empty() {
        return "none".to_string();
    }
    values
        .iter()
        .map(|value| value.to_string())
        .collect::<Vec<_>>()
        .join(", ")
}

#[cfg(any(not(coverage), test))]
/// Keeps `format_u32_list` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn format_u32_list(values: &[u32]) -> String {
    if values.is_empty() {
        return "none".to_string();
    }
    values
        .iter()
        .map(|value| value.to_string())
        .collect::<Vec<_>>()
        .join(", ")
}

include!("lesavka_sync_analyze/report_output_files.rs");

#[cfg(coverage)]
fn main() {}

#[cfg(test)]
#[path = "tests/lesavka_sync_analyze.rs"]
mod tests;
