// Copyright (c) 2025 Erick Bourgeois, firestoned
// SPDX-License-Identifier: MIT

//! BIND9 RNDC API Server
//!
//! A lightweight HTTP REST API server that manages BIND9 zones by:
//! - Creating zone files in `/var/cache/bind/`
//! - Executing local rndc commands to manage zones
//! - Providing authenticated access via Kubernetes ServiceAccount tokens
//!
//! This server runs as a sidecar container alongside BIND9, sharing
//! the zone file storage volume and rndc configuration.

use anyhow::Context;
use axum::{
    extract::State,
    http::StatusCode,
    middleware as axum_middleware,
    response::{IntoResponse, Response},
    routing::{get, post},
    Json, Router,
};
use serde::Serialize;
use std::net::SocketAddr;
use std::sync::Arc;
use tower_http::trace::TraceLayer;
use tracing::{debug, error, info, warn};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

// Import from the library
use bindcar::{
    auth::authenticate,
    metrics, middleware,
    rate_limit::RateLimitConfig,
    rndc::RndcExecutor,
    types::{AppState, ErrorResponse},
    zones,
};
use tower_governor::{
    governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer,
};

/// OpenAPI documentation structure
#[derive(OpenApi)]
#[openapi(
    paths(
        zones::create_zone,
        zones::delete_zone,
        zones::modify_zone,
        zones::reload_zone,
        zones::zone_status,
        zones::freeze_zone,
        zones::thaw_zone,
        zones::notify_zone,
        zones::retransfer_zone,
        zones::server_status,
        zones::list_zones,
        zones::get_zone,
    ),
    components(
        schemas(
            zones::CreateZoneRequest,
            zones::ModifyZoneRequest,
            zones::ZoneResponse,
            zones::ServerStatusResponse,
            zones::ZoneInfo,
            zones::ZoneListResponse,
            zones::ZoneConfig,
            zones::SoaRecord,
            zones::DnsRecord,
        )
    ),
    tags(
        (name = "zones", description = "Zone management endpoints"),
        (name = "server", description = "Server status endpoints")
    ),
    info(
        title = "Bindcar API",
        version = "0.1.0",
        description = "HTTP REST API for managing BIND9 zones via RNDC",
        license(name = "MIT")
    )
)]
struct ApiDoc;

/// Server configuration
const DEFAULT_BIND_ZONE_DIR: &str = "/var/cache/bind";
const DEFAULT_API_PORT: u16 = 8080;

/// Health check response
#[derive(Serialize)]
struct HealthResponse {
    status: String,
    version: String,
}

/// Readiness check response
#[derive(Serialize)]
struct ReadyResponse {
    ready: bool,
    checks: Vec<String>,
}

/// Health check endpoint
async fn health_check() -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "healthy".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    })
}

/// Metrics endpoint for Prometheus scraping
async fn metrics_handler() -> Response {
    match metrics::gather_metrics() {
        Ok(metrics_text) => (
            StatusCode::OK,
            [("Content-Type", "text/plain; version=0.0.4")],
            metrics_text,
        )
            .into_response(),
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse {
                error: format!("Failed to gather metrics: {}", e),
                details: None,
            }),
        )
            .into_response(),
    }
}

/// Readiness check endpoint
async fn ready_check(State(state): State<AppState>) -> Json<ReadyResponse> {
    let mut checks = Vec::new();
    let mut ready = true;

    // Check if zone directory is writable
    match tokio::fs::metadata(&state.zone_dir).await {
        Ok(metadata) => {
            if !metadata.is_dir() {
                ready = false;
                checks.push(format!("zone_dir_not_directory: {}", state.zone_dir));
            } else {
                checks.push(format!("zone_dir_accessible: {}", state.zone_dir));
            }
        }
        Err(e) => {
            ready = false;
            checks.push(format!("zone_dir_error: {}", e));
        }
    }

    // Check if rndc is available
    if let Err(e) = state.rndc.status().await {
        warn!("RNDC not ready: {}", e);
        ready = false;
        checks.push(format!("rndc_error: {}", e));
    } else {
        checks.push("rndc_available: true".to_string());
    }

    Json(ReadyResponse { ready, checks })
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize tracing
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .json()
        .init();

    info!(
        "starting bind9 rndc api server v{}",
        env!("CARGO_PKG_VERSION")
    );

    // initialize metrics
    metrics::init_metrics();

    // get configuration from environment
    let zone_dir =
        std::env::var("BIND_ZONE_DIR").unwrap_or_else(|_| DEFAULT_BIND_ZONE_DIR.to_string());
    let api_port = std::env::var("API_PORT")
        .ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(DEFAULT_API_PORT);
    let disable_auth = std::env::var("DISABLE_AUTH")
        .ok()
        .and_then(|v| v.parse::<bool>().ok())
        .unwrap_or(false);

    info!("zone directory: {}", zone_dir);
    info!("api port: {}", api_port);
    if disable_auth {
        warn!("⚠️  authentication is disabled - api endpoints are unprotected!");
        warn!("⚠️  this should only be used in trusted environments (e.g., linkerd service mesh)");
    } else {
        info!("authentication is enabled");
    }

    // load rate limiting configuration
    let rate_limit_config = RateLimitConfig::from_env();
    if let Err(e) = rate_limit_config.validate() {
        error!("invalid rate limit configuration: {}", e);
        return Err(anyhow::anyhow!("invalid rate limit configuration: {}", e));
    }

    if rate_limit_config.enabled {
        info!(
            "rate limiting enabled: {} requests per {} seconds (burst: {})",
            rate_limit_config.requests_per_period,
            rate_limit_config.period_secs,
            rate_limit_config.burst_size
        );
    } else {
        warn!("⚠️  rate limiting is disabled");
    }

    // get rndc configuration from environment or fallback to rndc.conf
    // Each parameter is checked independently - env var takes priority, then rndc.conf

    // Try to parse rndc.conf file first to have fallback values
    let config_paths = vec!["/etc/bind/rndc.conf", "/etc/rndc.conf"];
    let mut parsed_config = None;

    for path in &config_paths {
        match bindcar::rndc::parse_rndc_conf(path) {
            Ok(cfg) => {
                info!("successfully parsed rndc configuration from {}", path);
                parsed_config = Some(cfg);
                break;
            }
            Err(e) => {
                debug!("failed to parse {}: {}", path, e);
            }
        }
    }

    // Check each parameter independently: env var first, then rndc.conf, then hardcoded default
    let rndc_server = if let Ok(server) = std::env::var("RNDC_SERVER") {
        info!("using RNDC_SERVER from environment: {}", server);
        server
    } else if let Some(ref cfg) = parsed_config {
        info!("using server from rndc.conf: {}", cfg.server);
        cfg.server.clone()
    } else {
        let default = "127.0.0.1:953".to_string();
        warn!("using default RNDC_SERVER: {}", default);
        default
    };

    let rndc_algorithm = if let Ok(algorithm) = std::env::var("RNDC_ALGORITHM") {
        info!("using RNDC_ALGORITHM from environment: {}", algorithm);
        algorithm
    } else if let Some(ref cfg) = parsed_config {
        info!("using algorithm from rndc.conf: {}", cfg.algorithm);
        cfg.algorithm.clone()
    } else {
        let default = "sha256".to_string();
        warn!("using default RNDC_ALGORITHM: {}", default);
        default
    };

    let rndc_secret = if let Ok(secret) = std::env::var("RNDC_SECRET") {
        info!("using RNDC_SECRET from environment");
        secret
    } else if let Some(ref cfg) = parsed_config {
        info!("using secret from rndc.conf");
        cfg.secret.clone()
    } else {
        error!("rndc configuration not found!");
        error!("either set RNDC_SECRET environment variable or ensure /etc/bind/rndc.conf exists");
        return Err(anyhow::anyhow!(
            "rndc configuration required: set RNDC_SECRET env var or create /etc/bind/rndc.conf"
        ));
    };

    // verify zone directory exists
    if !tokio::fs::metadata(&zone_dir).await?.is_dir() {
        error!("zone directory does not exist: {}", zone_dir);
        return Err(anyhow::anyhow!("zone directory not found"));
    }

    // create rndc executor
    let rndc = Arc::new(
        RndcExecutor::new(rndc_server, rndc_algorithm, rndc_secret)
            .context("failed to create rndc client")?,
    );

    // create application state
    let state = AppState {
        rndc,
        zone_dir: zone_dir.clone(),
    };

    // build api routes
    let api_routes = Router::new()
        .route("/zones", post(zones::create_zone).get(zones::list_zones))
        .route(
            "/zones/{name}",
            get(zones::get_zone)
                .delete(zones::delete_zone)
                .patch(zones::modify_zone),
        )
        .route("/zones/{name}/reload", post(zones::reload_zone))
        .route("/zones/{name}/status", get(zones::zone_status))
        .route("/zones/{name}/freeze", post(zones::freeze_zone))
        .route("/zones/{name}/thaw", post(zones::thaw_zone))
        .route("/zones/{name}/notify", post(zones::notify_zone))
        .route("/zones/{name}/retransfer", post(zones::retransfer_zone))
        .route("/server/status", get(zones::server_status))
        .with_state(state.clone());

    // conditionally apply authentication middleware
    let api_routes = if !disable_auth {
        api_routes.layer(axum_middleware::from_fn(authenticate))
    } else {
        api_routes
    };

    // conditionally apply rate limiting layer
    let api_routes = if rate_limit_config.enabled {
        // Calculate requests per second from period
        let per_second =
            rate_limit_config.requests_per_period / rate_limit_config.period_secs.max(1) as u32;
        let per_second = per_second.max(1); // Ensure at least 1 request per second

        // Build governor configuration
        let governor_conf = Arc::new(
            GovernorConfigBuilder::default()
                .key_extractor(SmartIpKeyExtractor)
                .per_second(per_second.into())
                .burst_size(rate_limit_config.burst_size)
                .finish()
                .expect("Failed to create governor config"),
        );

        api_routes.layer(GovernorLayer::new(governor_conf))
    } else {
        api_routes
    };

    // build main router
    let app = Router::new()
        .merge(SwaggerUi::new("/api/v1/docs").url("/api/v1/openapi.json", ApiDoc::openapi()))
        .route("/api/v1/health", get(health_check))
        .route("/api/v1/ready", get(ready_check))
        .route("/metrics", get(metrics_handler))
        .nest("/api/v1", api_routes)
        .with_state(state)
        .layer(axum_middleware::from_fn(middleware::track_metrics))
        .layer(TraceLayer::new_for_http());

    // start server
    let addr = format!("0.0.0.0:{}", api_port);

    info!("bind9 rndc api server listening on {}", addr);
    info!("swagger ui available at http://{}/api/v1/docs", addr);

    let listener = tokio::net::TcpListener::bind(&addr).await?;

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

    Ok(())
}
