// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::BTreeSet;

use thiserror::Error;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArtifactSourceKind {
    WorkspaceBuild,
    ThirdPartyDownload,
    GeneratedReleaseAsset,
}

impl ArtifactSourceKind {
    pub const fn requires_digest(self) -> bool {
        matches!(self, Self::ThirdPartyDownload | Self::GeneratedReleaseAsset)
    }

    pub const fn requires_provenance(self) -> bool {
        matches!(self, Self::GeneratedReleaseAsset)
    }

    pub const fn requires_signature(self) -> bool {
        matches!(self, Self::GeneratedReleaseAsset)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtifactIntegrityPolicy {
    artifact: &'static str,
    source: ArtifactSourceKind,
    digest_required: bool,
    signature_required: bool,
    provenance_required: bool,
}

impl ArtifactIntegrityPolicy {
    pub const fn new(
        artifact: &'static str,
        source: ArtifactSourceKind,
        digest_required: bool,
        signature_required: bool,
        provenance_required: bool,
    ) -> Self {
        Self {
            artifact,
            source,
            digest_required,
            signature_required,
            provenance_required,
        }
    }

    pub const fn artifact(self) -> &'static str {
        self.artifact
    }

    pub const fn source(self) -> ArtifactSourceKind {
        self.source
    }

    pub const fn digest_required(self) -> bool {
        self.digest_required
    }

    pub const fn signature_required(self) -> bool {
        self.signature_required
    }

    pub const fn provenance_required(self) -> bool {
        self.provenance_required
    }
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum SupplyChainPolicyError {
    #[error("artifact integrity policy at index {index} has an empty artifact")]
    EmptyArtifact { index: usize },

    #[error("artifact integrity policy for {artifact} must require a digest")]
    DigestRequired { artifact: &'static str },

    #[error("artifact integrity policy for {artifact} must require provenance")]
    ProvenanceRequired { artifact: &'static str },

    #[error("artifact integrity policy for {artifact} must require a signature")]
    SignatureRequired { artifact: &'static str },

    #[error("duplicate artifact integrity policy for {artifact}")]
    DuplicateArtifact { artifact: &'static str },
}

pub fn validate_artifact_integrity_policies(policies: &[ArtifactIntegrityPolicy]) -> Result<(), SupplyChainPolicyError> {
    let mut artifacts = BTreeSet::new();

    for (index, policy) in policies.iter().copied().enumerate() {
        if policy.artifact.trim().is_empty() {
            return Err(SupplyChainPolicyError::EmptyArtifact { index });
        }

        if policy.source.requires_digest() && !policy.digest_required {
            return Err(SupplyChainPolicyError::DigestRequired {
                artifact: policy.artifact,
            });
        }

        if policy.source.requires_provenance() && !policy.provenance_required {
            return Err(SupplyChainPolicyError::ProvenanceRequired {
                artifact: policy.artifact,
            });
        }

        if policy.source.requires_signature() && !policy.signature_required {
            return Err(SupplyChainPolicyError::SignatureRequired {
                artifact: policy.artifact,
            });
        }

        if !artifacts.insert(policy.artifact) {
            return Err(SupplyChainPolicyError::DuplicateArtifact {
                artifact: policy.artifact,
            });
        }
    }

    Ok(())
}

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

    #[test]
    fn validates_artifact_integrity_policies() {
        let policies = [
            ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, true, false),
            ArtifactIntegrityPolicy::new(
                "rustfs-cli-windows-amd64.zip",
                ArtifactSourceKind::GeneratedReleaseAsset,
                true,
                true,
                true,
            ),
        ];

        assert!(validate_artifact_integrity_policies(&policies).is_ok());
        assert_eq!(policies[0].artifact(), "rustfs-server");
        assert_eq!(policies[0].source(), ArtifactSourceKind::WorkspaceBuild);
        assert!(policies[1].digest_required());
        assert!(policies[1].signature_required());
        assert!(policies[1].provenance_required());
    }

    #[test]
    fn rejects_empty_artifacts() {
        let policies = [ArtifactIntegrityPolicy::new(
            " ",
            ArtifactSourceKind::WorkspaceBuild,
            false,
            false,
            false,
        )];

        let err = validate_artifact_integrity_policies(&policies).expect_err("empty artifact should fail validation");

        assert_eq!(err, SupplyChainPolicyError::EmptyArtifact { index: 0 });
    }

    #[test]
    fn rejects_external_artifacts_without_digest() {
        let policies = [ArtifactIntegrityPolicy::new(
            "third-party-tool",
            ArtifactSourceKind::ThirdPartyDownload,
            false,
            false,
            false,
        )];

        let err = validate_artifact_integrity_policies(&policies).expect_err("third-party artifact should require digest");

        assert_eq!(
            err,
            SupplyChainPolicyError::DigestRequired {
                artifact: "third-party-tool"
            }
        );
    }

    #[test]
    fn rejects_generated_artifacts_without_digest() {
        let policies = [ArtifactIntegrityPolicy::new(
            "rustfs-cli-windows-amd64.zip",
            ArtifactSourceKind::GeneratedReleaseAsset,
            false,
            true,
            true,
        )];

        let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require digest");

        assert_eq!(
            err,
            SupplyChainPolicyError::DigestRequired {
                artifact: "rustfs-cli-windows-amd64.zip"
            }
        );
    }

    #[test]
    fn rejects_generated_artifacts_without_provenance() {
        let policies = [ArtifactIntegrityPolicy::new(
            "rustfs-cli-windows-amd64.zip",
            ArtifactSourceKind::GeneratedReleaseAsset,
            true,
            true,
            false,
        )];

        let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require provenance");

        assert_eq!(
            err,
            SupplyChainPolicyError::ProvenanceRequired {
                artifact: "rustfs-cli-windows-amd64.zip"
            }
        );
    }

    #[test]
    fn rejects_generated_artifacts_without_signature() {
        let policies = [ArtifactIntegrityPolicy::new(
            "rustfs-cli-windows-amd64.zip",
            ArtifactSourceKind::GeneratedReleaseAsset,
            true,
            false,
            true,
        )];

        let err = validate_artifact_integrity_policies(&policies).expect_err("generated artifact should require signature");

        assert_eq!(
            err,
            SupplyChainPolicyError::SignatureRequired {
                artifact: "rustfs-cli-windows-amd64.zip"
            }
        );
    }

    #[test]
    fn rejects_duplicate_artifacts() {
        let policies = [
            ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, true, false),
            ArtifactIntegrityPolicy::new("rustfs-server", ArtifactSourceKind::WorkspaceBuild, false, false, false),
        ];

        let err = validate_artifact_integrity_policies(&policies).expect_err("duplicate artifact should fail validation");

        assert_eq!(
            err,
            SupplyChainPolicyError::DuplicateArtifact {
                artifact: "rustfs-server"
            }
        );
    }
}
