//! CreditService reference server.
//!
//! Main entry point for the minimal auth-integrated quota and credit-control
//! service used to prove vendor-replaceable integration.

mod config;
mod rest;

use chainfire_client::Client as ChainFireClient;
use clap::Parser;
use config::ServerConfig;
use creditservice_api::{CreditServiceImpl, FlareDbStorage, SqlStorage};
use creditservice_proto::credit_service_server::CreditServiceServer;
use photon_auth_client::{connect_iam, AuthService};
use photon_state::{ensure_sql_backend_matches_url, StateBackend};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tonic::transport::Server;
use tonic::{Request, Status};
use tonic_health::server::health_reporter;
use tracing::info;

#[derive(Parser, Debug)]
#[command(name = "creditservice-server")]
#[command(about = "Minimal auth-integrated credit and quota control reference service")]
struct Args {
    /// Configuration file path
    #[arg(short, long, default_value = "creditservice.toml")]
    config: PathBuf,

    /// Listen address for gRPC (overrides config)
    #[arg(long)]
    listen_addr: Option<SocketAddr>,

    /// Listen address for HTTP REST API (overrides config)
    #[arg(long)]
    http_addr: Option<SocketAddr>,

    /// FlareDB endpoint for persistent metadata/user data storage (overrides config)
    #[arg(long)]
    flaredb_endpoint: Option<String>,

    /// Storage backend (flaredb, postgres, sqlite) (overrides config)
    #[arg(long)]
    storage_backend: Option<StateBackend>,

    /// SQL database URL for storage (required for postgres/sqlite backend; overrides config)
    #[arg(long)]
    database_url: Option<String>,

    /// Run in single-node mode (required when storage backend is SQLite)
    #[arg(long)]
    single_node: bool,

    /// ChainFire endpoint for cluster coordination (overrides config)
    #[arg(long)]
    chainfire_endpoint: Option<String>,

    /// IAM server address (overrides config)
    #[arg(long)]
    iam_server_addr: Option<String>,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    photon_runtime::init_tracing("info")?;

    let args = Args::parse();
    let mut config = config::load_config(&args.config)?;
    apply_overrides(&mut config, args);
    apply_secret_env_overrides(&mut config);

    info!("Starting CreditService server on {}", config.listen_addr);

    let (mut health_reporter, health_service) = health_reporter();
    health_reporter
        .set_serving::<CreditServiceServer<CreditServiceImpl>>()
        .await;

    let storage: Arc<dyn creditservice_api::CreditStorage> = match config.storage_backend {
        StateBackend::Flaredb => {
            let flaredb_endpoint = config
                .flaredb_endpoint
                .as_deref()
                .unwrap_or("127.0.0.1:2479");
            info!("Using FlareDB for persistent storage: {}", flaredb_endpoint);
            FlareDbStorage::new_with_pd(
                flaredb_endpoint,
                config.chainfire_endpoint.as_deref(),
            )
            .await?
        }
        StateBackend::Postgres | StateBackend::Sqlite => {
            let database_url = config.database_url.as_deref().ok_or_else(|| {
                anyhow::anyhow!(
                    "database_url is required when storage_backend={} (config key: database_url)",
                    config.storage_backend
                )
            })?;
            ensure_sql_backend_matches_url(config.storage_backend, database_url)?;
            info!(
                "Using {} storage backend: {}",
                config.storage_backend, database_url
            );
            SqlStorage::new(database_url, config.single_node).await?
        }
        other => {
            return Err(anyhow::anyhow!(
                "unsupported storage backend '{}' for creditservice; expected flaredb, postgres, or sqlite",
                other
            ));
        }
    };

    if let Some(endpoint) = &config.chainfire_endpoint {
        let endpoint = endpoint.clone();
        let addr = config.listen_addr.to_string();
        tokio::spawn(async move {
            if let Err(error) =
                register_chainfire_membership(&endpoint, "creditservice", addr).await
            {
                tracing::warn!(error = %error, "ChainFire membership registration failed");
            }
        });
    }

    info!("Connecting to IAM server at {}", config.iam_server_addr);
    let auth_service = Arc::new(connect_iam(&config.iam_server_addr).await?);

    let credit_service = Arc::new(CreditServiceImpl::new(storage, auth_service.clone()));

    let auth_runtime = Arc::new(tokio::runtime::Runtime::new()?);
    let make_interceptor = |auth: Arc<AuthService>| {
        let rt = auth_runtime.clone();
        move |mut req: Request<()>| -> Result<Request<()>, Status> {
            let auth = auth.clone();
            tokio::task::block_in_place(|| {
                rt.block_on(async move {
                    let tenant_context = auth.authenticate_request(&req).await?;
                    req.extensions_mut().insert(tenant_context);
                    Ok(req)
                })
            })
        }
    };

    let grpc_server = Server::builder()
        .add_service(health_service)
        .add_service(tonic::codegen::InterceptedService::new(
            CreditServiceServer::new(credit_service.as_ref().clone()),
            make_interceptor(auth_service.clone()),
        ))
        .serve(config.listen_addr);

    let http_addr = config.http_addr;
    let rest_state = rest::RestApiState {
        credit_service: credit_service.clone(),
        auth_service: auth_service.clone(),
    };
    let rest_app = rest::build_router(rest_state);
    let http_listener = tokio::net::TcpListener::bind(&http_addr).await?;

    info!(
        "CreditService HTTP REST API server starting on {}",
        http_addr
    );

    let http_server = async move {
        axum::serve(http_listener, rest_app)
            .await
            .map_err(|e| anyhow::anyhow!("HTTP server error: {}", e))
    };

    tokio::select! {
        result = grpc_server => {
            result?;
        }
        result = http_server => {
            result?;
        }
    }

    Ok(())
}

fn apply_overrides(config: &mut ServerConfig, args: Args) {
    if let Some(listen_addr) = args.listen_addr {
        config.listen_addr = listen_addr;
    }
    if let Some(http_addr) = args.http_addr {
        config.http_addr = http_addr;
    }
    if let Some(flaredb_endpoint) = args.flaredb_endpoint {
        config.flaredb_endpoint = Some(flaredb_endpoint);
    }
    if let Some(storage_backend) = args.storage_backend {
        config.storage_backend = storage_backend;
    }
    if let Some(database_url) = args.database_url {
        config.database_url = Some(database_url);
    }
    if args.single_node {
        config.single_node = true;
    }
    if let Some(chainfire_endpoint) = args.chainfire_endpoint {
        config.chainfire_endpoint = Some(chainfire_endpoint);
    }
    if let Some(iam_server_addr) = args.iam_server_addr {
        config.iam_server_addr = iam_server_addr;
    }
}

fn apply_secret_env_overrides(config: &mut ServerConfig) {
    if config.database_url.is_none() {
        if let Ok(database_url) = std::env::var("CREDITSERVICE_DATABASE_URL") {
            let trimmed = database_url.trim();
            if !trimmed.is_empty() {
                config.database_url = Some(trimmed.to_string());
            }
        }
    }
}

async fn register_chainfire_membership(
    endpoint: &str,
    service: &str,
    addr: String,
) -> anyhow::Result<()> {
    let node_id =
        std::env::var("HOSTNAME").unwrap_or_else(|_| format!("{}-{}", service, std::process::id()));
    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    let key = format!("/cluster/{}/members/{}", service, node_id);
    let value = format!(r#"{{"addr":"{}","ts":{}}}"#, addr, ts);
    let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(120);
    let mut attempt = 0usize;
    let mut last_error = String::new();

    loop {
        attempt += 1;
        match ChainFireClient::connect(endpoint).await {
            Ok(mut client) => match client.put_str(&key, &value).await {
                Ok(_) => return Ok(()),
                Err(error) => last_error = format!("put failed: {}", error),
            },
            Err(error) => last_error = format!("connect failed: {}", error),
        }

        if tokio::time::Instant::now() >= deadline {
            break;
        }

        tracing::warn!(
            attempt,
            endpoint,
            service,
            error = %last_error,
            "retrying ChainFire membership registration"
        );
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
    }

    anyhow::bail!(
        "failed to register ChainFire membership for {} via {} after {} attempts: {}",
        service,
        endpoint,
        attempt,
        last_error
    )
}
