use axum::{
    extract::{Form, Path},
    http::StatusCode,
    response::{Html, Json},
    routing::{get, post},
    Router,
};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
use url::Url;

use microformats::jf2::IntoJf2;

mod parser_ext;
mod schemas;
mod validation;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", get(root))
        .route("/validate", post(validate))
        .route("/schema/:type", get(schema))
        .layer(CorsLayer::permissive());

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("listening on {}", addr);
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn root() -> Html<&'static str> {
    Html(include_str!("../static/index.html"))
}

#[derive(Deserialize)]
struct ValidateRequest {
    input: String,
}

#[derive(Serialize)]
struct ValidateResponse {
    jf2: serde_json::Value,
    html: String,
    invalid_ids: Vec<String>,
    errors: Vec<String>,
}

async fn validate(Form(req): Form<ValidateRequest>) -> Result<Json<ValidateResponse>, StatusCode> {
    let client = Client::new();
    let html = if req.input.starts_with("http://") || req.input.starts_with("https://") {
        // Fetch URL
        match client.get(&req.input).send().await {
            Ok(resp) => match resp.text().await {
                Ok(text) => text,
                Err(_) => return Err(StatusCode::BAD_REQUEST),
            },
            Err(_) => return Err(StatusCode::BAD_REQUEST),
        }
    } else {
        req.input.clone()
    };

    // Parse with extended parser
    let extended_parser = match parser_ext::ExtendedParser::from_html(html.clone()) {
        Ok(p) => p,
        Err(_) => return Err(StatusCode::BAD_REQUEST),
    };

    let (doc, source_map, html_with_ids) = match extended_parser.into_document_with_tracking(Some(Url::parse("http://example.com").unwrap())) {
        Ok(result) => result,
        Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
    };

    // Convert to JF2
    let jf2 = match doc.into_jf2() {
        Ok(j) => j,
        Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
    };

    let jf2_value = serde_json::to_value(jf2).unwrap();

    // Validate
    let validator = validation::Validator::new();
    let (invalid_ids, errors) = validator.validate(&jf2_value, &source_map);

    Ok(Json(ValidateResponse {
        jf2: jf2_value,
        html: html_with_ids,
        invalid_ids,
        errors,
    }))
}

async fn schema(Path(type_name): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
    let validator = validation::Validator::new();
    if let Some(schema) = validator.get_schema(&type_name) {
        // Get the compiled schema's JSON representation
        // This is a bit hacky, but schemars doesn't expose the JSON directly
        let schema_json = match type_name.as_str() {
            "h-entry" => schemas::generate_schema::<schemas::HEntry>(),
            "h-review" => schemas::generate_schema::<schemas::HReview>(),
            "h-feed" => schemas::generate_schema::<schemas::HFeed>(),
            "h-item" => schemas::generate_schema::<schemas::HItem>(),
            _ => return Err(StatusCode::NOT_FOUND),
        };
        Ok(Json(schema_json))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}