use serde::{Deserialize, Serialize};

use crate::manifest::{
    HostSandboxRequest, ManifestError, PreparedProductBundle, ProductConsentSummary,
    ProductManifest, ProductSource, SandboxRequestKind,
};

/// Exact user-approved sandbox grant for one verified product artifact.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SandboxGrant {
    pub product_id: String,
    pub bundle_hash_or_cid: String,
    pub requests: Vec<GrantedSandboxRequest>,
}

impl SandboxGrant {
    /// Build the exact grant key a host would persist for this manifest.
    pub fn for_manifest(manifest: &ProductManifest, bundle_hash_or_cid: impl Into<String>) -> Self {
        Self {
            product_id: manifest.product.id.clone(),
            bundle_hash_or_cid: bundle_hash_or_cid.into(),
            requests: canonicalize_requests(&manifest.host.sandbox.requests),
        }
    }

    /// Returns true only when this stored grant exactly matches the artifact
    /// and requested sandbox exception set in the manifest.
    pub fn matches_manifest(&self, manifest: &ProductManifest, bundle_hash_or_cid: &str) -> bool {
        self.product_id == manifest.product.id
            && self.bundle_hash_or_cid == bundle_hash_or_cid
            && self.requests == canonicalize_requests(&manifest.host.sandbox.requests)
    }
}

/// Canonical granted sandbox request shape used for persistence and matching.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct GrantedSandboxRequest {
    pub kind: SandboxRequestKind,
    pub origins: Vec<String>,
}

/// Effective sandbox deltas a host may apply after grant resolution.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct EffectiveSandboxPermissions {
    pub approved_network_origins: Vec<String>,
}

impl EffectiveSandboxPermissions {
    pub fn from_grant(grant: &SandboxGrant) -> Self {
        let mut approved_network_origins = grant
            .requests
            .iter()
            .filter(|request| request.kind == SandboxRequestKind::Network)
            .flat_map(|request| request.origins.iter().cloned())
            .collect::<Vec<_>>();
        approved_network_origins.sort();
        approved_network_origins.dedup();

        Self {
            approved_network_origins,
        }
    }

    pub fn from_manifest(manifest: &ProductManifest) -> Self {
        let mut approved_network_origins = manifest
            .host
            .sandbox
            .requests
            .iter()
            .filter(|request| request.kind == SandboxRequestKind::Network)
            .flat_map(|request| request.origins.iter().cloned())
            .collect::<Vec<_>>();
        approved_network_origins.sort();
        approved_network_origins.dedup();

        Self {
            approved_network_origins,
        }
    }
}

/// Result of comparing a verified manifest to a stored user grant.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum SandboxPermissionResolution {
    /// The product requests no sandbox exceptions.
    NoExceptionsRequested,
    /// The product requests exceptions and the stored grant matches exactly.
    Approved(EffectiveSandboxPermissions),
    /// The product requests exceptions and the host must prompt the user.
    RequiresUserConsent {
        summary: Box<ProductConsentSummary>,
        proposed_grant: SandboxGrant,
    },
}

impl ProductManifest {
    /// Resolve sandbox exception requests against an optional stored grant.
    pub fn evaluate_sandbox_permissions(
        &self,
        bundle_hash_or_cid: impl Into<String>,
        source: ProductSource,
        supported_chain_genesis_hashes: &[String],
        stored_grant: Option<&SandboxGrant>,
    ) -> Result<SandboxPermissionResolution, ManifestError> {
        self.validate()?;

        if self.host.sandbox.requests.is_empty() {
            return Ok(SandboxPermissionResolution::NoExceptionsRequested);
        }

        let bundle_hash_or_cid = bundle_hash_or_cid.into();
        if stored_grant.is_some_and(|grant| grant.matches_manifest(self, &bundle_hash_or_cid)) {
            return Ok(SandboxPermissionResolution::Approved(
                EffectiveSandboxPermissions::from_manifest(self),
            ));
        }

        let summary = self.build_consent_summary(
            bundle_hash_or_cid.clone(),
            source,
            supported_chain_genesis_hashes,
        )?;
        Ok(SandboxPermissionResolution::RequiresUserConsent {
            summary: Box::new(summary),
            proposed_grant: SandboxGrant::for_manifest(self, bundle_hash_or_cid),
        })
    }
}

impl PreparedProductBundle {
    /// Convenience wrapper over [`ProductManifest::evaluate_sandbox_permissions`].
    pub fn evaluate_sandbox_permissions(
        &self,
        bundle_hash_or_cid: impl Into<String>,
        source: ProductSource,
        supported_chain_genesis_hashes: &[String],
        stored_grant: Option<&SandboxGrant>,
    ) -> Result<SandboxPermissionResolution, ManifestError> {
        let Some(manifest) = &self.manifest else {
            return Ok(SandboxPermissionResolution::NoExceptionsRequested);
        };

        manifest.evaluate_sandbox_permissions(
            bundle_hash_or_cid,
            source,
            supported_chain_genesis_hashes,
            stored_grant,
        )
    }
}

fn canonicalize_requests(requests: &[HostSandboxRequest]) -> Vec<GrantedSandboxRequest> {
    let mut canonical = requests
        .iter()
        .map(|request| {
            let mut origins = request.origins.clone();
            origins.sort();
            origins.dedup();
            GrantedSandboxRequest {
                kind: request.kind,
                origins,
            }
        })
        .collect::<Vec<_>>();
    canonical.sort();
    canonical
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::manifest::{ProductModality, PublisherMetadata, SandboxRequestKind};

    fn manifest_with_requests() -> ProductManifest {
        ProductManifest {
            product: crate::manifest::ProductMetadata {
                id: "example.dot".into(),
                name: "Example".into(),
                version: "1.0.0".into(),
                description: None,
                icon: None,
                modality: ProductModality::Spa,
            },
            publisher: Some(PublisherMetadata {
                display_name: Some("Example Labs".into()),
                account: Some("5F...".into()),
            }),
            host: crate::manifest::HostManifest {
                manifest_version: 1,
                chains: crate::manifest::HostChainRequirements {
                    required: false,
                    targets: Vec::new(),
                },
                sandbox: crate::manifest::HostSandboxRequirements {
                    version: 1,
                    requests: vec![HostSandboxRequest {
                        kind: SandboxRequestKind::Network,
                        required: false,
                        origins: vec![
                            "https://b.example.com".into(),
                            "https://a.example.com".into(),
                        ],
                        reason: Some("Fetches metadata".into()),
                    }],
                },
                analytics: None,
            },
        }
    }

    #[test]
    fn grant_matches_manifest_ignoring_origin_order() {
        let manifest = manifest_with_requests();
        let grant = SandboxGrant {
            product_id: "example.dot".into(),
            bundle_hash_or_cid: "bafytest".into(),
            requests: vec![GrantedSandboxRequest {
                kind: SandboxRequestKind::Network,
                origins: vec![
                    "https://a.example.com".into(),
                    "https://b.example.com".into(),
                ],
            }],
        };

        assert!(grant.matches_manifest(&manifest, "bafytest"));
    }

    #[test]
    fn evaluate_permissions_returns_no_requests_when_manifest_is_empty() {
        let mut manifest = manifest_with_requests();
        manifest.host.sandbox.requests.clear();

        let resolution = manifest
            .evaluate_sandbox_permissions("bafytest", ProductSource::Dotns, &[], None)
            .unwrap();

        assert_eq!(
            resolution,
            SandboxPermissionResolution::NoExceptionsRequested
        );
    }

    #[test]
    fn evaluate_permissions_requires_consent_without_matching_grant() {
        let manifest = manifest_with_requests();

        let resolution = manifest
            .evaluate_sandbox_permissions("bafytest", ProductSource::Dotns, &[], None)
            .unwrap();

        match resolution {
            SandboxPermissionResolution::RequiresUserConsent {
                summary,
                proposed_grant,
            } => {
                assert_eq!(summary.as_ref().verified.product_id, "example.dot");
                assert_eq!(proposed_grant.product_id, "example.dot");
                assert_eq!(proposed_grant.bundle_hash_or_cid, "bafytest");
            }
            other => panic!("unexpected resolution: {other:?}"),
        }
    }

    #[test]
    fn evaluate_permissions_approves_matching_grant() {
        let manifest = manifest_with_requests();
        let grant = SandboxGrant::for_manifest(&manifest, "bafytest");

        let resolution = manifest
            .evaluate_sandbox_permissions("bafytest", ProductSource::Dotns, &[], Some(&grant))
            .unwrap();

        assert_eq!(
            resolution,
            SandboxPermissionResolution::Approved(EffectiveSandboxPermissions {
                approved_network_origins: vec![
                    "https://a.example.com".into(),
                    "https://b.example.com".into(),
                ],
            }),
        );
    }

    #[test]
    fn evaluate_permissions_reprompts_when_bundle_changes() {
        let manifest = manifest_with_requests();
        let grant = SandboxGrant::for_manifest(&manifest, "bafyold");

        let resolution = manifest
            .evaluate_sandbox_permissions("bafynew", ProductSource::Dotns, &[], Some(&grant))
            .unwrap();

        assert!(matches!(
            resolution,
            SandboxPermissionResolution::RequiresUserConsent { .. }
        ));
    }

    #[test]
    fn prepared_bundle_without_manifest_has_no_exceptions() {
        let bundle = PreparedProductBundle {
            assets: crate::state::ProductAssets::from_html(b"<html></html>".to_vec()),
            manifest: None,
        };

        let resolution = bundle
            .evaluate_sandbox_permissions("bafytest", ProductSource::File, &[], None)
            .unwrap();

        assert_eq!(
            resolution,
            SandboxPermissionResolution::NoExceptionsRequested
        );
    }
}
