use diesel::prelude::*;
use std::env;
use std::sync::Arc;
use anyhow::{anyhow, Result, Context};
use askama_axum::Template;
use axum::{
    extract::{Path, State},
    http::{Method, header, StatusCode, Request, Uri},
    response::{IntoResponse, Response, Html},
    routing::{get, post},
    Json, Router,
};
use deadpool_diesel::sqlite::{Runtime, Manager, Pool};
use clap::Parser;
use tokio::sync::Mutex;
use tower::{ServiceBuilder};
use tower_http::{
    cors::{Any, CorsLayer},
    trace::{TraceLayer, DefaultOnResponse},
    services::ServeDir,
    LatencyUnit
};
use tracing::{
    info, error, debug, info_span, enabled,
    instrument, Level, Span
};

pub mod models;
pub mod schema;

use self::models::*;

#[derive(Parser,Clone)]
struct Config {
    #[arg(long,env)]
    database_url: String,
    #[arg(long,env)]
    http_host: String,
    #[arg(long,env)]
    http_port: u16,
}

#[derive(Clone)]
struct AppContext {
    config: Config,
    pool: Pool,
    counter: Arc<Mutex<i32>>
}

async fn establish_connection(database_url: &str) -> Pool {
    let manager = Manager::new(database_url, Runtime::Tokio1);
    let pool = Pool::builder(manager)
        .max_size(8)
        .build()
        .unwrap();
    let _ = pool.get().await.expect("DB connection failed"); // Test if DB connection was fine
    pool
}

#[tokio::main]
async fn main() -> Result<()> {
    // Load Env File
    dotenvy::dotenv().ok();
    // Init Tracing
    tracing_subscriber::fmt::init();
    // Parse Env/CLI
    let config = Config::parse();
    // Establish Connection Pool
    let pool: Pool = establish_connection(&config.database_url).await;
    // Prepare Web Server Context
    let context = AppContext {
        config: config.clone(),
        pool: pool.clone(),
        counter: Arc::new(Mutex::new(0)),
    };
    // Prepare Middlewares
    let cors = CorsLayer::very_permissive();
    let tracelayer = TraceLayer::new_for_http()
        .make_span_with(|request: &Request<_>| {
            // Log the matched route's path (with placeholders not filled in).
            let path = request.uri().to_string();
            info_span!(
                "request",
                method = ?request.method(),
                path
            )
        })
        .on_request(|_request: &Request<_>, _span: &Span| {
            debug!("Request received");
        })
        .on_response(
            DefaultOnResponse::new()
                .level(Level::DEBUG)
                .latency_unit(LatencyUnit::Millis)
        );
    let middlewares = ServiceBuilder::new()
        .layer(tracelayer).layer(cors);
    // Launch Web Server
    let app = Router::new()
        .nest_service("/assets", ServeDir::new("assets"))
        .route("/", get(index))
        .route("/feed/:id/", get(get_feed))
        .layer(middlewares)
        .with_state(context.clone());
    // Run our app with hyper
    let listener = tokio::net::TcpListener::bind((config.http_host.as_str(), config.http_port))
        .await.context(format!("Failed to bind Web Service on {}:{}", config.http_host, config.http_port))?;
    info!("Listening on {}:{}", config.http_host, config.http_port);
    axum::serve(listener, app).await.context("Failed to serve Web Service")?;
    Ok(())
}

#[derive(Template)]
#[template(path="feed.html")]
struct FeedTemplate {
    feed: RssFeed
}

async fn get_feed(
    State(ctx): State<AppContext>,
    Path(feed_id): Path<i32>
) -> FeedTemplate {
    let conn = ctx.pool.get().await.unwrap();
    use self::schema::rss_feeds::dsl::*;
    let result = conn.interact(move |conn| {
        rss_feeds
            .find(feed_id)
            .select(RssFeed::as_select())
            .first(conn)
            .optional()
            .expect("Error loading feeds")
    }).await.unwrap();
    if let Some(feed) = result {
        FeedTemplate{ feed }
    } else {
        FeedTemplate{
            feed: RssFeed{
                id: -1,
                name: "ERROR".to_string(),
                feed_url: "ERROR".to_string(),
                last_pub_date: Some("ERROR".to_string())
            }
        }
    }
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    feeds: Vec<RssFeed>,
}

async fn index(
    State(ctx): State<AppContext>,
) -> IndexTemplate {
    let conn = ctx.pool.get().await.unwrap();
    use self::schema::rss_feeds::dsl::*;
    let result = conn.interact(|conn| {
        rss_feeds
            .select(RssFeed::as_select())
            .load(conn)
            .expect("Error loading feeds")
    }).await.unwrap();
    IndexTemplate { feeds: result }
}
