use serde::Serialize;

use crate::{
    grant_store::{SandboxGrantStore, SandboxGrantStoreError},
    manifest::{
        ManifestError, PreparedProductBundle, ProductConsentSummary, ProductManifest, ProductSource,
    },
    permissions::{EffectiveSandboxPermissions, SandboxGrant, SandboxPermissionResolution},
};

/// Shared host-side context for preflighting a verified product bundle before load.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProductPreflightContext {
    pub bundle_hash_or_cid: String,
    pub source: ProductSource,
    /// `None` means the host cannot yet evaluate chain support and wants to skip
    /// the required-chain denial step for this load attempt.
    pub supported_chain_genesis_hashes: Option<Vec<String>>,
}

/// High-level allow/prompt/deny outcome for a verified bundle.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum ProductPreflightOutcome {
    Allow {
        effective_permissions: EffectiveSandboxPermissions,
    },
    Deny {
        reason: ProductPreflightDenyReason,
    },
    Prompt(Box<ProductPreflightPrompt>),
}

/// Host-visible prompt payload derived from a verified manifest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProductPreflightPrompt {
    pub summary: ProductConsentSummary,
    pub proposed_grant: SandboxGrant,
    pub deny_behavior: ConsentDenyBehavior,
}

/// What the host should do if the user denies the prompt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ConsentDenyBehavior {
    LoadInStrictSandbox,
    DenyLoad,
}

/// User decision returned from a host-owned consent UI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProductConsentDecision {
    Approve,
    Deny,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum ProductPreflightDenyReason {
    UnsupportedRequiredChains { genesis_hashes: Vec<String> },
    RequiredSandboxConsentDenied,
}

#[derive(Debug, thiserror::Error)]
pub enum ProductPreflightError {
    #[error(transparent)]
    Manifest(#[from] ManifestError),

    #[error(transparent)]
    GrantStore(#[from] SandboxGrantStoreError),
}

impl ProductPreflightPrompt {
    /// Resolve a host-owned user decision into the final allow/deny outcome.
    pub fn resolve_with_store<S: SandboxGrantStore>(
        &self,
        decision: ProductConsentDecision,
        grant_store: &mut S,
    ) -> Result<ProductPreflightOutcome, ProductPreflightError> {
        match decision {
            ProductConsentDecision::Approve => {
                grant_store.save_grant(self.proposed_grant.clone())?;
                Ok(ProductPreflightOutcome::Allow {
                    effective_permissions: EffectiveSandboxPermissions::from_grant(
                        &self.proposed_grant,
                    ),
                })
            }
            ProductConsentDecision::Deny => match self.deny_behavior {
                ConsentDenyBehavior::LoadInStrictSandbox => Ok(ProductPreflightOutcome::Allow {
                    effective_permissions: EffectiveSandboxPermissions::default(),
                }),
                ConsentDenyBehavior::DenyLoad => Ok(ProductPreflightOutcome::Deny {
                    reason: ProductPreflightDenyReason::RequiredSandboxConsentDenied,
                }),
            },
        }
    }
}

impl ProductManifest {
    /// Evaluate a verified manifest into a host-facing allow/prompt/deny decision.
    pub fn preflight_load<S: SandboxGrantStore>(
        &self,
        context: &ProductPreflightContext,
        grant_store: &S,
    ) -> Result<ProductPreflightOutcome, ProductPreflightError> {
        self.validate()?;

        if let Some(supported_chain_genesis_hashes) = &context.supported_chain_genesis_hashes {
            if let Err(ManifestError::UnsupportedRequiredChains { genesis_hashes }) =
                self.validate_required_chains_supported(supported_chain_genesis_hashes)
            {
                return Ok(ProductPreflightOutcome::Deny {
                    reason: ProductPreflightDenyReason::UnsupportedRequiredChains {
                        genesis_hashes,
                    },
                });
            }
        }

        let stored_grant = grant_store.load_grant(&self.product.id, &context.bundle_hash_or_cid)?;

        match self.evaluate_sandbox_permissions(
            context.bundle_hash_or_cid.clone(),
            context.source,
            context
                .supported_chain_genesis_hashes
                .as_deref()
                .unwrap_or_default(),
            stored_grant.as_ref(),
        )? {
            SandboxPermissionResolution::NoExceptionsRequested => {
                Ok(ProductPreflightOutcome::Allow {
                    effective_permissions: EffectiveSandboxPermissions::default(),
                })
            }
            SandboxPermissionResolution::Approved(effective_permissions) => {
                Ok(ProductPreflightOutcome::Allow {
                    effective_permissions,
                })
            }
            SandboxPermissionResolution::RequiresUserConsent {
                summary,
                proposed_grant,
            } => {
                let deny_behavior = if summary
                    .verified
                    .sandbox_requests
                    .iter()
                    .any(|request| request.required)
                {
                    ConsentDenyBehavior::DenyLoad
                } else {
                    ConsentDenyBehavior::LoadInStrictSandbox
                };

                Ok(ProductPreflightOutcome::Prompt(Box::new(
                    ProductPreflightPrompt {
                        summary: *summary,
                        proposed_grant,
                        deny_behavior,
                    },
                )))
            }
        }
    }
}

impl PreparedProductBundle {
    /// Convenience wrapper over [`ProductManifest::preflight_load`].
    pub fn preflight_load<S: SandboxGrantStore>(
        &self,
        context: &ProductPreflightContext,
        grant_store: &S,
    ) -> Result<ProductPreflightOutcome, ProductPreflightError> {
        let Some(manifest) = &self.manifest else {
            return Ok(ProductPreflightOutcome::Allow {
                effective_permissions: EffectiveSandboxPermissions::default(),
            });
        };

        manifest.preflight_load(context, grant_store)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        grant_store::InMemorySandboxGrantStore,
        manifest::{
            HostChainRequirements, HostManifest, HostSandboxRequest, HostSandboxRequirements,
            ProductMetadata, ProductModality, PublisherMetadata, SandboxRequestKind,
        },
    };

    fn manifest(required_request: bool) -> ProductManifest {
        ProductManifest {
            product: 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: HostManifest {
                manifest_version: 1,
                chains: HostChainRequirements {
                    required: false,
                    targets: Vec::new(),
                },
                sandbox: HostSandboxRequirements {
                    version: 1,
                    requests: vec![HostSandboxRequest {
                        kind: SandboxRequestKind::Network,
                        required: required_request,
                        origins: vec!["https://api.example.com".into()],
                        reason: Some("Fetches metadata".into()),
                    }],
                },
                analytics: None,
            },
        }
    }

    fn context() -> ProductPreflightContext {
        ProductPreflightContext {
            bundle_hash_or_cid: "bafytest".into(),
            source: ProductSource::Dotns,
            supported_chain_genesis_hashes: Some(Vec::new()),
        }
    }

    #[test]
    fn preflight_prompts_when_matching_grant_is_missing() {
        let manifest = manifest(false);
        let store = InMemorySandboxGrantStore::default();

        let outcome = manifest.preflight_load(&context(), &store).unwrap();

        match outcome {
            ProductPreflightOutcome::Prompt(prompt) => {
                assert_eq!(prompt.summary.verified.product_id, "example.dot");
                assert_eq!(
                    prompt.deny_behavior,
                    ConsentDenyBehavior::LoadInStrictSandbox
                );
            }
            other => panic!("unexpected outcome: {other:?}"),
        }
    }

    #[test]
    fn approving_prompt_saves_grant_and_allows_permissions() {
        let manifest = manifest(false);
        let store = InMemorySandboxGrantStore::default();
        let ProductPreflightOutcome::Prompt(prompt) =
            manifest.preflight_load(&context(), &store).unwrap()
        else {
            panic!("expected prompt");
        };

        let mut store = store;
        let outcome = prompt
            .resolve_with_store(ProductConsentDecision::Approve, &mut store)
            .unwrap();

        assert_eq!(
            outcome,
            ProductPreflightOutcome::Allow {
                effective_permissions: EffectiveSandboxPermissions {
                    approved_network_origins: vec!["https://api.example.com".into()],
                },
            }
        );
        assert_eq!(store.grants().len(), 1);
    }

    #[test]
    fn denying_optional_prompt_falls_back_to_strict_sandbox() {
        let manifest = manifest(false);
        let store = InMemorySandboxGrantStore::default();
        let ProductPreflightOutcome::Prompt(prompt) =
            manifest.preflight_load(&context(), &store).unwrap()
        else {
            panic!("expected prompt");
        };

        let mut store = store;
        let outcome = prompt
            .resolve_with_store(ProductConsentDecision::Deny, &mut store)
            .unwrap();

        assert_eq!(
            outcome,
            ProductPreflightOutcome::Allow {
                effective_permissions: EffectiveSandboxPermissions::default(),
            }
        );
        assert!(store.grants().is_empty());
    }

    #[test]
    fn denying_required_prompt_denies_load() {
        let manifest = manifest(true);
        let store = InMemorySandboxGrantStore::default();
        let ProductPreflightOutcome::Prompt(prompt) =
            manifest.preflight_load(&context(), &store).unwrap()
        else {
            panic!("expected prompt");
        };

        let mut store = store;
        let outcome = prompt
            .resolve_with_store(ProductConsentDecision::Deny, &mut store)
            .unwrap();

        assert_eq!(
            outcome,
            ProductPreflightOutcome::Deny {
                reason: ProductPreflightDenyReason::RequiredSandboxConsentDenied,
            }
        );
    }

    #[test]
    fn preflight_denies_unsupported_required_chains() {
        let mut manifest = manifest(false);
        manifest
            .host
            .chains
            .targets
            .push(crate::manifest::HostChainTarget {
                genesis_hash: "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122"
                    .into(),
                label: Some("polkadot".into()),
                required: true,
            });

        let outcome = manifest
            .preflight_load(&context(), &InMemorySandboxGrantStore::default())
            .unwrap();

        assert_eq!(
            outcome,
            ProductPreflightOutcome::Deny {
                reason: ProductPreflightDenyReason::UnsupportedRequiredChains {
                    genesis_hashes: vec![
                        "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122".into()
                    ],
                },
            }
        );
    }
}
