use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

use crate::state::ProductAssets;

/// Proposed manifest envelope for hosted products.
///
/// The manifest separates product metadata, optional publisher metadata, and
/// host-owned requirements under the `host` namespace.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProductManifest {
    pub product: ProductMetadata,
    #[serde(default)]
    pub publisher: Option<PublisherMetadata>,
    pub host: HostManifest,
}

impl ProductManifest {
    /// Parse and validate a manifest from TOML text.
    pub fn from_toml_str(value: &str) -> Result<Self, ProductBundleError> {
        let manifest: ProductManifest =
            toml::from_str(value).map_err(|error| ProductBundleError::ManifestParseFailed {
                message: error.to_string(),
            })?;
        manifest.validate()?;
        Ok(manifest)
    }

    /// Parse and validate a manifest from TOML bytes.
    pub fn from_toml_bytes(value: &[u8]) -> Result<Self, ProductBundleError> {
        let text = std::str::from_utf8(value).map_err(|_| ProductBundleError::ManifestNotUtf8)?;
        Self::from_toml_str(text)
    }

    /// Validate the manifest against the current v1 design rules.
    pub fn validate(&self) -> Result<(), ManifestError> {
        if self.product.id.trim().is_empty() {
            return Err(ManifestError::MissingField("product.id"));
        }
        if self.product.name.trim().is_empty() {
            return Err(ManifestError::MissingField("product.name"));
        }
        if self.product.version.trim().is_empty() {
            return Err(ManifestError::MissingField("product.version"));
        }
        if self.product.modality != ProductModality::Spa {
            return Err(ManifestError::UnsupportedModality {
                found: self.product.modality.as_str().to_string(),
            });
        }
        if self.host.manifest_version != 1 {
            return Err(ManifestError::UnsupportedManifestVersion {
                found: self.host.manifest_version,
            });
        }

        for chain in &self.host.chains.targets {
            validate_genesis_hash(&chain.genesis_hash)?;
        }

        if self.host.sandbox.version != 1 {
            return Err(ManifestError::UnsupportedSandboxVersion {
                found: self.host.sandbox.version,
            });
        }

        for request in &self.host.sandbox.requests {
            request.validate()?;
        }

        Ok(())
    }

    /// Ensure every required chain is supported by the host before load.
    pub fn validate_required_chains_supported(
        &self,
        supported_chain_genesis_hashes: &[String],
    ) -> Result<(), ManifestError> {
        let supported: HashSet<String> = supported_chain_genesis_hashes
            .iter()
            .map(|s| s.to_ascii_lowercase())
            .collect();

        let unsupported: Vec<String> = self
            .host
            .chains
            .targets
            .iter()
            .filter(|chain| {
                chain.required && !supported.contains(&chain.genesis_hash.to_ascii_lowercase())
            })
            .map(|chain| chain.genesis_hash.clone())
            .collect();

        if unsupported.is_empty() {
            Ok(())
        } else {
            Err(ManifestError::UnsupportedRequiredChains {
                genesis_hashes: unsupported,
            })
        }
    }

    /// Build a user-facing consent summary from the manifest and the host's
    /// current context. Validation is run before summary generation.
    pub fn build_consent_summary(
        &self,
        bundle_hash_or_cid: impl Into<String>,
        source: ProductSource,
        supported_chain_genesis_hashes: &[String],
    ) -> Result<ProductConsentSummary, ManifestError> {
        self.validate()?;

        let supported: HashSet<String> = supported_chain_genesis_hashes
            .iter()
            .map(|s| s.to_ascii_lowercase())
            .collect();

        let verified = VerifiedConsentSummary {
            product_id: self.product.id.clone(),
            version: self.product.version.clone(),
            modality: self.product.modality,
            bundle_hash_or_cid: bundle_hash_or_cid.into(),
            source,
            publisher_account: self.publisher.as_ref().and_then(|p| p.account.clone()),
            chains: self
                .host
                .chains
                .targets
                .iter()
                .map(|chain| ConsentChainTarget {
                    genesis_hash: chain.genesis_hash.clone(),
                    label: chain.label.clone(),
                    required: chain.required,
                    supported_by_host: supported.contains(&chain.genesis_hash.to_ascii_lowercase()),
                })
                .collect(),
            sandbox_requests: self
                .host
                .sandbox
                .requests
                .iter()
                .map(|request| ConsentSandboxRequest {
                    kind: request.kind,
                    required: request.required,
                    origins: request.origins.clone(),
                    reason: request.reason.clone(),
                })
                .collect(),
        };

        let presentation = PresentationConsentSummary {
            name: Some(self.product.name.clone()),
            description: self.product.description.clone(),
            icon_path: self.product.icon.clone(),
            publisher_display_name: self.publisher.as_ref().and_then(|p| p.display_name.clone()),
        };

        Ok(ProductConsentSummary {
            verified,
            presentation,
        })
    }
}

/// Prepared product bundle ready for ProductView loading.
///
/// Converts a raw asset map into loadable `ProductAssets` and parses an
/// optional `manifest.toml` when present.
#[derive(Debug, Clone)]
pub struct PreparedProductBundle {
    pub assets: ProductAssets,
    pub manifest: Option<ProductManifest>,
}

impl PreparedProductBundle {
    pub fn from_asset_map(
        mut asset_map: HashMap<String, Vec<u8>>,
    ) -> Result<Self, ProductBundleError> {
        let index_html = asset_map
            .remove("index.html")
            .ok_or(ProductBundleError::MissingIndexHtml)?;

        let manifest = match asset_map.get("manifest.toml") {
            Some(bytes) => Some(ProductManifest::from_toml_bytes(bytes)?),
            None => None,
        };

        Ok(Self {
            assets: ProductAssets {
                index_html,
                index_mime: "text/html; charset=utf-8".to_string(),
                resources: asset_map,
            },
            manifest,
        })
    }

    pub fn build_consent_summary(
        &self,
        bundle_hash_or_cid: impl Into<String>,
        source: ProductSource,
        supported_chain_genesis_hashes: &[String],
    ) -> Result<Option<ProductConsentSummary>, ManifestError> {
        self.manifest
            .as_ref()
            .map(|manifest| {
                manifest.build_consent_summary(
                    bundle_hash_or_cid.into(),
                    source,
                    supported_chain_genesis_hashes,
                )
            })
            .transpose()
    }

    pub fn validate_required_chains_supported(
        &self,
        supported_chain_genesis_hashes: &[String],
    ) -> Result<(), ManifestError> {
        if let Some(manifest) = &self.manifest {
            manifest.validate_required_chains_supported(supported_chain_genesis_hashes)?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProductMetadata {
    pub id: String,
    pub name: String,
    pub version: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub icon: Option<String>,
    pub modality: ProductModality,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PublisherMetadata {
    #[serde(default)]
    pub display_name: Option<String>,
    #[serde(default)]
    pub account: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HostManifest {
    pub manifest_version: u32,
    pub chains: HostChainRequirements,
    pub sandbox: HostSandboxRequirements,
    /// Optional analytics configuration declared by the product developer.
    ///
    /// When present, the host may route product-side analytics events to the
    /// declared endpoint. The host is free to ignore this section entirely
    /// or present it as a consent prompt during preflight.
    #[serde(default)]
    pub analytics: Option<AnalyticsConfig>,
}

/// Analytics configuration declared in a product manifest.
///
/// Products declare where they want their analytics events routed. The host
/// mediates all event delivery — products cannot contact analytics backends
/// directly (they are sandboxed).
///
/// # Example
///
/// ```toml
/// [host.analytics]
/// endpoint = "https://us.i.posthog.com/capture/"
/// api_key = "phc_abc123..."
/// ```
#[derive(Clone, Deserialize, Serialize)]
pub struct AnalyticsConfig {
    /// HTTPS endpoint that accepts Segment-shaped JSON event payloads.
    ///
    /// Must be an `https://` URL. The host sends POST requests with a JSON body
    /// following the Segment `track`/`identify`/`page` schema.
    #[serde(default)]
    pub endpoint: Option<String>,
    /// API key or project key for the analytics backend.
    ///
    /// Interpretation depends on the endpoint (e.g. PostHog project API key,
    /// RudderStack write key). The host includes this in the `Authorization`
    /// header or request body as appropriate.
    #[serde(default)]
    pub api_key: Option<String>,
}

impl std::fmt::Debug for AnalyticsConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AnalyticsConfig")
            .field("endpoint", &self.endpoint)
            .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]"))
            .finish()
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HostChainRequirements {
    pub required: bool,
    #[serde(default)]
    pub targets: Vec<HostChainTarget>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HostChainTarget {
    pub genesis_hash: String,
    #[serde(default)]
    pub label: Option<String>,
    #[serde(default)]
    pub required: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HostSandboxRequirements {
    pub version: u32,
    #[serde(default)]
    pub requests: Vec<HostSandboxRequest>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HostSandboxRequest {
    pub kind: SandboxRequestKind,
    #[serde(default)]
    pub required: bool,
    #[serde(default)]
    pub origins: Vec<String>,
    #[serde(default)]
    pub reason: Option<String>,
}

impl HostSandboxRequest {
    fn validate(&self) -> Result<(), ManifestError> {
        if self.origins.is_empty() {
            return Err(ManifestError::EmptyOrigins {
                kind: self.kind.as_str().to_string(),
            });
        }

        match self.kind {
            SandboxRequestKind::Network => {
                for origin in &self.origins {
                    validate_https_origin(origin)?;
                }
            }
        }

        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProductModality {
    Spa,
}

impl ProductModality {
    pub fn as_str(self) -> &'static str {
        match self {
            ProductModality::Spa => "spa",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxRequestKind {
    Network,
}

impl SandboxRequestKind {
    pub fn as_str(self) -> &'static str {
        match self {
            SandboxRequestKind::Network => "network",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProductSource {
    Dotns,
    File,
    Sideload,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProductConsentSummary {
    pub verified: VerifiedConsentSummary,
    pub presentation: PresentationConsentSummary,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct VerifiedConsentSummary {
    pub product_id: String,
    pub version: String,
    pub modality: ProductModality,
    pub bundle_hash_or_cid: String,
    pub source: ProductSource,
    pub publisher_account: Option<String>,
    pub chains: Vec<ConsentChainTarget>,
    pub sandbox_requests: Vec<ConsentSandboxRequest>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PresentationConsentSummary {
    pub name: Option<String>,
    pub description: Option<String>,
    pub icon_path: Option<String>,
    pub publisher_display_name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConsentChainTarget {
    pub genesis_hash: String,
    pub label: Option<String>,
    pub required: bool,
    pub supported_by_host: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConsentSandboxRequest {
    pub kind: SandboxRequestKind,
    pub required: bool,
    pub origins: Vec<String>,
    pub reason: Option<String>,
}

#[derive(Debug, thiserror::Error)]
pub enum ManifestError {
    #[error("missing required field: {0}")]
    MissingField(&'static str),

    #[error("unsupported modality: {found}")]
    UnsupportedModality { found: String },

    #[error("unsupported manifest_version: {found}")]
    UnsupportedManifestVersion { found: u32 },

    #[error("unsupported sandbox version: {found}")]
    UnsupportedSandboxVersion { found: u32 },

    #[error("invalid genesis hash: {value}")]
    InvalidGenesisHash { value: String },

    #[error("unsupported required chains: {genesis_hashes:?}")]
    UnsupportedRequiredChains { genesis_hashes: Vec<String> },

    #[error("request kind '{kind}' must declare at least one origin")]
    EmptyOrigins { kind: String },

    #[error("invalid https origin: {value}")]
    InvalidHttpsOrigin { value: String },
}

#[derive(Debug, thiserror::Error)]
pub enum ProductBundleError {
    #[error("assets map must contain index.html")]
    MissingIndexHtml,

    #[error("manifest.toml is not valid UTF-8")]
    ManifestNotUtf8,

    #[error("manifest.toml parse failed: {message}")]
    ManifestParseFailed { message: String },

    #[error(transparent)]
    Manifest(#[from] ManifestError),
}

fn validate_genesis_hash(value: &str) -> Result<(), ManifestError> {
    if value.len() != 66 || !value.starts_with("0x") {
        return Err(ManifestError::InvalidGenesisHash {
            value: value.to_string(),
        });
    }
    if !value[2..].bytes().all(|b| b.is_ascii_hexdigit()) {
        return Err(ManifestError::InvalidGenesisHash {
            value: value.to_string(),
        });
    }
    Ok(())
}

fn validate_https_origin(value: &str) -> Result<(), ManifestError> {
    let rest = value
        .strip_prefix("https://")
        .ok_or_else(|| ManifestError::InvalidHttpsOrigin {
            value: value.to_string(),
        })?;

    if rest.is_empty()
        || rest.contains('/')
        || rest.contains('?')
        || rest.contains('#')
        || rest.contains('*')
        || rest.contains(' ')
        || rest.starts_with(':')
        || rest.ends_with(':')
    {
        return Err(ManifestError::InvalidHttpsOrigin {
            value: value.to_string(),
        });
    }

    Ok(())
}

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

    fn valid_manifest_json() -> &'static str {
        r#"{
          "product": {
            "id": "example.dot",
            "name": "Example Product",
            "version": "1.2.0",
            "description": "Portfolio and staking dashboard.",
            "icon": "icon.png",
            "modality": "spa"
          },
          "publisher": {
            "display_name": "Example Labs",
            "account": "5F..."
          },
          "host": {
            "manifest_version": 1,
            "chains": {
              "required": true,
              "targets": [
                {
                  "genesis_hash": "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122",
                  "label": "polkadot",
                  "required": true
                }
              ]
            },
            "sandbox": {
              "version": 1,
              "requests": [
                {
                  "kind": "network",
                  "required": false,
                  "origins": ["https://api.example.com"],
                  "reason": "Fetches product metadata."
                }
              ]
            }
          }
        }"#
    }

    fn valid_manifest_toml() -> &'static str {
        r#"
[product]
id = "example.dot"
name = "Example Product"
version = "1.2.0"
description = "Portfolio and staking dashboard."
icon = "icon.png"
modality = "spa"

[publisher]
display_name = "Example Labs"
account = "5F..."

[host]
manifest_version = 1

[host.chains]
required = true

[[host.chains.targets]]
genesis_hash = "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122"
label = "polkadot"
required = true

[host.sandbox]
version = 1

[[host.sandbox.requests]]
kind = "network"
required = false
origins = ["https://api.example.com"]
reason = "Fetches product metadata."
"#
    }

    #[test]
    fn deserializes_manifest() {
        let manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        assert_eq!(manifest.product.id, "example.dot");
        assert_eq!(manifest.product.modality, ProductModality::Spa);
        assert_eq!(manifest.host.sandbox.requests.len(), 1);
    }

    #[test]
    fn validates_manifest() {
        let manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        manifest.validate().unwrap();
    }

    #[test]
    fn rejects_non_https_origin() {
        let mut manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        manifest.host.sandbox.requests[0].origins = vec!["http://api.example.com".into()];
        let err = manifest.validate().unwrap_err();
        assert_eq!(
            err.to_string(),
            "invalid https origin: http://api.example.com"
        );
    }

    #[test]
    fn rejects_origin_with_path() {
        let mut manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        manifest.host.sandbox.requests[0].origins = vec!["https://api.example.com/v1".into()];
        let err = manifest.validate().unwrap_err();
        assert_eq!(
            err.to_string(),
            "invalid https origin: https://api.example.com/v1"
        );
    }

    #[test]
    fn rejects_invalid_genesis_hash() {
        let mut manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        manifest.host.chains.targets[0].genesis_hash = "0x1234".into();
        let err = manifest.validate().unwrap_err();
        assert_eq!(err.to_string(), "invalid genesis hash: 0x1234");
    }

    #[test]
    fn builds_consent_summary() {
        let manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        let summary = manifest
            .build_consent_summary(
                "bafyexamplecid",
                ProductSource::Dotns,
                &[
                    "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122"
                        .to_string(),
                ],
            )
            .unwrap();

        assert_eq!(summary.verified.product_id, "example.dot");
        assert_eq!(summary.verified.modality, ProductModality::Spa);
        assert_eq!(summary.verified.source, ProductSource::Dotns);
        assert_eq!(summary.verified.chains.len(), 1);
        assert!(summary.verified.chains[0].supported_by_host);
        assert_eq!(
            summary.presentation.name.as_deref(),
            Some("Example Product")
        );
        assert_eq!(
            summary.presentation.publisher_display_name.as_deref(),
            Some("Example Labs")
        );
    }

    #[test]
    fn marks_unsupported_chain_in_summary() {
        let manifest: ProductManifest = serde_json::from_str(valid_manifest_json()).unwrap();
        let summary = manifest
            .build_consent_summary("bafyexamplecid", ProductSource::File, &[])
            .unwrap();

        assert!(!summary.verified.chains[0].supported_by_host);
    }

    #[test]
    fn parses_manifest_from_toml() {
        let manifest = ProductManifest::from_toml_str(valid_manifest_toml()).unwrap();
        assert_eq!(manifest.product.id, "example.dot");
        assert_eq!(manifest.product.modality, ProductModality::Spa);
    }

    #[test]
    fn prepared_bundle_parses_manifest_from_assets() {
        let mut assets = HashMap::new();
        assets.insert("index.html".into(), b"<html></html>".to_vec());
        assets.insert(
            "manifest.toml".into(),
            valid_manifest_toml().as_bytes().to_vec(),
        );
        assets.insert("app.js".into(), b"console.log('ok')".to_vec());

        let bundle = PreparedProductBundle::from_asset_map(assets).unwrap();
        assert!(bundle.manifest.is_some());
        assert!(bundle.assets.resources.contains_key("manifest.toml"));
        assert!(bundle.assets.resources.contains_key("app.js"));
    }

    #[test]
    fn prepared_bundle_requires_index_html() {
        let err = PreparedProductBundle::from_asset_map(HashMap::new()).unwrap_err();
        assert_eq!(err.to_string(), "assets map must contain index.html");
    }

    #[test]
    fn required_chain_validation_fails_when_unsupported() {
        let manifest = ProductManifest::from_toml_str(valid_manifest_toml()).unwrap();
        let err = manifest
            .validate_required_chains_supported(&[])
            .unwrap_err();
        assert_eq!(
            err.to_string(),
            "unsupported required chains: [\"0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122\"]"
        );
    }

    #[test]
    fn prepared_bundle_builds_optional_consent_summary() {
        let mut assets = HashMap::new();
        assets.insert("index.html".into(), b"<html></html>".to_vec());
        assets.insert(
            "manifest.toml".into(),
            valid_manifest_toml().as_bytes().to_vec(),
        );

        let bundle = PreparedProductBundle::from_asset_map(assets).unwrap();
        let summary = bundle
            .build_consent_summary(
                "bafyexamplecid",
                ProductSource::Dotns,
                &[
                    "0x91b171bb158e2d3848fa23a9f1c25182d0f1aa1b1c2d3e4f5a6b7c8d9e0f1122"
                        .to_string(),
                ],
            )
            .unwrap()
            .unwrap();

        assert_eq!(summary.verified.product_id, "example.dot");
        assert!(summary.verified.chains[0].supported_by_host);
    }
}
