use diesel::prelude::*;
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, Form, http};
use axum::http::HeaderValue;
use deadpool_diesel::sqlite::{Runtime, Manager, Pool};
use clap::Parser;
use tower::{ServiceBuilder};
use tower_http::{
    cors::{Any, CorsLayer},
    trace::{TraceLayer, DefaultOnResponse},
    services::ServeDir,
    LatencyUnit
};
use serde::Deserialize;
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,
}

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(),
    };
    // 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/", get(get_list_feed).post(post_feed))
        .route("/feed/:id", get(get_feed).delete(delete_feed).put(put_feed))
        .route("/feed/:id/edit/form", get(get_feed_form))
        .route("/feed/:id/edit/inline", get(get_feed_inline))
        .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="feed_form.html")]
struct FeedFormTemplate {
    feed: RssFeed
}

async fn get_feed_form(
    State(ctx): State<AppContext>,
    Path(feed_id): Path<i32>
) -> impl IntoResponse {
    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 {
        FeedFormTemplate{ feed }
    } else {
        FeedFormTemplate{
            feed: RssFeed{
                id: -1,
                name: "ERROR".to_string(),
                feed_url: "ERROR".to_string(),
                last_pub_date: Some("ERROR".to_string())
            }
        }
    }
}

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

async fn get_feed_inline(
    State(ctx): State<AppContext>,
    Path(feed_id): Path<i32>
) -> impl IntoResponse {
    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 {
        FeedInlineTemplate{ feed }
    } else {
        FeedInlineTemplate{
            feed: RssFeed{
                id: -1,
                name: "ERROR".to_string(),
                feed_url: "ERROR".to_string(),
                last_pub_date: Some("ERROR".to_string())
            }
        }
    }
}

#[derive(Deserialize, AsChangeset)]
#[diesel(table_name = crate::schema::rss_feeds)]
struct PostForm{
    name: String,
    feed_url: String,
}

async fn post_feed(
    State(ctx): State<AppContext>,
    Form(post): Form<PostForm>,
) -> impl IntoResponse {
    let conn = ctx.pool.get().await.unwrap();
    use self::schema::rss_feeds::dsl::*;
    let result = conn.interact(move |conn| {
        let new_feed = NewRssFeed{name: post.name.as_str(), feed_url: post.feed_url.as_str() };
        diesel::insert_into(rss_feeds)
            .values(&new_feed)
            .returning(RssFeed::as_select())
            .get_result(conn)
            .expect("Error saving new feed")
    }).await.unwrap();
    FeedTemplate{ feed: result }
}

async fn put_feed(
    State(ctx): State<AppContext>,
    Path(feed_id): Path<i32>,
    Form(post): Form<PostForm>
) -> impl IntoResponse {
    let conn = ctx.pool.get().await.unwrap();
    use self::schema::rss_feeds::dsl::*;
    let result = conn.interact(move |conn| {
        diesel::update(rss_feeds.find(feed_id))
            .set(post)
            .returning(RssFeed::as_select())
            .get_result(conn)
            .expect("Error updating feed")
    }).await.unwrap();
    FeedTemplate{ feed: result }
}


#[derive(Template)]
#[template(source="\
{% for feed in feeds %}
{% include \"feed.html\" %}
{% endfor %}", ext="html")]
struct FeedsTemplate {
    feeds: Vec<RssFeed>
}

async fn get_list_feed(
    State(ctx): State<AppContext>,
) -> FeedsTemplate {
    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();
    FeedsTemplate{ feeds: result }
}

async fn delete_feed(
    State(ctx): State<AppContext>,
    Path(feed_id): Path<i32>,
) -> impl IntoResponse {
    let conn = ctx.pool.get().await.unwrap();
    use self::schema::rss_feeds::dsl::*;
    let num_deleted = conn.interact(move |conn| {
        diesel::delete(rss_feeds.find(feed_id))
            .execute(conn)
            .expect("Error deleting posts")
    }).await.unwrap();
    if num_deleted == 0 {
        (StatusCode::NOT_FOUND, "Not found")
    } else {
        // HTMX swaps only on 200, so we cannot use NO_CONTENT
        // In fact, MSDN says that NO_CONTENT means "do nothing", but this is debated.
        (StatusCode::OK, "")
    }
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    feeds: FeedsTemplate
}

async fn index(
    State(ctx): State<AppContext>,
) -> IndexTemplate {
    let feeds = get_list_feed(State(ctx)).await;
    IndexTemplate { feeds }
}
