//! HTTP Gateway - Web access to Synor Storage
//!
//! Provides HTTP endpoints for accessing content stored on Synor.
//! Makes decentralized storage accessible from any web browser.
//!
//! # URL Patterns
//!
//! ## Path-based (legacy)
//! - `GET /synor1abc...xyz` - Fetch content by CID
//! - `GET /synor1abc...xyz/path/to/file` - Fetch file from directory CID
//!
//! ## Subdomain-based (secure, recommended)
//! - `GET https://synor1abc...xyz.gateway.synor.cc/` - Origin-isolated content
//! - `GET https://synor1abc...xyz.gateway.synor.cc/path/to/file` - Subpath
//!
//! ## Trustless (CAR files)
//! - `GET /synor1abc...xyz?format=car` - Get content as CAR file
//!
//! # Origin Isolation
//!
//! Subdomain gateways provide origin isolation, preventing XSS attacks
//! between different content. Each CID gets its own web origin:
//!
//! ```text
//! https://synor1abc.gateway.synor.cc  - Origin A
//! https://synor1def.gateway.synor.cc  - Origin B (isolated from A)
//! ```
//!
//! # CDN Integration
//!
//! Immutable content (identified by CID) can be cached indefinitely:
//! - `Cache-Control: public, max-age=31536000, immutable`
//! - `ETag: "synor1abc...xyz"` (CID as ETag)

mod handler;
mod resolver;
mod cache;

pub use handler::GatewayHandler;
pub use resolver::ContentResolver;
pub use cache::GatewayCache;

use crate::cid::ContentId;
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

/// Gateway configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayConfig {
    /// HTTP listen address
    pub listen_addr: String,
    /// Maximum content size to serve (bytes)
    pub max_content_size: u64,
    /// Cache size in bytes
    pub cache_size: u64,
    /// Request timeout in seconds
    pub timeout_secs: u64,
    /// Enable upload endpoint
    pub enable_upload: bool,
    /// CORS allowed origins
    pub cors_origins: Vec<String>,
    /// Storage node endpoints for content retrieval
    pub storage_nodes: Vec<String>,
    /// Rate limit (requests per minute per IP)
    pub rate_limit: u32,
    /// Gateway hostname for subdomain routing (e.g., "gateway.synor.cc")
    pub gateway_hostname: Option<String>,
    /// Enable subdomain-based CID routing
    pub enable_subdomain_routing: bool,
    /// Enable CAR file responses
    pub enable_car_responses: bool,
    /// CDN configuration
    pub cdn_config: CdnConfig,
}

impl Default for GatewayConfig {
    fn default() -> Self {
        Self {
            listen_addr: "0.0.0.0:8080".to_string(),
            max_content_size: 100 * 1024 * 1024, // 100 MB
            cache_size: 1024 * 1024 * 1024,      // 1 GB
            timeout_secs: 60,
            enable_upload: false, // Disabled by default for public gateways
            cors_origins: vec!["*".to_string()],
            storage_nodes: vec![],
            rate_limit: 100,
            gateway_hostname: Some("gateway.synor.cc".to_string()),
            enable_subdomain_routing: true,
            enable_car_responses: true,
            cdn_config: CdnConfig::default(),
        }
    }
}

/// CDN configuration for edge caching
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CdnConfig {
    /// Enable CDN cache headers
    pub enabled: bool,
    /// Max age for immutable content (CID-addressed)
    pub immutable_max_age: u32,
    /// Max age for mutable content (IPNS, DNSLink)
    pub mutable_max_age: u32,
    /// Enable stale-while-revalidate
    pub stale_while_revalidate: u32,
    /// CDN provider hints
    pub provider: CdnProvider,
}

impl Default for CdnConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            immutable_max_age: 31536000, // 1 year
            mutable_max_age: 300,         // 5 minutes
            stale_while_revalidate: 86400, // 1 day
            provider: CdnProvider::Generic,
        }
    }
}

/// CDN provider for optimized headers
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CdnProvider {
    /// Generic CDN headers
    Generic,
    /// Cloudflare-optimized
    Cloudflare,
    /// Fastly-optimized
    Fastly,
    /// AWS CloudFront
    CloudFront,
    /// Vercel Edge
    Vercel,
}

/// Subdomain routing result
#[derive(Debug, Clone)]
pub struct SubdomainRoute {
    /// Extracted CID from subdomain
    pub cid: ContentId,
    /// Remaining path after CID
    pub path: String,
    /// Original host header
    pub original_host: String,
}

/// Parse subdomain routing from Host header
/// Format: {cid}.{gateway_hostname}
pub fn parse_subdomain_route(host: &str, gateway_hostname: &str) -> Result<SubdomainRoute> {
    let host_lower = host.to_lowercase();
    let gateway_lower = gateway_hostname.to_lowercase();

    // Check if this is a subdomain of our gateway
    if !host_lower.ends_with(&format!(".{}", gateway_lower)) {
        return Err(Error::InvalidHost(format!(
            "Host '{}' is not a subdomain of '{}'",
            host, gateway_hostname
        )));
    }

    // Extract the subdomain (CID)
    let subdomain_end = host_lower.len() - gateway_lower.len() - 1;
    let cid_str = &host[..subdomain_end];

    // Parse CID
    let cid = ContentId::from_string(cid_str)
        .map_err(|e| Error::InvalidCid(format!("Invalid CID in subdomain: {}", e)))?;

    Ok(SubdomainRoute {
        cid,
        path: "/".to_string(),
        original_host: host.to_string(),
    })
}

/// Generate gateway URL for a CID
pub fn gateway_url(cid: &ContentId, gateway_hostname: &str, use_subdomain: bool) -> String {
    if use_subdomain {
        format!("https://{}.{}/", cid.to_string_repr(), gateway_hostname)
    } else {
        format!("https://{}/{}", gateway_hostname, cid.to_string_repr())
    }
}

/// HTTP headers for CDN caching
pub fn cdn_cache_headers(config: &CdnConfig, is_immutable: bool) -> HashMap<String, String> {
    let mut headers = HashMap::new();

    if !config.enabled {
        headers.insert("Cache-Control".to_string(), "no-store".to_string());
        return headers;
    }

    if is_immutable {
        // Immutable content (CID-addressed) can be cached forever
        headers.insert(
            "Cache-Control".to_string(),
            format!(
                "public, max-age={}, immutable",
                config.immutable_max_age
            ),
        );
    } else {
        // Mutable content needs shorter cache
        headers.insert(
            "Cache-Control".to_string(),
            format!(
                "public, max-age={}, stale-while-revalidate={}",
                config.mutable_max_age, config.stale_while_revalidate
            ),
        );
    }

    // Provider-specific headers
    match config.provider {
        CdnProvider::Cloudflare => {
            // Cloudflare-specific cache hints
            if is_immutable {
                headers.insert("CF-Cache-Status".to_string(), "HIT".to_string());
            }
        }
        CdnProvider::Fastly => {
            // Fastly Surrogate-Control for edge caching
            headers.insert(
                "Surrogate-Control".to_string(),
                format!("max-age={}", config.immutable_max_age),
            );
        }
        CdnProvider::CloudFront => {
            // CloudFront cache behaviors
            headers.insert(
                "X-Cache".to_string(),
                if is_immutable { "Hit from cloudfront" } else { "Miss from cloudfront" }.to_string(),
            );
        }
        CdnProvider::Vercel => {
            // Vercel edge caching
            headers.insert(
                "X-Vercel-Cache".to_string(),
                if is_immutable { "HIT" } else { "STALE" }.to_string(),
            );
        }
        CdnProvider::Generic => {}
    }

    // Security headers
    headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
    headers.insert("X-Frame-Options".to_string(), "DENY".to_string());

    headers
}

/// HTTP Gateway service
pub struct Gateway {
    /// Configuration
    config: GatewayConfig,
    /// Content resolver
    resolver: Arc<ContentResolver>,
    /// Response cache
    cache: Arc<RwLock<GatewayCache>>,
    /// Gateway statistics
    stats: Arc<RwLock<GatewayStats>>,
    /// Running state
    running: Arc<std::sync::atomic::AtomicBool>,
}

/// Gateway statistics
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GatewayStats {
    /// Total requests handled
    pub total_requests: u64,
    /// Successful requests
    pub successful_requests: u64,
    /// Failed requests
    pub failed_requests: u64,
    /// Cache hits
    pub cache_hits: u64,
    /// Cache misses
    pub cache_misses: u64,
    /// Bytes served
    pub bytes_served: u64,
    /// Upload requests (if enabled)
    pub upload_requests: u64,
    /// Unique CIDs requested
    pub unique_cids: u64,
}

impl Gateway {
    /// Create a new gateway
    pub fn new(config: GatewayConfig) -> Self {
        let resolver = Arc::new(ContentResolver::new(config.storage_nodes.clone()));
        let cache = Arc::new(RwLock::new(GatewayCache::new(config.cache_size)));

        Self {
            config,
            resolver,
            cache,
            stats: Arc::new(RwLock::new(GatewayStats::default())),
            running: Arc::new(std::sync::atomic::AtomicBool::new(false)),
        }
    }

    /// Start the gateway server
    pub async fn start(&self) -> Result<()> {
        self.running
            .store(true, std::sync::atomic::Ordering::SeqCst);

        // TODO: Start HTTP server using axum/actix-web/warp
        // Routes:
        // - GET /{cid} -> fetch content
        // - HEAD /{cid} -> check existence
        // - POST /upload -> upload content (if enabled)
        // - GET /stats -> gateway statistics

        Ok(())
    }

    /// Stop the gateway
    pub async fn stop(&self) {
        self.running
            .store(false, std::sync::atomic::Ordering::SeqCst);
    }

    /// Check if gateway is running
    pub fn is_running(&self) -> bool {
        self.running.load(std::sync::atomic::Ordering::SeqCst)
    }

    /// Get gateway statistics
    pub async fn stats(&self) -> GatewayStats {
        self.stats.read().await.clone()
    }

    /// Fetch content by CID
    pub async fn fetch(&self, cid: &ContentId) -> Result<GatewayResponse> {
        // Check cache first
        {
            let cache = self.cache.read().await;
            if let Some(cached) = cache.get(cid) {
                let mut stats = self.stats.write().await;
                stats.cache_hits += 1;
                return Ok(cached);
            }
        }

        // Resolve from storage network
        let content = self.resolver.resolve(cid).await?;

        // Determine MIME type
        let mime_type = detect_mime_type(&content);

        // Create response with CDN headers
        let response = GatewayResponse::standard(cid.clone(), content, mime_type)
            .with_cdn_headers(&self.config.cdn_config);

        // Cache the response
        {
            let mut cache = self.cache.write().await;
            cache.put(cid.clone(), response.clone());
        }

        // Update stats
        {
            let mut stats = self.stats.write().await;
            stats.cache_misses += 1;
            stats.bytes_served += response.size;
        }

        Ok(response)
    }
}

/// Gateway response
#[derive(Debug, Clone)]
pub struct GatewayResponse {
    /// Content ID
    pub cid: ContentId,
    /// Content bytes
    pub content: Vec<u8>,
    /// MIME type
    pub mime_type: String,
    /// Content size
    pub size: u64,
    /// Response format
    pub format: ResponseFormat,
    /// HTTP headers
    pub headers: HashMap<String, String>,
}

impl GatewayResponse {
    /// Create standard response
    pub fn standard(cid: ContentId, content: Vec<u8>, mime_type: String) -> Self {
        let size = content.len() as u64;
        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), mime_type.clone());
        headers.insert("Content-Length".to_string(), size.to_string());
        headers.insert("ETag".to_string(), format!("\"{}\"", cid.to_string_repr()));
        headers.insert("X-Ipfs-Roots".to_string(), cid.to_string_repr());

        Self {
            cid,
            content,
            mime_type,
            size,
            format: ResponseFormat::Raw,
            headers,
        }
    }

    /// Create CAR response for trustless verification
    pub fn as_car(cid: ContentId, content: Vec<u8>) -> Self {
        use crate::car::CarFile;

        let car = CarFile::from_content(&content);
        let car_data = car.encode();

        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), "application/vnd.ipld.car".to_string());
        headers.insert("Content-Length".to_string(), car_data.len().to_string());
        headers.insert("X-Ipfs-Roots".to_string(), cid.to_string_repr());
        headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
        headers.insert("Cache-Control".to_string(), "public, max-age=31536000, immutable".to_string());

        Self {
            cid,
            content: car_data,
            mime_type: "application/vnd.ipld.car".to_string(),
            size: content.len() as u64,
            format: ResponseFormat::Car,
            headers,
        }
    }

    /// Add CDN headers
    pub fn with_cdn_headers(mut self, cdn_config: &CdnConfig) -> Self {
        let cdn_headers = cdn_cache_headers(cdn_config, true);
        self.headers.extend(cdn_headers);
        self
    }
}

/// Response format
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseFormat {
    /// Raw content
    Raw,
    /// CAR file (trustless verification)
    Car,
    /// Directory listing (HTML)
    DirectoryHtml,
    /// Directory listing (JSON)
    DirectoryJson,
}

/// Detect MIME type from content
fn detect_mime_type(content: &[u8]) -> String {
    // Magic byte detection for common types
    if content.starts_with(b"\x89PNG\r\n\x1a\n") {
        "image/png".to_string()
    } else if content.starts_with(b"\xff\xd8\xff") {
        "image/jpeg".to_string()
    } else if content.starts_with(b"GIF87a") || content.starts_with(b"GIF89a") {
        "image/gif".to_string()
    } else if content.starts_with(b"RIFF") && content.len() > 12 && &content[8..12] == b"WEBP" {
        "image/webp".to_string()
    } else if content.starts_with(b"PK\x03\x04") {
        // ZIP-based formats
        "application/zip".to_string()
    } else if content.starts_with(b"%PDF") {
        "application/pdf".to_string()
    } else if content.starts_with(b"<!DOCTYPE html") || content.starts_with(b"<html") {
        "text/html".to_string()
    } else if content.starts_with(b"<?xml") {
        "application/xml".to_string()
    } else if content.starts_with(b"{") || content.starts_with(b"[") {
        "application/json".to_string()
    } else if content.iter().take(1024).all(|&b| b.is_ascii()) {
        "text/plain".to_string()
    } else {
        "application/octet-stream".to_string()
    }
}

/// Content type for directories (DAG structures)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryListing {
    /// Directory entries
    pub entries: Vec<DirectoryEntry>,
}

/// Single directory entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryEntry {
    /// Entry name
    pub name: String,
    /// CID of the entry
    pub cid: ContentId,
    /// Size in bytes
    pub size: u64,
    /// True if this is a directory
    pub is_directory: bool,
}
