use thiserror::Error;

use crate::ax_backend_lowering::prelude::*;
use crate::ax_backend_parser::{parse_backend_ax, AxBackendParseError};

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendCodegenError {
    #[error("route path is required to generate a route handler")]
    MissingRoutePath,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendCompileError {
    #[error("backend parse failed: {0}")]
    Parse(#[from] AxBackendParseError),
    #[error("backend lowering failed: {0}")]
    Lower(#[from] AxBackendLowerError),
    #[error("backend code generation failed: {0}")]
    Codegen(#[from] AxBackendCodegenError),
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum AxBackendBundleError {
    #[error("backend source `{name}` failed to compile: {source}")]
    Source {
        name: String,
        #[source]
        source: AxBackendCompileError,
    },
}

pub fn generate_backend_module(plan: &AxBackendPlan) -> Result<String, AxBackendCodegenError> {
    let mut out = String::new();
    out.push_str("// @generated by Axonyx backend draft codegen\n");
    out.push_str("#![allow(dead_code, unused_variables)]\n\n");
    out.push_str("use std::collections::BTreeMap;\n\n");
    out.push_str("use axonyx_runtime::backend_prelude::*;\n");
    out.push_str("use serde_json::{json, Value};\n\n");

    for handler in &plan.handlers {
        if let AxHandlerKind::Action { input } = &handler.kind {
            out.push_str(&render_input_struct(handler, input));
            out.push('\n');
        }

        out.push_str(&render_handler_fn(handler)?);
        out.push('\n');
    }

    Ok(out)
}

pub fn compile_backend_ax_to_module(input: &str) -> Result<String, AxBackendCompileError> {
    let document = parse_backend_ax(input)?;
    let plan = lower_backend_document(&document)?;
    Ok(generate_backend_module(&plan)?)
}

pub fn compile_backend_sources_to_module(
    sources: &[(&str, &str)],
) -> Result<String, AxBackendBundleError> {
    let mut handlers = Vec::new();

    for (name, input) in sources {
        let document = parse_backend_ax(input).map_err(|source| AxBackendBundleError::Source {
            name: (*name).to_string(),
            source: AxBackendCompileError::Parse(source),
        })?;
        let plan =
            lower_backend_document(&document).map_err(|source| AxBackendBundleError::Source {
                name: (*name).to_string(),
                source: AxBackendCompileError::Lower(source),
            })?;
        handlers.extend(plan.handlers);
    }

    generate_backend_module(&AxBackendPlan::new(handlers)).map_err(|source| {
        AxBackendBundleError::Source {
            name: "bundle".to_string(),
            source: AxBackendCompileError::Codegen(source),
        }
    })
}

fn render_input_struct(handler: &AxHandlerPlan, input: &[AxFieldPlan]) -> String {
    let mut out = String::new();
    out.push_str("#[derive(Debug, Clone)]\n");
    out.push_str(&format!(
        "pub struct {} {{\n",
        action_input_struct_name(&handler.rust_fn)
    ));

    for field in input {
        out.push_str(&format!("    pub {}: {},\n", field.name, field.rust_ty));
    }

    out.push_str("}\n");
    out
}

fn render_handler_fn(handler: &AxHandlerPlan) -> Result<String, AxBackendCodegenError> {
    let signature = match &handler.kind {
        AxHandlerKind::Action { .. } => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime, input: &{}) -> AxRuntimeResult<Value>",
            handler.rust_fn,
            action_input_struct_name(&handler.rust_fn)
        ),
        AxHandlerKind::Loader => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime, context: &AxLoaderContext) -> AxRuntimeResult<Value>",
            handler.rust_fn
        ),
        _ => format!(
            "pub fn {}(runtime: &impl AxBackendRuntime) -> AxRuntimeResult<Value>",
            handler.rust_fn
        ),
    };

    let mut out = String::new();
    out.push_str(&signature);
    out.push_str(" {\n");

    if let AxHandlerKind::Route { method, path } = &handler.kind {
        if path.is_empty() {
            return Err(AxBackendCodegenError::MissingRoutePath);
        }
        out.push_str(&format!("    // route {method} {path}\n"));
    }

    for step in &handler.steps {
        out.push_str(&render_step(step));
    }

    if !handler
        .steps
        .iter()
        .any(|step| matches!(step, AxStepPlan::Return(_)))
    {
        out.push_str("    Ok(ok_payload())\n");
    }

    out.push_str("}\n");
    Ok(out)
}

fn render_step(step: &AxStepPlan) -> String {
    match step {
        AxStepPlan::Let { binding, value } => {
            format!("    let {binding} = {};\n", render_value_plan(value))
        }
        AxStepPlan::Insert { collection, fields } => format!(
            "    runtime.insert(&AxInsertRequest {{\n        collection: {:?}.to_string(),\n        fields: {},\n    }})?;\n",
            collection,
            render_fields_map(fields)
        ),
        AxStepPlan::Update {
            collection,
            fields,
            filters,
        } => format!(
            "    runtime.update(&AxUpdateRequest {{\n        collection: {:?}.to_string(),\n        fields: {},\n        filters: {},\n    }})?;\n",
            collection,
            render_fields_map(fields),
            render_query_filters(filters)
        ),
        AxStepPlan::Delete { collection, filters } => format!(
            "    runtime.delete(&AxDeleteRequest {{\n        collection: {:?}.to_string(),\n        filters: {},\n    }})?;\n",
            collection,
            render_query_filters(filters)
        ),
        AxStepPlan::Revalidate { target } => {
            format!("    runtime.revalidate({})?;\n", render_borrowed_expr(target))
        }
        AxStepPlan::Return(value) => match value {
            AxReturnPlan::Expr(expr) => format!("    Ok(json!({}))\n", render_borrowed_expr(expr)),
            AxReturnPlan::Ok => "    Ok(ok_payload())\n".to_string(),
        },
        AxStepPlan::Send { target, payload } => format!(
            "    runtime.send(&AxSendRequest {{\n        target: {:?}.to_string(),\n        payload: json!({}),\n    }})?;\n",
            target,
            render_borrowed_expr(payload)
        ),
    }
}

fn render_value_plan(value: &AxValuePlan) -> String {
    match value {
        AxValuePlan::Expr(expr) => format!("json!({})", render_borrowed_expr(expr)),
        AxValuePlan::Query(query) => format!("runtime.load(&{})?", render_query_plan(query)),
    }
}

fn render_query_plan(query: &AxQueryPlan) -> String {
    let filters = if query.filters.is_empty() {
        "vec![]".to_string()
    } else {
        let entries = query
            .filters
            .iter()
            .map(|filter| {
                format!(
                    "AxQueryFilterRequest {{ field: {:?}.to_string(), op: {}, value: json!({}) }}",
                    filter.field,
                    render_filter_op(filter.op),
                    render_borrowed_expr(&filter.value)
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        format!("vec![{entries}]")
    };

    let orders = if query.orders.is_empty() {
        "vec![]".to_string()
    } else {
        let entries = query
            .orders
            .iter()
            .map(|order| {
                format!(
                    "AxQueryOrderRequest {{ field: {:?}.to_string(), direction: {} }}",
                    order.field,
                    render_order_direction(order.direction)
                )
            })
            .collect::<Vec<_>>()
            .join(", ");
        format!("vec![{entries}]")
    };

    let source = match &query.source {
        AxQuerySourcePlan::Stream { collection } => format!("{collection:?}.to_string()"),
        AxQuerySourcePlan::ContentCollection { collection } => {
            format!("{collection:?}.to_string()")
        }
    };

    format!(
        "AxQueryRequest {{ collection: {source}, filters: {filters}, orders: {orders}, limit: {}, offset: {} }}",
        render_option_u32(query.limit),
        render_option_u32(query.offset)
    )
}

fn render_fields_map(fields: &[AxAssignmentPlan]) -> String {
    if fields.is_empty() {
        return "BTreeMap::new()".to_string();
    }

    let entries = fields
        .iter()
        .map(|field| {
            format!(
                "({:?}.to_string(), json!({}))",
                field.name,
                render_borrowed_expr(&field.value)
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    format!("BTreeMap::from([{entries}])")
}

fn render_query_filters(filters: &[AxQueryFilterPlan]) -> String {
    if filters.is_empty() {
        return "Vec::new()".to_string();
    }

    let entries = filters
        .iter()
        .map(|filter| {
            format!(
                "AxQueryFilterRequest {{ field: {:?}.to_string(), op: {}, value: json!({}) }}",
                filter.field,
                render_filter_op(filter.op),
                render_borrowed_expr(&filter.value)
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    format!("vec![{entries}]")
}

fn render_borrowed_expr(expr: &AxRustExpr) -> String {
    if let Some(context_expr) = render_context_lookup(&expr.code) {
        context_expr
    } else {
        format!("&{}", expr.code)
    }
}

fn render_context_lookup(code: &str) -> Option<String> {
    let (source, field) = code.split_once('.')?;
    match source {
        "params" => Some(format!("&context.param({field:?})?")),
        "query" => Some(format!("&context.query({field:?})?")),
        _ => None,
    }
}

fn render_filter_op(op: AxQueryFilterOpPlan) -> &'static str {
    match op {
        AxQueryFilterOpPlan::Eq => "AxQueryFilterOp::Eq",
    }
}

fn render_order_direction(direction: AxQueryOrderDirectionPlan) -> &'static str {
    match direction {
        AxQueryOrderDirectionPlan::Asc => "AxQueryOrderDirection::Asc",
        AxQueryOrderDirectionPlan::Desc => "AxQueryOrderDirection::Desc",
    }
}

fn render_option_u32(value: Option<u32>) -> String {
    match value {
        Some(value) => format!("Some({value})"),
        None => "None".to_string(),
    }
}

fn action_input_struct_name(rust_fn: &str) -> String {
    let suffix = rust_fn.strip_prefix("action_").unwrap_or(rust_fn);
    let mut out = String::from("Action");

    for part in suffix.split('_').filter(|part| !part.is_empty()) {
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            out.push(first.to_ascii_uppercase());
            out.extend(chars.map(|ch| ch.to_ascii_lowercase()));
        }
    }

    out.push_str("Input");
    out
}

pub mod prelude {
    pub use super::compile_backend_ax_to_module;
    pub use super::compile_backend_sources_to_module;
    pub use super::generate_backend_module;
    pub use super::AxBackendBundleError;
    pub use super::AxBackendCodegenError;
    pub use super::AxBackendCompileError;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ax_ast::prelude::AxExpr;
    use crate::ax_backend_ast::prelude::*;
    use crate::ax_backend_lowering::lower_backend_document;

    #[test]
    fn generates_loader_and_action_module_against_runtime_traits() {
        let document = AxBackendDocument::new([
            AxBackendBlock::Loader(AxLoader::new(
                "PostsList",
                [
                    AxBackendStmt::data(
                        "posts",
                        crate::ax_query_ast::prelude::AxQuerySpec::new(
                            crate::ax_query_ast::prelude::AxQuerySource::Stream {
                                collection: "posts".to_string(),
                            },
                        )
                        .limit(20),
                    ),
                    AxBackendStmt::r#return(AxExpr::ident("posts")),
                ],
            )),
            AxBackendBlock::Action(
                AxAction::new("CreatePost")
                    .input([
                        AxField::new("title", "string"),
                        AxField::new("featured", "bool"),
                    ])
                    .body([
                        AxBackendStmt::insert(
                            "posts",
                            [
                                AxAssignment::new("title", AxExpr::ident("input").member("title")),
                                AxAssignment::new(
                                    "featured",
                                    AxExpr::ident("input").member("featured"),
                                ),
                            ],
                        ),
                        AxBackendStmt::r#return("ok"),
                    ]),
            ),
        ]);

        let plan = lower_backend_document(&document).expect("document should lower");
        let module = generate_backend_module(&plan).expect("module should generate");

        assert!(module.contains("use axonyx_runtime::backend_prelude::*;"));
        assert!(module.contains("#![allow(dead_code, unused_variables)]"));
        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains("runtime.load(&AxQueryRequest"));
        assert!(module.contains("pub struct ActionCreatePostInput"));
        assert!(module.contains("pub fn action_create_post(runtime: &impl AxBackendRuntime, input: &ActionCreatePostInput)"));
        assert!(module.contains("runtime.insert(&AxInsertRequest"));
        assert!(module.contains("Ok(ok_payload())"));
    }

    #[test]
    fn compiles_backend_ax_source_directly_into_module() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostsList
  data posts = Db.Stream("posts")
    where status = "published"
    limit 12
  return posts
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains("field: \"status\".to_string()"));
        assert!(module.contains("limit: Some(12)"));
    }

    #[test]
    fn compiles_loader_params_and_query_into_context_lookups() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostDetail
  data posts = Db.Stream("posts")
    where slug = params.slug
    where status = query.status
    limit 1
  return posts
"#,
        )
        .expect("source should compile");

        assert!(module.contains(
            "pub fn loader_post_detail(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains(r#"value: json!(&context.param("slug")?)"#));
        assert!(module.contains(r#"value: json!(&context.query("status")?)"#));
    }

    #[test]
    fn compiles_runtime_env_access_into_runtime_env_calls() {
        let module = compile_backend_ax_to_module(
            r#"
loader PostsList
  data db_url = Runtime.Env.secret.db_url
  data app_name = Runtime.Env.public.app_name
  return app_name
"#,
        )
        .expect("source should compile");

        assert!(module.contains(r#"let db_url = json!(&runtime.env().secret("db_url")?);"#));
        assert!(module.contains(r#"let app_name = json!(&runtime.env().public("app_name")?);"#));
    }

    #[test]
    fn bundles_multiple_backend_sources_into_single_module() {
        let module = compile_backend_sources_to_module(&[
            (
                "app/posts/loader.ax",
                r#"
loader PostsList
  data posts = Db.Stream("posts")
  return posts
"#,
            ),
            (
                "routes/api/posts.ax",
                r#"
route GET "/api/posts"
  data posts = Db.Stream("posts")
  return posts
"#,
            ),
        ])
        .expect("sources should compile");

        assert!(module.contains(
            "pub fn loader_posts_list(runtime: &impl AxBackendRuntime, context: &AxLoaderContext)"
        ));
        assert!(module.contains("pub fn route_get_api_posts(runtime: &impl AxBackendRuntime)"));
    }

    #[test]
    fn compiles_delete_where_into_runtime_delete_request() {
        let module = compile_backend_ax_to_module(
            r#"
action RemovePost
  input:
    id: i64

  delete "posts"
    where id = input.id

  return ok
"#,
        )
        .expect("source should compile");

        assert!(module.contains("runtime.delete(&AxDeleteRequest"));
        assert!(module.contains("filters: vec![AxQueryFilterRequest"));
        assert!(module.contains("field: \"id\".to_string()"));
    }
}
