mod auth;
mod endpoints;
mod error;
mod middleware;
mod user;
mod validate;

use std::env;

use actix_web::{
    App, HttpServer,
    middleware::from_fn,
    rt::System,
    web::{self},
};
use aws_config::BehaviorVersion;
use sqlx::PgPool;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
    EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt,
};

use crate::{
    auth::{Auth, JWT},
    user::UserRepository,
};

struct AppState {
    reqwest_client: reqwest::Client,
    auth: Auth,
    user: UserRepository,
}

async fn run() -> std::io::Result<()> {
    let reqwest_client = reqwest::Client::builder()
        .user_agent(concat!(
            env!("CARGO_PKG_NAME"),
            "/",
            env!("CARGO_PKG_VERSION")
        ))
        .build()
        .expect("failed to create reqwest client");
    let config = aws_config::load_defaults(BehaviorVersion::v2026_01_12()).await;
    let dynamodb_client = aws_sdk_dynamodb::Client::new(&config);

    let app_data = web::Data::new(AppState {
        reqwest_client: reqwest_client.clone(),
        auth: Auth::JWT(JWT::new(reqwest_client.clone())),
        user: UserRepository::new(
            PgPool::connect(
                &env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set"),
            )
            .await
            .expect("error connecting to db"),
            dynamodb_client,
        ),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(app_data.clone())
            .wrap(
                sentry::integrations::actix::Sentry::builder()
                    .capture_server_errors(true)
                    .start_transaction(true)
                    .finish(),
            )
            .wrap(tracing_actix_web::TracingLogger::default())
            .wrap(actix_cors::Cors::permissive())
            .route(
                "/",
                web::get()
                    .to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
            )
            .service(
                web::scope("/api").service(
                    web::scope("/v0")
                        .service(
                            web::scope("/user")
                                .route("/repos", web::get().to(endpoints::get_repos::get_repos))
                                .wrap(from_fn(middleware::protected))
                                .route(
                                    "/repos/search",
                                    web::get().to(endpoints::search_repos::search_repos),
                                )
                                .wrap(from_fn(middleware::protected))
                                .route("/repo/add", web::post().to(endpoints::add_repo::add_repo))
                                .wrap(from_fn(middleware::protected)),
                        )
                        .route(
                            "/repos",
                            web::get().to(endpoints::global_repos::global_repos),
                        )
                        .route(
                            "/repos/{repo_id}/files/{file:.*}",
                            web::get().to(endpoints::proxy_file::proxy_file),
                        ),
                ),
            )
    })
    .bind((env::var("ADDRESS").unwrap_or("0.0.0.0".to_string()), 8080))?
    .run()
    .await
}

// cant use #[actix_web::main] because of Sentry
// Note: "Macros like #[tokio::main] and #[actix_web::main] are not supported. The Sentry client must be initialized before the async runtime is started."
// https://docs.sentry.io/platforms/rust/guides/actix-web/
fn main() -> std::io::Result<()> {
    let tracing_env_filter = EnvFilter::builder()
        .with_default_directive(LevelFilter::INFO.into())
        .from_env_lossy()
        .add_directive("reqwest=info".parse().unwrap())
        .add_directive("hyper=info".parse().unwrap())
        .add_directive("h2=info".parse().unwrap())
        .add_directive("rustls=info".parse().unwrap())
        .add_directive("aws=info".parse().unwrap());

    tracing_subscriber::registry()
        .with(tracing_env_filter)
        .with(
            tracing_subscriber::fmt::layer()
                .compact()
                .with_span_events(FmtSpan::CLOSE),
        )
        .with(sentry::integrations::tracing::layer())
        .init();

    let guard = sentry::init((
        env::var("SENTRY_DSN").ok(),
        sentry::ClientOptions {
            release: sentry::release_name!(),
            traces_sample_rate: 1.0,
            session_mode: sentry::SessionMode::Request,
            debug: true,
            ..Default::default()
        },
    ));

    if guard.is_enabled() {
        tracing::info!("sentry initialized");
    } else {
        tracing::info!("sentry **NOT** initialized")
    };

    System::new().block_on(run())
}
