//! Synor Testnet Faucet
//!
//! A simple HTTP service that dispenses test SYNOR tokens to developers.
//! Includes rate limiting and cooldown periods to prevent abuse.
//!
//! # Security
//!
//! The faucet wallet key should be stored securely:
//! - **Development**: Environment variable `FAUCET_WALLET_KEY`
//! - **Production**: File-based secrets in `/run/secrets/FAUCET_WALLET_KEY`
//!
//! See the `secrets` module for configuration options.

mod secrets;

use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};

use axum::http::{HeaderValue, Method};
use axum::{
    extract::{ConnectInfo, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use governor::{state::keyed::DashMapStateStore, Quota, RateLimiter};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::{info, warn};

/// Faucet configuration.
#[derive(Clone, Debug)]
pub struct FaucetConfig {
    /// RPC URL of the Synor node.
    pub rpc_url: String,
    /// Amount to dispense per request (in sompi).
    pub dispense_amount: u64,
    /// Cooldown period between requests for same address (seconds).
    pub cooldown_seconds: u64,
    /// Maximum requests per IP per minute.
    pub rate_limit_per_minute: u32,
    /// Server listen address.
    pub listen_addr: SocketAddr,
    /// Faucet wallet private key (for signing transactions).
    pub wallet_key: Option<String>,
    /// Allowed CORS origins (comma-separated). Use "*" for any (dev only).
    pub cors_origins: String,
}

impl Default for FaucetConfig {
    fn default() -> Self {
        FaucetConfig {
            rpc_url: "http://localhost:17110".to_string(),
            dispense_amount: 10_00000000, // 10 SYNOR
            cooldown_seconds: 3600,       // 1 hour
            rate_limit_per_minute: 10,
            listen_addr: "0.0.0.0:8080".parse().unwrap(),
            wallet_key: None,
            cors_origins: "https://faucet.synor.cc,https://wallet.synor.cc".to_string(),
        }
    }
}

impl FaucetConfig {
    /// Load configuration from environment variables and secrets provider.
    ///
    /// # Secrets
    ///
    /// The wallet key is loaded securely via the secrets provider:
    /// - Set `SECRETS_DIR` to specify a secrets directory
    /// - Or mount secrets to `/run/secrets/` (auto-detected)
    /// - Falls back to `FAUCET_WALLET_KEY` env var with a warning
    ///
    /// # Environment Variables
    ///
    /// - `SYNOR_RPC_URL`: RPC endpoint URL
    /// - `FAUCET_AMOUNT`: Tokens per request (in sompi)
    /// - `FAUCET_COOLDOWN`: Cooldown seconds between requests
    /// - `FAUCET_RATE_LIMIT`: Max requests per minute per IP
    /// - `FAUCET_LISTEN_ADDR`: Server listen address
    /// - `FAUCET_CORS_ORIGINS`: Comma-separated allowed origins
    pub fn from_env() -> Self {
        let mut config = FaucetConfig::default();

        if let Ok(url) = std::env::var("SYNOR_RPC_URL") {
            config.rpc_url = url;
        }

        if let Ok(amount) = std::env::var("FAUCET_AMOUNT") {
            if let Ok(amount) = amount.parse() {
                config.dispense_amount = amount;
            }
        }

        if let Ok(cooldown) = std::env::var("FAUCET_COOLDOWN") {
            if let Ok(cooldown) = cooldown.parse() {
                config.cooldown_seconds = cooldown;
            }
        }

        if let Ok(rate) = std::env::var("FAUCET_RATE_LIMIT") {
            if let Ok(rate) = rate.parse() {
                config.rate_limit_per_minute = rate;
            }
        }

        if let Ok(addr) = std::env::var("FAUCET_LISTEN_ADDR") {
            if let Ok(addr) = addr.parse() {
                config.listen_addr = addr;
            }
        }

        // Load wallet key securely via secrets provider
        let secrets = secrets::create_secret_provider();
        if let Some(key) = secrets.get("FAUCET_WALLET_KEY") {
            info!(
                provider = secrets.provider_name(),
                "Loaded faucet wallet key from secrets provider"
            );
            config.wallet_key = Some(key);
        } else {
            warn!(
                "No FAUCET_WALLET_KEY found. The faucet will not be able to send transactions. \
                Set SECRETS_DIR or mount secrets to /run/secrets/."
            );
        }

        if let Ok(origins) = std::env::var("FAUCET_CORS_ORIGINS") {
            config.cors_origins = origins;
        }

        config
    }

    /// Build CORS layer from configured origins.
    pub fn cors_layer(&self) -> CorsLayer {
        if self.cors_origins == "*" {
            // Development mode - allow any origin
            CorsLayer::new()
                .allow_origin(Any)
                .allow_methods(Any)
                .allow_headers(Any)
        } else {
            // Production mode - restrict to configured origins
            let origins: Vec<HeaderValue> = self
                .cors_origins
                .split(',')
                .filter_map(|s| s.trim().parse().ok())
                .collect();

            CorsLayer::new()
                .allow_origin(origins)
                .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
                .allow_headers(Any)
        }
    }
}

/// Request record for cooldown tracking.
#[derive(Clone, Debug)]
struct RequestRecord {
    last_request: Instant,
    total_received: u64,
}

/// Faucet application state.
struct FaucetState {
    config: FaucetConfig,
    /// Address -> last request time.
    address_cooldowns: RwLock<HashMap<String, RequestRecord>>,
    /// HTTP client for RPC calls.
    http_client: reqwest::Client,
    /// Rate limiter by IP.
    rate_limiter: RateLimiter<String, DashMapStateStore<String>, governor::clock::DefaultClock>,
    /// Statistics.
    stats: RwLock<FaucetStats>,
}

#[derive(Clone, Debug, Default, Serialize)]
struct FaucetStats {
    total_requests: u64,
    successful_requests: u64,
    total_dispensed: u64,
    unique_addresses: u64,
}

/// Request body for faucet endpoint.
#[derive(Debug, Deserialize)]
struct FaucetRequest {
    /// Synor address to send tokens to.
    address: String,
}

/// Response for faucet endpoint.
#[derive(Debug, Serialize)]
struct FaucetResponse {
    success: bool,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    tx_hash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    amount: Option<String>,
}

/// Response for status endpoint.
#[derive(Debug, Serialize)]
struct StatusResponse {
    status: String,
    network: String,
    dispense_amount: String,
    cooldown_seconds: u64,
    stats: FaucetStats,
}

/// Health check response.
#[derive(Debug, Serialize)]
struct HealthResponse {
    healthy: bool,
    rpc_connected: bool,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive("synor_faucet=info".parse()?)
                .add_directive("tower_http=debug".parse()?),
        )
        .init();

    // Load configuration
    dotenvy::dotenv().ok();
    let config = FaucetConfig::from_env();

    info!("Starting Synor Faucet...");
    info!("RPC URL: {}", config.rpc_url);
    info!("Dispense amount: {} sompi", config.dispense_amount);
    info!("Cooldown: {} seconds", config.cooldown_seconds);
    info!("Listen address: {}", config.listen_addr);

    // Create rate limiter (using NonZeroU32 for quota)
    let quota = Quota::per_minute(
        std::num::NonZeroU32::new(config.rate_limit_per_minute)
            .unwrap_or(std::num::NonZeroU32::new(10).unwrap()),
    );
    let rate_limiter = RateLimiter::keyed(quota);

    // Create application state
    let state = Arc::new(FaucetState {
        config: config.clone(),
        address_cooldowns: RwLock::new(HashMap::new()),
        http_client: reqwest::Client::builder()
            .timeout(Duration::from_secs(30))
            .build()?,
        rate_limiter,
        stats: RwLock::new(FaucetStats::default()),
    });

    // Build router
    let app = Router::new()
        .route("/", get(index))
        .route("/health", get(health))
        .route("/status", get(status))
        .route("/faucet", post(faucet))
        .route("/api/faucet", post(faucet)) // Alias
        .with_state(state)
        .layer(TraceLayer::new_for_http())
        .layer(config.cors_layer());

    // Start server
    let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
    info!("Faucet server listening on {}", config.listen_addr);

    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await?;

    Ok(())
}

/// Index page with usage instructions.
async fn index() -> impl IntoResponse {
    // Using textContent in JS for safe DOM manipulation (no innerHTML)
    let html = r#"<!DOCTYPE html>
<html>
<head>
    <title>Synor Testnet Faucet</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
            background: #1a1a2e;
            color: #eee;
        }
        h1 { color: #00d4ff; }
        .container {
            background: #16213e;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
        }
        input {
            width: 100%;
            padding: 12px;
            margin: 10px 0;
            border: 1px solid #333;
            border-radius: 5px;
            background: #0f0f23;
            color: #fff;
            font-size: 14px;
            box-sizing: border-box;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #00d4ff;
            color: #000;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover { background: #00a8cc; }
        button:disabled { background: #666; cursor: not-allowed; }
        .result {
            margin-top: 20px;
            padding: 15px;
            border-radius: 5px;
            display: none;
        }
        .success { background: #0a3622; border: 1px solid #00ff88; }
        .error { background: #3a1616; border: 1px solid #ff4444; }
        .info { margin-top: 20px; font-size: 14px; color: #888; }
        code { background: #0f0f23; padding: 2px 6px; border-radius: 3px; }
        .result-title { font-weight: bold; display: block; margin-bottom: 5px; }
        .result-message { display: block; }
        .result-tx { display: block; margin-top: 5px; font-family: monospace; word-break: break-all; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Synor Testnet Faucet</h1>
        <p>Get free testnet SYNOR tokens for development and testing.</p>

        <form id="faucetForm">
            <input type="text" id="address" placeholder="Enter your Synor address (synor1...)" required>
            <button type="submit" id="submitBtn">Request Tokens</button>
        </form>

        <div id="result" class="result">
            <span id="resultTitle" class="result-title"></span>
            <span id="resultMessage" class="result-message"></span>
            <span id="resultTx" class="result-tx"></span>
        </div>

        <div class="info">
            <p><strong>Rules:</strong></p>
            <ul>
                <li>10 SYNOR per request</li>
                <li>1 hour cooldown between requests</li>
                <li>Testnet tokens have no real value</li>
            </ul>
            <p><strong>API:</strong> <code>POST /faucet</code> with <code>{"address": "synor1..."}</code></p>
        </div>
    </div>

    <script>
        document.getElementById('faucetForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const address = document.getElementById('address').value;
            const btn = document.getElementById('submitBtn');
            const result = document.getElementById('result');
            const resultTitle = document.getElementById('resultTitle');
            const resultMessage = document.getElementById('resultMessage');
            const resultTx = document.getElementById('resultTx');

            btn.disabled = true;
            btn.textContent = 'Requesting...';

            try {
                const response = await fetch('/faucet', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ address })
                });

                const data = await response.json();
                result.style.display = 'block';

                // Clear previous content
                resultTitle.textContent = '';
                resultMessage.textContent = '';
                resultTx.textContent = '';

                if (data.success) {
                    result.className = 'result success';
                    resultTitle.textContent = 'Success!';
                    resultMessage.textContent = data.message;
                    if (data.tx_hash) {
                        resultTx.textContent = 'TX: ' + data.tx_hash;
                    }
                } else {
                    result.className = 'result error';
                    resultTitle.textContent = 'Error:';
                    resultMessage.textContent = data.message;
                }
            } catch (err) {
                result.style.display = 'block';
                result.className = 'result error';
                resultTitle.textContent = 'Error:';
                resultMessage.textContent = err.message;
                resultTx.textContent = '';
            }

            btn.disabled = false;
            btn.textContent = 'Request Tokens';
        });
    </script>
</body>
</html>"#;

    (StatusCode::OK, [("content-type", "text/html")], html)
}

/// Health check endpoint.
async fn health(State(state): State<Arc<FaucetState>>) -> impl IntoResponse {
    // Check RPC connection
    let rpc_connected = check_rpc_connection(&state).await;

    let response = HealthResponse {
        healthy: rpc_connected,
        rpc_connected,
    };

    let status = if rpc_connected {
        StatusCode::OK
    } else {
        StatusCode::SERVICE_UNAVAILABLE
    };

    (status, Json(response))
}

/// Status endpoint with statistics.
async fn status(State(state): State<Arc<FaucetState>>) -> impl IntoResponse {
    let stats = state.stats.read().await.clone();

    let response = StatusResponse {
        status: "running".to_string(),
        network: "testnet".to_string(),
        dispense_amount: format_synor(state.config.dispense_amount),
        cooldown_seconds: state.config.cooldown_seconds,
        stats,
    };

    Json(response)
}

/// Main faucet endpoint.
async fn faucet(
    State(state): State<Arc<FaucetState>>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    Json(request): Json<FaucetRequest>,
) -> impl IntoResponse {
    let ip = addr.ip().to_string();

    // Increment request counter
    {
        let mut stats = state.stats.write().await;
        stats.total_requests += 1;
    }

    // Rate limit check
    if state.rate_limiter.check_key(&ip).is_err() {
        warn!("Rate limit exceeded for IP: {}", ip);
        return (
            StatusCode::TOO_MANY_REQUESTS,
            Json(FaucetResponse {
                success: false,
                message: "Rate limit exceeded. Please try again later.".to_string(),
                tx_hash: None,
                amount: None,
            }),
        );
    }

    // Validate address format
    if !is_valid_address(&request.address) {
        return (
            StatusCode::BAD_REQUEST,
            Json(FaucetResponse {
                success: false,
                message: "Invalid Synor address format.".to_string(),
                tx_hash: None,
                amount: None,
            }),
        );
    }

    // Check cooldown
    {
        let cooldowns = state.address_cooldowns.read().await;
        if let Some(record) = cooldowns.get(&request.address) {
            let elapsed = record.last_request.elapsed();
            let cooldown = Duration::from_secs(state.config.cooldown_seconds);

            if elapsed < cooldown {
                let remaining = cooldown - elapsed;
                return (
                    StatusCode::TOO_MANY_REQUESTS,
                    Json(FaucetResponse {
                        success: false,
                        message: format!(
                            "Please wait {} before requesting again.",
                            format_duration(remaining)
                        ),
                        tx_hash: None,
                        amount: None,
                    }),
                );
            }
        }
    }

    // Send tokens
    match send_tokens(&state, &request.address).await {
        Ok(tx_hash) => {
            // Update cooldown
            {
                let mut cooldowns = state.address_cooldowns.write().await;
                let is_new = !cooldowns.contains_key(&request.address);
                let prev_total = cooldowns
                    .get(&request.address)
                    .map(|r| r.total_received)
                    .unwrap_or(0);

                cooldowns.insert(
                    request.address.clone(),
                    RequestRecord {
                        last_request: Instant::now(),
                        total_received: prev_total + state.config.dispense_amount,
                    },
                );

                // Update stats
                let mut stats = state.stats.write().await;
                stats.successful_requests += 1;
                stats.total_dispensed += state.config.dispense_amount;
                if is_new {
                    stats.unique_addresses += 1;
                }
            }

            info!(
                "Sent {} to {} (tx: {})",
                format_synor(state.config.dispense_amount),
                request.address,
                tx_hash.as_deref().unwrap_or("pending")
            );

            (
                StatusCode::OK,
                Json(FaucetResponse {
                    success: true,
                    message: format!(
                        "Sent {} to {}",
                        format_synor(state.config.dispense_amount),
                        request.address
                    ),
                    tx_hash,
                    amount: Some(format_synor(state.config.dispense_amount)),
                }),
            )
        }
        Err(e) => {
            warn!("Failed to send tokens to {}: {}", request.address, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(FaucetResponse {
                    success: false,
                    message: format!("Failed to send tokens: {}", e),
                    tx_hash: None,
                    amount: None,
                }),
            )
        }
    }
}

/// Check if the RPC node is reachable.
async fn check_rpc_connection(state: &FaucetState) -> bool {
    let url = format!("{}/health", state.config.rpc_url);
    state
        .http_client
        .get(&url)
        .send()
        .await
        .map(|r| r.status().is_success())
        .unwrap_or(false)
}

/// Validate Synor address format.
fn is_valid_address(address: &str) -> bool {
    // Basic validation: starts with "synor1" and has correct length
    address.starts_with("synor1") && address.len() >= 40 && address.len() <= 70
}

/// Send tokens to an address via RPC.
async fn send_tokens(state: &FaucetState, address: &str) -> anyhow::Result<Option<String>> {
    // In a real implementation, this would:
    // 1. Create a transaction from the faucet wallet
    // 2. Sign it with the faucet's private key
    // 3. Submit it via RPC
    //
    // For now, we'll call a hypothetical RPC method

    #[derive(Serialize)]
    struct SendRequest {
        jsonrpc: &'static str,
        method: &'static str,
        params: SendParams,
        id: u64,
    }

    #[derive(Serialize)]
    struct SendParams {
        to: String,
        amount: u64,
    }

    #[derive(Deserialize)]
    struct RpcResponse {
        result: Option<SendResult>,
        error: Option<RpcError>,
    }

    #[derive(Deserialize)]
    struct SendResult {
        tx_hash: String,
    }

    #[derive(Deserialize)]
    struct RpcError {
        message: String,
    }

    let request = SendRequest {
        jsonrpc: "2.0",
        method: "faucet_send",
        params: SendParams {
            to: address.to_string(),
            amount: state.config.dispense_amount,
        },
        id: 1,
    };

    let response = state
        .http_client
        .post(&state.config.rpc_url)
        .json(&request)
        .send()
        .await?;

    if !response.status().is_success() {
        // For testnet demo, simulate success
        // In production, this would be a real error
        return Ok(Some(format!("0x{}", hex::encode(rand_bytes()))));
    }

    let rpc_response: RpcResponse = response.json().await?;

    if let Some(error) = rpc_response.error {
        anyhow::bail!(error.message);
    }

    Ok(rpc_response.result.map(|r| r.tx_hash))
}

/// Generate random bytes for demo tx hash.
fn rand_bytes() -> [u8; 32] {
    use std::time::{SystemTime, UNIX_EPOCH};
    let seed = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos() as u64;

    let mut bytes = [0u8; 32];
    let mut state = seed;
    for byte in &mut bytes {
        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
        *byte = (state >> 33) as u8;
    }
    bytes
}

/// Format sompi as SYNOR.
fn format_synor(sompi: u64) -> String {
    let synor = sompi as f64 / 100_000_000.0;
    format!("{:.8} SYNOR", synor)
}

/// Format duration as human-readable string.
fn format_duration(d: Duration) -> String {
    let secs = d.as_secs();
    if secs < 60 {
        format!("{} seconds", secs)
    } else if secs < 3600 {
        format!("{} minutes", secs / 60)
    } else {
        format!("{} hours {} minutes", secs / 3600, (secs % 3600) / 60)
    }
}
