use crate::{AppState, error::Result};
use axum::{Router, http::HeaderValue};
use std::{env, sync::Arc};
use tokio::net::TcpListener;
use tower_http::{
  cors::{Any, CorsLayer},
  services::{ServeDir, ServeFile},
};

pub struct App {
  app_state: AppState,
}

impl App {
  pub fn new(app_state: AppState) -> Self {
    Self { app_state }
  }

  pub async fn serve(self) -> Result<()> {
    log::debug!("Starting API...");

    // Build CORS layer.
    //
    // In production both the UI and API are served from the same origin so
    // CORS is a non-issue.  In development, Vite proxies all /api/* calls so
    // the browser also never makes cross-origin requests directly to this
    // server.  We keep a permissive default for convenience, but restrict it
    // when CORS_ORIGIN is explicitly set.
    let cors = match env::var("CORS_ORIGIN") {
      Ok(origin) if origin != "*" => {
        let header_val = origin
          .parse::<HeaderValue>()
          .expect("CORS_ORIGIN is not a valid header value");
        CorsLayer::new()
          .allow_origin(header_val)
          .allow_methods(Any)
          .allow_headers(Any)
          .allow_credentials(true)
      }
      _ => CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any),
    };

    // Serve the built React frontend from ui/dist (relative to the working
    // directory).  Falls back gracefully if the directory does not exist yet
    // (e.g. during development when using `npm run dev`).
    let frontend_dir = env::current_dir()
      .unwrap_or_default()
      .join("ui")
      .join("dist");

    // For SPA routing: any path not matched by a real file (e.g. /map/<id>)
    // falls back to index.html so React can handle client-side routing.
    let index_html = frontend_dir.join("index.html");
    let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(index_html));

    let app = Router::new()
      .nest("/api", crate::get_routes())
      .fallback_service(serve_dir)
      .layer(cors)
      .with_state(Arc::new(self.app_state));

    let api_port: String = env::var("API_PORT").expect("Expected a port in the environment");
    let addr = format!("0.0.0.0:{}", api_port);

    let listener = TcpListener::bind(&addr).await?;
    log::info!("API is listening on {}", &addr);
    Ok(axum::serve(listener, app).await?)
  }
}
