//! Pure waiver matching: partition report findings into kept and waived, and
//! turn waivers that matched nothing into warning findings.
//!
//! A finding matches a waiver when their `rule` and `subject` are equal and the
//! selector agrees: a waiver with no selector matches any finding (for that
//! rule + subject), and a waiver with a selector matches only the finding whose
//! selector is exactly equal. Mark-not-delete: a waived finding is reported
//! under `waived`, not dropped. A waiver that matched nothing becomes a
//! warning-severity finding (stale, or the deviation is already resolved) -
//! surfaced in the one findings list, but not failing the run.

use shakrs_json_parser::Waiver;

use crate::types::{Applied, Finding, WaivedFinding};

/// True when `finding` is covered by `waiver`.
fn covers(finding: &Finding, waiver: &Waiver) -> bool {
    if !finding.waivable {
        return false;
    }
    if finding.key != waiver.key || finding.subject != waiver.subject {
        return false;
    }
    waiver
        .selector
        .as_ref()
        .is_none_or(|selector| finding.selector.as_deref() == Some(selector.as_str()))
}

/// The warning finding that surfaces a waiver which matched nothing this run.
fn unused_warning(waiver: Waiver) -> Finding {
    Finding {
        key: waiver.key,
        subject: waiver.subject,
        selector: waiver.selector,
        severity: "warning".to_owned(),
        message: "waiver matched no finding; it is stale (remove it) or the deviation is already resolved".to_owned(),
        current: None,
        expected: None,
        policies: Vec::new(),
        waivable: true,
    }
}

/// Partition `findings` against `waivers`.
///
/// Each finding is checked against every waiver; the first matching waiver
/// suppresses it. A waiver that suppressed nothing becomes a warning finding
/// appended to `kept`.
#[must_use]
pub(crate) fn apply(findings: Vec<Finding>, waivers: Vec<Waiver>) -> Applied {
    let mut used = vec![false; waivers.len()];
    let mut kept: Vec<Finding> = Vec::new();
    let mut waived: Vec<WaivedFinding> = Vec::new();

    for finding in findings {
        let hit = waivers.iter().position(|waiver| covers(&finding, waiver));
        match hit.and_then(|index| waivers.get(index).map(|waiver| (index, waiver.clone()))) {
            Some((index, matched)) => {
                if let Some(flag) = used.get_mut(index) {
                    *flag = true;
                }
                waived.push(WaivedFinding {
                    finding,
                    waiver: matched,
                });
            }
            None => kept.push(finding),
        }
    }

    for (waiver, hit) in waivers.into_iter().zip(used) {
        if !hit {
            kept.push(unused_warning(waiver));
        }
    }

    Applied { kept, waived }
}
