//! The scan-applicability matrix.
//!
//! Which scans are possible for a target is a function of its [`TargetType`] and
//! which [`ArtifactKind`]s are actually present: SAST needs code, DAST needs a
//! running URL, firmware-static analysis needs a firmware image, and so on. This
//! module encodes that as a table — one rule set per target type — and resolves
//! it against a concrete [`OnboardedTarget`] into a list of [`ScanOption`]s the
//! onboarding wizard and the scan pipeline both consume.

use crate::models::{ArtifactKind, OnboardedTarget, ScanType, TargetType};

/// What an artifact a scan needs in order to run.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactRequirement {
    /// Source code — a git repo or a source archive.
    Code,
    /// A reachable running instance (any live URL / endpoint, scheme-agnostic —
    /// e.g. the ICS probe works off the host:port of a modbus:// or http:// ref).
    RunningUrl,
    /// A reachable **web** endpoint — a live URL with an http(s) scheme. DAST is
    /// an HTTP crawler, so a modbus:// / opc.tcp:// endpoint does not satisfy it.
    HttpUrl,
    /// A firmware image / binary blob.
    Firmware,
    /// A PLC project (PLCopen XML or Structured Text).
    Plc,
    /// A mobile package (APK / AAB / IPA).
    Mobile,
    /// A container image.
    Container,
    /// No specific artifact required.
    Any,
}

/// A static rule: this scan applies to a target type, needs this artifact, and
/// defaults on/off. The rationale explains the entry to the user.
#[derive(Debug, Clone, Copy)]
pub struct ScanRule {
    /// The scan this rule governs.
    pub scan: ScanType,
    /// Whether the scan is on by default (only when its artifact is present).
    pub default_on: bool,
    /// Human-readable explanation of what the scan does here.
    pub rationale: &'static str,
    /// The artifact the scan consumes.
    pub requires: ArtifactRequirement,
}

impl ScanRule {
    const fn new(
        scan: ScanType,
        default_on: bool,
        rationale: &'static str,
        requires: ArtifactRequirement,
    ) -> Self {
        Self {
            scan,
            default_on,
            rationale,
            requires,
        }
    }
}

/// A resolved scan choice for a specific target: a rule intersected with the
/// artifacts actually present. `blocked_reason` is `Some` when the required
/// artifact is missing.
#[derive(Debug, Clone)]
pub struct ScanOption {
    /// The scan.
    pub scan: ScanType,
    /// Whether to pre-select the scan (false when blocked).
    pub default_on: bool,
    /// Why the scan is offered.
    pub rationale: String,
    /// The artifact kind the scan needs, if any specific one.
    pub required_artifact: Option<ArtifactKind>,
    /// Set when the required artifact is absent, explaining the block.
    pub blocked_reason: Option<String>,
}

/// The SAST umbrella: every static-analysis sub-scan that runs over source code.
fn sast_umbrella() -> Vec<ScanRule> {
    use ArtifactRequirement::Code;
    vec![
        ScanRule::new(
            ScanType::Sast,
            true,
            "Static analysis (Semgrep) over source",
            Code,
        ),
        ScanRule::new(
            ScanType::Sbom,
            true,
            "Software bill of materials from source",
            Code,
        ),
        ScanRule::new(
            ScanType::Cve,
            true,
            "Match dependencies against known CVEs",
            Code,
        ),
        ScanRule::new(
            ScanType::SecretDetection,
            true,
            "Scan source for committed secrets",
            Code,
        ),
        ScanRule::new(ScanType::Lint, true, "Language linters over source", Code),
        ScanRule::new(
            ScanType::Gdpr,
            true,
            "GDPR data-handling pattern checks",
            Code,
        ),
        ScanRule::new(
            ScanType::OAuth,
            true,
            "OAuth misconfiguration patterns",
            Code,
        ),
        ScanRule::new(
            ScanType::Graph,
            true,
            "Build the code graph for impact analysis",
            Code,
        ),
        ScanRule::new(
            ScanType::CodeReview,
            false,
            "LLM code review over changed source",
            Code,
        ),
    ]
}

/// The rule set for a target type. Scans that are never applicable to a type are
/// simply absent (e.g. DAST is not listed for a PLC target).
pub fn rules_for(target_type: TargetType) -> Vec<ScanRule> {
    use ArtifactRequirement::{Firmware, HttpUrl, Mobile, Plc, RunningUrl};
    match target_type {
        TargetType::WebApp | TargetType::BackendService => {
            let mut r = sast_umbrella();
            r.push(ScanRule::new(
                ScanType::Dast,
                true,
                "Dynamic scan of the running endpoint",
                HttpUrl,
            ));
            r
        }
        TargetType::DesktopApp => sast_umbrella(),
        TargetType::AndroidApp | TargetType::IosApp => {
            let mut r = sast_umbrella();
            r.push(ScanRule::new(
                ScanType::MobileStatic,
                true,
                "Static analysis of the mobile package (manifest, permissions, libs)",
                Mobile,
            ));
            r
        }
        TargetType::FirmwareBareMetal | TargetType::FirmwareRtos => {
            let mut r = sast_umbrella();
            r.push(ScanRule::new(
                ScanType::FirmwareStatic,
                true,
                "Unpack and statically analyze the firmware image",
                Firmware,
            ));
            r.push(ScanRule::new(
                ScanType::Sbom,
                true,
                "SBOM from the firmware image (binwalk / tramiton)",
                Firmware,
            ));
            r.push(ScanRule::new(
                ScanType::Cve,
                true,
                "Match firmware components against known CVEs",
                Firmware,
            ));
            r
        }
        TargetType::EmbeddedLinuxYocto => {
            let mut r = sast_umbrella();
            r.push(ScanRule::new(
                ScanType::FirmwareStatic,
                true,
                "EMBA / binwalk static analysis of the image",
                Firmware,
            ));
            r.push(ScanRule::new(
                ScanType::Sbom,
                true,
                "SBOM from image layers / recipes",
                Firmware,
            ));
            r.push(ScanRule::new(
                ScanType::Cve,
                true,
                "Match image components against known CVEs",
                Firmware,
            ));
            r.push(ScanRule::new(
                ScanType::Dast,
                false,
                "Dynamic scan of exposed network services (if any)",
                HttpUrl,
            ));
            r
        }
        TargetType::PlcSps => {
            // A PLC/SPS device is a composite: the control application *and* the
            // device it runs on (firmware/OS + reachable runtime services). The
            // control-logic scan runs on the PLC project; the firmware and DAST
            // scans light up only when a firmware image / running endpoint is
            // attached (e.g. a CODESYS runtime on a Yocto image with WebVisu).
            // Firmware-image SBOM/CVE *execution* is shared with the firmware
            // families and tracked in #151/#128; DAST over a WebVisu/OPC-UA
            // endpoint uses the existing DAST path.
            vec![
                ScanRule::new(
                    ScanType::PlcControlLogic,
                    true,
                    "Control-logic security rules over the PLC program",
                    Plc,
                ),
                // Device-level scans are offered but opt-in (default-off): they
                // apply only when a firmware image is attached, and firmware-image
                // SBOM/CVE *execution* is shared with the firmware families and
                // still landing (#151/#128), so they must not silently auto-run.
                ScanRule::new(
                    ScanType::FirmwareStatic,
                    false,
                    "Static analysis of the device firmware image (OS + runtime)",
                    Firmware,
                ),
                ScanRule::new(
                    ScanType::Sbom,
                    false,
                    "SBOM from the device firmware image (OS packages + CODESYS runtime)",
                    Firmware,
                ),
                ScanRule::new(
                    ScanType::Cve,
                    false,
                    "Match device firmware components against known CVEs",
                    Firmware,
                ),
                ScanRule::new(
                    ScanType::Dast,
                    false,
                    "Dynamic scan of the running device (WebVisu / exposed services)",
                    HttpUrl,
                ),
                ScanRule::new(
                    ScanType::IcsProbe,
                    false,
                    "Probe the running device over industrial protocols (Modbus/TCP, …)",
                    RunningUrl,
                ),
            ]
        }
    }
}

/// Whether an active penetration test is applicable to this target type.
///
/// Pentest runs as its own session (not a [`ScanType`] scan) and needs a
/// reachable running target, so it is offered only for the network-reachable
/// families.
pub fn supports_pentest(target_type: TargetType) -> bool {
    matches!(
        target_type,
        TargetType::WebApp
            | TargetType::BackendService
            | TargetType::AndroidApp
            | TargetType::IosApp
            | TargetType::EmbeddedLinuxYocto
            // A PLC/SPS device exposes reachable runtime services (WebVisu, OPC UA,
            // the CODESYS programming protocol), so an active pentest applies.
            | TargetType::PlcSps
    )
}

/// The representative artifact kind a requirement is satisfied by.
fn representative_kind(req: ArtifactRequirement) -> Option<ArtifactKind> {
    match req {
        ArtifactRequirement::Code => Some(ArtifactKind::GitRepo),
        ArtifactRequirement::RunningUrl | ArtifactRequirement::HttpUrl => {
            Some(ArtifactKind::LiveUrl)
        }
        ArtifactRequirement::Firmware => Some(ArtifactKind::FirmwareImage),
        ArtifactRequirement::Plc => Some(ArtifactKind::PlcProject),
        ArtifactRequirement::Mobile => Some(ArtifactKind::MobilePackage),
        ArtifactRequirement::Container => Some(ArtifactKind::ContainerImage),
        ArtifactRequirement::Any => None,
    }
}

/// Whether a live-URL reference is an http(s) web endpoint (vs. an industrial
/// endpoint like `modbus://` / `opc.tcp://`, which DAST cannot crawl).
fn is_http_url(source_ref: &str) -> bool {
    let s = source_ref.trim();
    s.starts_with("http://") || s.starts_with("https://")
}

/// Whether the target carries an artifact that satisfies the requirement.
fn requirement_satisfied(req: ArtifactRequirement, target: &OnboardedTarget) -> bool {
    match req {
        ArtifactRequirement::Code => target.code_artifact().is_some(),
        ArtifactRequirement::RunningUrl => target.has(ArtifactKind::LiveUrl),
        ArtifactRequirement::HttpUrl => target
            .artifacts
            .iter()
            .any(|a| a.kind == ArtifactKind::LiveUrl && is_http_url(&a.source_ref)),
        ArtifactRequirement::Firmware => target.has(ArtifactKind::FirmwareImage),
        // A PLC project artifact, or a code artifact (git repo / source archive)
        // holding the control logic as PLCopen XML / ST exports — the common way
        // CODESYS projects are version-controlled.
        ArtifactRequirement::Plc => {
            target.has(ArtifactKind::PlcProject) || target.code_artifact().is_some()
        }
        ArtifactRequirement::Mobile => target.has(ArtifactKind::MobilePackage),
        ArtifactRequirement::Container => target.has(ArtifactKind::ContainerImage),
        ArtifactRequirement::Any => true,
    }
}

/// Resolve the matrix for a concrete target into the scans it can run, marking
/// any whose required artifact is missing as blocked.
pub fn applicable_scans(target: &OnboardedTarget) -> Vec<ScanOption> {
    rules_for(target.target_type)
        .into_iter()
        .map(|rule| {
            let satisfied = requirement_satisfied(rule.requires, target);
            let required_artifact = representative_kind(rule.requires);
            let blocked_reason = if satisfied {
                None
            } else if rule.requires == ArtifactRequirement::HttpUrl {
                // A live URL may be present but non-HTTP (e.g. modbus://): be
                // specific so the user knows DAST needs a web endpoint.
                Some("no http(s) live URL — DAST needs a web endpoint".to_string())
            } else {
                Some(match required_artifact {
                    Some(kind) => format!("no {kind} artifact provided"),
                    None => "required artifact missing".to_string(),
                })
            };
            ScanOption {
                scan: rule.scan,
                default_on: rule.default_on && satisfied,
                rationale: rule.rationale.to_string(),
                required_artifact,
                blocked_reason,
            }
        })
        .collect()
}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::models::{Artifact, PlcFormat};

    fn target_with(target_type: TargetType, artifacts: Vec<Artifact>) -> OnboardedTarget {
        let mut t = OnboardedTarget::new("t".to_string(), target_type);
        t.artifacts = artifacts;
        t
    }

    fn option<'a>(opts: &'a [ScanOption], scan: ScanType) -> Option<&'a ScanOption> {
        opts.iter().find(|o| o.scan == scan)
    }

    #[test]
    fn webapp_with_code_and_url_offers_sast_and_dast() {
        let t = target_with(
            TargetType::WebApp,
            vec![
                Artifact::git_repo("u", "main"),
                Artifact::live_url("http://x"),
            ],
        );
        let opts = applicable_scans(&t);
        let sast = option(&opts, ScanType::Sast).expect("sast offered");
        assert!(sast.default_on && sast.blocked_reason.is_none());
        let dast = option(&opts, ScanType::Dast).expect("dast offered");
        assert!(dast.default_on && dast.blocked_reason.is_none());
    }

    #[test]
    fn webapp_without_url_blocks_dast() {
        let t = target_with(TargetType::WebApp, vec![Artifact::git_repo("u", "main")]);
        let opts = applicable_scans(&t);
        let dast = option(&opts, ScanType::Dast).expect("dast listed");
        assert!(!dast.default_on);
        assert!(dast.blocked_reason.is_some());
        assert_eq!(dast.required_artifact, Some(ArtifactKind::LiveUrl));
    }

    #[test]
    fn firmware_offers_firmware_static_and_not_dast() {
        let t = target_with(
            TargetType::FirmwareBareMetal,
            vec![Artifact::firmware_image("fw.bin")],
        );
        let opts = applicable_scans(&t);
        let fw = option(&opts, ScanType::FirmwareStatic).expect("firmware static offered");
        assert!(fw.default_on && fw.blocked_reason.is_none());
        assert!(option(&opts, ScanType::Dast).is_none());
    }

    #[test]
    fn plc_control_logic_is_default_on_and_device_scans_block_without_artifacts() {
        // A PLC project alone: control-logic runs; the device-level scans are
        // offered but blocked until a firmware image / running endpoint is added.
        let t = target_with(
            TargetType::PlcSps,
            vec![Artifact::plc_project("p.xml", PlcFormat::PlcopenXml)],
        );
        let opts = applicable_scans(&t);
        let plc = option(&opts, ScanType::PlcControlLogic).expect("control-logic offered");
        assert!(plc.default_on && plc.blocked_reason.is_none());
        for scan in [ScanType::FirmwareStatic, ScanType::Sbom, ScanType::Cve] {
            let o = option(&opts, scan).expect("device scan offered");
            assert!(
                !o.default_on,
                "{scan} must not pre-select without a firmware image"
            );
            assert!(o.blocked_reason.is_some());
        }
        let dast = option(&opts, ScanType::Dast).expect("dast offered");
        assert!(!dast.default_on);
        assert!(dast.blocked_reason.is_some());
    }

    #[test]
    fn plc_control_logic_is_satisfied_by_a_git_repo() {
        // A CODESYS project version-controlled in git (PLCopen XML / ST exports),
        // no uploaded PlcProject artifact.
        let t = target_with(TargetType::PlcSps, vec![Artifact::git_repo("u", "main")]);
        let opts = applicable_scans(&t);
        let plc = option(&opts, ScanType::PlcControlLogic).expect("control-logic offered");
        assert!(
            plc.default_on && plc.blocked_reason.is_none(),
            "a git repo should satisfy PLC control-logic"
        );
    }

    #[test]
    fn plc_composite_lights_up_device_scans_with_firmware_and_url() {
        // A CODESYS-on-Yocto device: PLC project + firmware image + WebVisu URL.
        let t = target_with(
            TargetType::PlcSps,
            vec![
                Artifact::plc_project("p.xml", PlcFormat::PlcopenXml),
                Artifact::firmware_image("device.img"),
                Artifact::live_url("http://plc.local/webvisu"),
            ],
        );
        let opts = applicable_scans(&t);
        for scan in [
            ScanType::PlcControlLogic,
            ScanType::FirmwareStatic,
            ScanType::Sbom,
            ScanType::Cve,
        ] {
            let o = option(&opts, scan).expect("scan offered");
            assert!(o.blocked_reason.is_none(), "{scan} should be unblocked");
        }
        // Control-logic auto-runs; the device-level scans are unblocked but opt-in
        // (default-off) until firmware-image execution lands (#151/#128).
        assert!(option(&opts, ScanType::PlcControlLogic).unwrap().default_on);
        assert!(!option(&opts, ScanType::Sbom).unwrap().default_on);
        assert!(!option(&opts, ScanType::Dast).unwrap().default_on);
        assert!(option(&opts, ScanType::Dast)
            .unwrap()
            .blocked_reason
            .is_none());
    }

    #[test]
    fn plc_with_modbus_url_offers_ics_probe_but_blocks_dast() {
        // A soft-PLC reachable only over Modbus/TCP (no WebVisu). The ICS probe
        // is applicable (it works off host:port), but DAST — an HTTP crawler —
        // must be blocked so it isn't offered/run against a non-web endpoint.
        let t = target_with(
            TargetType::PlcSps,
            vec![Artifact::live_url("modbus://plc-sim:502")],
        );
        let opts = applicable_scans(&t);
        let ics = option(&opts, ScanType::IcsProbe).expect("ics probe offered");
        assert!(
            ics.blocked_reason.is_none(),
            "ICS probe should be unblocked for a modbus:// endpoint"
        );
        assert!(!ics.default_on, "ICS probe stays opt-in (default-off)");
        let dast = option(&opts, ScanType::Dast).expect("dast listed");
        assert!(
            dast.blocked_reason.is_some(),
            "DAST must be blocked without an http(s) endpoint"
        );
        assert!(!dast.default_on);
    }

    #[test]
    fn plc_with_http_webvisu_offers_both_dast_and_ics_probe() {
        // A PLC exposing a WebVisu over HTTP: both DAST (web) and the ICS probe
        // (OT ports on the same host) are applicable.
        let t = target_with(
            TargetType::PlcSps,
            vec![Artifact::live_url("http://plc.local/webvisu")],
        );
        let opts = applicable_scans(&t);
        assert!(option(&opts, ScanType::Dast)
            .expect("dast offered")
            .blocked_reason
            .is_none());
        assert!(option(&opts, ScanType::IcsProbe)
            .expect("ics probe offered")
            .blocked_reason
            .is_none());
    }

    #[test]
    fn pentest_support_matches_reachable_families() {
        assert!(supports_pentest(TargetType::WebApp));
        assert!(supports_pentest(TargetType::BackendService));
        assert!(supports_pentest(TargetType::EmbeddedLinuxYocto));
        // A PLC/SPS device is network-reachable (WebVisu / OPC UA / 11740).
        assert!(supports_pentest(TargetType::PlcSps));
        assert!(!supports_pentest(TargetType::FirmwareBareMetal));
        assert!(!supports_pentest(TargetType::DesktopApp));
    }

    #[test]
    fn every_target_type_has_at_least_one_rule() {
        for tt in [
            TargetType::WebApp,
            TargetType::BackendService,
            TargetType::DesktopApp,
            TargetType::AndroidApp,
            TargetType::IosApp,
            TargetType::FirmwareBareMetal,
            TargetType::FirmwareRtos,
            TargetType::EmbeddedLinuxYocto,
            TargetType::PlcSps,
        ] {
            assert!(!rules_for(tt).is_empty(), "{tt} has no rules");
        }
    }
}
