pub mod handlers;
pub mod state;

use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use anyhow::Context;
use axum::{
    extract::ConnectInfo, routing::{any, get, post}, Extension
};
use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore};
use axum_session_sqlx::SessionPgPool;
use handlers::{
    fileserv::file_and_error_handler, leptos::{leptos_routes_handler, server_fn_handler}, oauth, user::activate_account, websocket::ws_handler
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use state::AppState;
use tokio::net;

use crate::domain::{api::ports::ApiService, leptos::app::App};

/// The application's HTTP server. The underlying HTTP package is opaque to module consumers.
pub struct HttpServer {
    router: axum::Router,
    listener: net::TcpListener,
}

impl HttpServer {
    /// Returns a new HTTP server bound to the port specified in `config`.
    pub async fn new<S: ApiService>(app_state: AppState<S>, pool: sqlx::Pool<sqlx::Postgres>) -> anyhow::Result<Self> {
        let leptos_options = &app_state.leptos_options;
        let addr = leptos_options.site_addr;

        let session_config = SessionConfig::default();

        let session_pool = SessionAnyPool::new(SessionPgPool::from(pool));

        let session_store = SessionStore::<SessionAnyPool>::new(Some(session_pool), session_config)
            .await
            .unwrap();

        let trace_layer = tower_http::trace::TraceLayer::new_for_http().make_span_with(
            |request: &axum::extract::Request<_>| {
                let uri = request.uri().to_string();
                
                let remote_addr = match request.headers().get("x-forwarded-for") {
                    Some(a) => a.to_str().unwrap_or("127.0.0.1").parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)),
                    None => {
                        let socket_addr = request.extensions().get::<ConnectInfo<SocketAddr>>();
                        socket_addr.unwrap().ip()
                    }
                };

                tracing::info_span!("http_request", remote_addr = ?remote_addr, method = ?request.method(), uri)
            },
        );

        let router = axum::Router::new()
            .route("/ws", any(ws_handler))
            .nest("/oauth2", oauth::routes())
            .route("/auth/activate/:token", get(activate_account))
            .route("/api/*fn_name", post(server_fn_handler))
            .leptos_routes_with_handler(generate_route_list(App), get(leptos_routes_handler))
            .fallback(file_and_error_handler)
            .layer(Extension(app_state.api_service()))
            .layer(SessionLayer::new(session_store))
            .layer(trace_layer)
            .with_state(app_state);

        let listener = net::TcpListener::bind(&addr)
            .await
            .with_context(|| format!("failed to listen on {}", &addr))?;

        Ok(Self { router, listener })
    }

    /// Runs the HTTP server.
    pub async fn run(self) -> anyhow::Result<()> {
        tracing::debug!("listening on {}", self.listener.local_addr().unwrap());
        axum::serve(
            self.listener,
            self.router
                .into_make_service_with_connect_info::<SocketAddr>(),
        )
        .await
        .context("received error from running server")?;
        Ok(())
    }
}

