//! Code emission — generates Rust source text for Axum REST handlers.
//!
//! All `let _ = write!(...)` / `let _ = writeln!(...)` calls discard the `Result`
//! because writing to a `String` is infallible (`fmt::Write for String` never fails).

use std::fmt::Write as _;

use super::config::RestCodegenConfig;
use super::types::{MethodRoute, ParamAssignment, ServiceRoute};

pub(crate) fn generate_code(services: &[ServiceRoute], config: &RestCodegenConfig) -> String {
    let mut code = String::with_capacity(16_384);

    write_header(&mut code, services, config);

    // Per-service routers and handlers
    for service in services {
        generate_service(&mut code, service, config);
    }

    // Combined router
    generate_all_routes(&mut code, services, config);

    code
}

fn write_header(code: &mut String, services: &[ServiceRoute], config: &RestCodegenConfig) {
    // Scan all methods to determine which imports are actually needed.
    let mut needs_json = false;
    let mut needs_query = false;
    let mut needs_path = false;
    let mut needs_status_code = false;
    let mut needs_sse = false;

    for service in services {
        for method in &service.methods {
            if method.server_streaming {
                needs_sse = true;
                if method.http_method == "get" {
                    needs_query = true;
                } else {
                    needs_json = true;
                }
            } else {
                // JSON handler
                if method.returns_empty {
                    needs_status_code = true;
                } else {
                    needs_json = true; // Json<Response>
                }
                if method.has_body && method.http_method != "get" {
                    needs_json = true; // Json(body)
                } else if method.http_method == "get" {
                    needs_query = true; // Query(body)
                }
            }
            if !method.path_params.is_empty() {
                needs_path = true;
            }
        }
    }

    code.push_str(
        "\
// Auto-generated REST routes from proto `google.api.http` annotations.
//
// **Do not edit** — regenerated by `build.rs` when proto files change.
//
// Each handler transcodes HTTP/JSON <-> proto and calls the Tonic service trait,
// sharing auth, validation, and business logic with gRPC handlers.

",
    );

    // std imports
    if needs_sse {
        code.push_str("use std::convert::Infallible;\n");
    }
    code.push_str("use std::sync::Arc;\n");
    if needs_sse {
        code.push_str("use std::time::Duration;\n");
    }
    code.push('\n');

    // axum::extract imports
    let mut extractors = Vec::new();
    if config.extension_type.is_some() {
        extractors.push("Extension");
    }
    if needs_json {
        extractors.push("Json");
    }
    if needs_path {
        extractors.push("Path");
    }
    if needs_query {
        extractors.push("Query");
    }
    extractors.push("State");
    write_use_stmt(code, "axum::extract", &extractors);

    // axum::http imports
    let mut http_types = vec!["HeaderMap"];
    if needs_status_code {
        http_types.push("StatusCode");
    }
    write_use_stmt(code, "axum::http", &http_types);

    // SSE imports
    if needs_sse {
        code.push_str("use axum::response::sse::{Event, KeepAlive, Sse};\n");
    }

    code.push_str("use axum::Router;\n");

    // futures imports (only for SSE)
    if needs_sse {
        code.push_str("use futures::stream::{Stream, StreamExt};\n");
    }

    code.push('\n');

    // Combined forwarded headers constant (when extra headers configured)
    if !config.extra_forwarded_headers.is_empty() {
        let rt = &config.runtime_crate;
        code.push_str("/// Combined default + extra headers forwarded to gRPC metadata.\n");
        let _ = writeln!(
            code,
            "const ALL_FORWARDED_HEADERS: &[&str] = {rt}::concat_forwarded_headers!("
        );
        for h in &config.extra_forwarded_headers {
            let _ = writeln!(code, "    \"{h}\",");
        }
        code.push_str(");\n\n");
    }
}

fn generate_service(code: &mut String, service: &ServiceRoute, config: &RestCodegenConfig) {
    let svc_snake = super::to_snake_case(&service.service_name);
    let trait_path = format!(
        "{}::{}::{}_server::{}",
        config.proto_root, service.package_mod, svc_snake, service.service_name
    );

    // Router builder function
    let _ = write!(
        code,
        "\
// =============================================================================
// {service_name} REST routes
// =============================================================================

/// Build Axum REST routes for `{service_name}`.
///
/// Generated from `google.api.http` annotations in `{package}.proto`.
pub fn {svc_snake}_rest_router<S>(service: Arc<S>) -> Router
where
    S: {trait_path} + Send + Sync + 'static,
{{
    Router::new()
",
        service_name = service.service_name,
        package = service.package_mod,
    );

    for method in &service.methods {
        let handler_name = format!("rest_{}_{}", svc_snake, method.rust_name);
        let _ = writeln!(
            code,
            "        .route(\"{path}\", axum::routing::{http_method}({handler}::<S>))",
            path = method.axum_path,
            http_method = method.http_method,
            handler = handler_name,
        );
    }

    code.push_str("        .with_state(service)\n}\n\n");

    // Handler functions
    for method in &service.methods {
        if method.server_streaming {
            generate_sse_handler(code, service, method, config);
        } else {
            generate_json_handler(code, service, method, config);
        }
    }
}

fn generate_sse_handler(
    code: &mut String,
    service: &ServiceRoute,
    method: &MethodRoute,
    config: &RestCodegenConfig,
) {
    let svc_snake = super::to_snake_case(&service.service_name);
    let handler_name = format!("rest_{}_{}", svc_snake, method.rust_name);
    let trait_path = format!(
        "{}::{}::{}_server::{}",
        config.proto_root, service.package_mod, svc_snake, service.service_name
    );
    let rt = &config.runtime_crate;
    let ext_extractor = config.extension_extractor_line();
    let ext_and_req = config.extension_and_request_lines("query");

    let extractor = if method.http_method == "get" {
        format!("    Query(query): Query<{}>,\n", method.input_type)
    } else {
        format!("    Json(query): Json<{}>,\n", method.input_type)
    };

    let _ = write!(
        code,
        "\
#[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
/// `{proto_name}` — SSE streaming endpoint.
///
/// `{http_method} {path}` → `text/event-stream`
async fn {handler_name}<S>(
    State(service): State<Arc<S>>,
    headers: HeaderMap,
{ext_extractor}\
{extractor}\
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, {rt}::RestError>
where
    S: {trait_path} + Send + Sync + 'static,
{{
{ext_and_req}    let response = service.{rust_name}(req).await.map_err({rt}::RestError::from)?;
    let stream = response.into_inner();

    let sse_stream = stream.map(|result| {{
        Ok::<_, Infallible>(match result {{
            Ok(item) => Event::default()
                .json_data(&item)
                .unwrap_or_else(|_| Event::default().data(\"{{}}\")),
            Err(status) => {rt}::sse_error_event(&status),
        }})
    }});

    Ok(Sse::new(sse_stream).keep_alive(
        KeepAlive::new()
            .interval(Duration::from_secs({keep_alive}))
            .text(\"keep-alive\"),
    ))
}}

",
        proto_name = method.proto_name,
        http_method = method.http_method.to_uppercase(),
        path = method.path,
        rust_name = method.rust_name,
        keep_alive = config.sse_keep_alive_secs,
    );
}

fn generate_json_handler(
    code: &mut String,
    service: &ServiceRoute,
    method: &MethodRoute,
    config: &RestCodegenConfig,
) {
    let svc_snake = super::to_snake_case(&service.service_name);
    let handler_name = format!("rest_{}_{}", svc_snake, method.rust_name);
    let trait_path = format!(
        "{}::{}::{}_server::{}",
        config.proto_root, service.package_mod, svc_snake, service.service_name
    );
    let rt = &config.runtime_crate;
    let ext_extractor = config.extension_extractor_line();
    let ext_and_req = config.extension_and_request_lines("body");

    let has_path_params = !method.path_params.is_empty();
    let needs_mut_body = has_path_params;

    let extractors = build_extractors(method, needs_mut_body);
    let body_creation = build_body_creation(method, needs_mut_body);
    let path_assigns = build_path_assigns(method, config);

    // --- Response shape ---
    let return_type_owned;
    let (return_type, call_line, ok_expr) = if method.returns_empty {
        (
            "StatusCode",
            format!(
                "    service.{rust_name}(req).await.map_err({rt}::RestError::from)?;",
                rust_name = method.rust_name,
            ),
            "Ok(StatusCode::NO_CONTENT)".to_string(),
        )
    } else {
        return_type_owned = format!("Json<{}>", method.output_type);
        (
            return_type_owned.as_str(),
            format!(
                "    let response = service.{rust_name}(req).await.map_err({rt}::RestError::from)?;",
                rust_name = method.rust_name,
            ),
            "Ok(Json(response.into_inner()))".to_string(),
        )
    };

    let _ = write!(
        code,
        "\
#[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
/// `{proto_name}` — JSON endpoint.
///
/// `{http_method} {path}`
async fn {handler_name}<S>(
    State(service): State<Arc<S>>,
    headers: HeaderMap,
{ext_extractor}\
{extractors}\
) -> Result<{return_type}, {rt}::RestError>
where
    S: {trait_path} + Send + Sync + 'static,
{{
{body_creation}{path_assigns}{ext_and_req}{call_line}
    {ok_expr}
}}

",
        proto_name = method.proto_name,
        http_method = method.http_method.to_uppercase(),
        path = method.path,
    );
}

/// Build Axum extractor parameters for a JSON handler.
fn build_extractors(method: &MethodRoute, needs_mut_body: bool) -> String {
    let mut out = String::new();

    // Path extractor
    if !method.path_params.is_empty() {
        let names: Vec<&str> = method
            .path_params
            .iter()
            .map(|p| p.axum_name.as_str())
            .collect();
        let types: Vec<&str> = method
            .path_params
            .iter()
            .map(|p| match &p.assignment {
                ParamAssignment::TypedField { rust_type, .. } => rust_type,
                _ => "String",
            })
            .collect();

        if names.len() == 1 {
            let _ = writeln!(
                out,
                "    Path({name}): Path<{ty}>,",
                name = names[0],
                ty = types[0],
            );
        } else {
            let _ = writeln!(
                out,
                "    Path(({names})): Path<({types})>,",
                names = names.join(", "),
                types = types.join(", "),
            );
        }
    }

    // Body/query extractor
    let mut_kw = if needs_mut_body { "mut " } else { "" };
    if method.has_body && method.http_method != "get" {
        let _ = writeln!(out, "    Json({mut_kw}body): Json<{}>,", method.input_type);
    } else if method.http_method == "get" {
        let _ = writeln!(
            out,
            "    Query({mut_kw}body): Query<{}>,",
            method.input_type
        );
    }

    out
}

/// Build the `let body = T::default();` line for endpoints without a request body.
fn build_body_creation(method: &MethodRoute, needs_mut_body: bool) -> String {
    if method.has_body || method.http_method == "get" {
        return String::new();
    }
    let mut_kw = if needs_mut_body { "mut " } else { "" };
    format!("    let {mut_kw}body = {}::default();\n", method.input_type)
}

/// Build path parameter → request field assignment lines.
fn build_path_assigns(method: &MethodRoute, config: &RestCodegenConfig) -> String {
    let mut out = String::new();
    let rt = &config.runtime_crate;

    for param in &method.path_params {
        match &param.assignment {
            ParamAssignment::UuidWrapper { parent_field } => {
                // Invariant: extract_path_params returns Err if wrapper_type is
                // None when UuidWrapper params are present.
                let Some(wrapper) = config.wrapper_type.as_deref() else {
                    unreachable!(
                        "UuidWrapper param requires wrapper_type; \
                         validated during extraction"
                    );
                };
                let _ = writeln!(
                    out,
                    "    body.{parent} = Some({wrapper} {{ value: {axum} }});",
                    parent = parent_field,
                    axum = param.axum_name,
                );
            }
            ParamAssignment::StringField { field_name }
            | ParamAssignment::TypedField { field_name, .. } => {
                let _ = writeln!(
                    out,
                    "    body.{field} = {axum};",
                    field = field_name,
                    axum = param.axum_name,
                );
            }
            ParamAssignment::EnumField {
                field_name,
                enum_rust_type,
            } => {
                let _ = write!(
                    out,
                    "    body.{field} = match {enum_type}::from_str_name(&{axum}.to_ascii_uppercase()) {{
        Some(v) => v as i32,
        None => {axum}.parse::<i32>().map_err(|_| {rt}::RestError::new(tonic::Status::invalid_argument(
            \"invalid enum value for '{field}'\",
        )))?,
    }};\n",
                    field = field_name,
                    axum = param.axum_name,
                    enum_type = enum_rust_type,
                );
            }
        }
    }

    out
}

fn generate_all_routes(code: &mut String, services: &[ServiceRoute], config: &RestCodegenConfig) {
    // Collect public REST paths from config-specified method names
    let mut public_paths = Vec::new();
    for service in services {
        for method in &service.methods {
            if config.public_methods.contains(method.proto_name.as_str()) {
                public_paths.push(method.path.clone());
            }
        }
    }

    // Emit public REST paths constant
    code.push_str(
        "\n\
// =============================================================================
// Public REST paths (bypass auth middleware)
// =============================================================================

/// REST paths that are marked as public (no authentication required).
///
/// Auto-generated from `google.api.http` annotations on public RPC methods.
/// Used by middleware to identify unauthenticated endpoints.
pub const PUBLIC_REST_PATHS: &[&str] = &[\n",
    );
    for path in &public_paths {
        let _ = writeln!(code, "    \"{path}\",");
    }
    code.push_str("];\n");

    // Emit combined router
    if services.is_empty() {
        // No services — emit a trivial router that compiles cleanly
        code.push_str(
            "\n\
// =============================================================================
// Combined REST router
// =============================================================================

/// Build a combined Axum router with REST routes for all proto services.
///
/// No services with `google.api.http` annotations were found.
pub fn all_rest_routes() -> Router {
    Router::new()
}
",
        );
        return;
    }

    let mut type_params = Vec::new();
    let mut bounds = Vec::new();
    let mut fn_params = Vec::new();
    let mut router_merges = Vec::new();

    for (i, service) in services.iter().enumerate() {
        let type_name = format!("S{i}");
        let svc_snake = super::to_snake_case(&service.service_name);
        let trait_path = format!(
            "{}::{}::{}_server::{}",
            config.proto_root, service.package_mod, svc_snake, service.service_name
        );

        type_params.push(type_name.clone());
        bounds.push(format!(
            "    {type_name}: {trait_path} + Send + Sync + 'static,"
        ));
        fn_params.push(format!("{svc_snake}: Arc<{type_name}>"));
        router_merges.push(format!(
            "        .merge({svc_snake}_rest_router({svc_snake}))"
        ));
    }

    let _ = write!(
        code,
        "\n\
// =============================================================================
// Combined REST router
// =============================================================================

/// Build a combined Axum router with REST routes for all proto services.
///
/// Each service is generic — pass your concrete implementations as `Arc<T>`.
pub fn all_rest_routes<{type_params}>(
    {fn_params},
) -> Router
where
{bounds}
{{
    Router::new()
{merges}
}}
",
        type_params = type_params.join(", "),
        fn_params = fn_params.join(",\n    "),
        bounds = bounds.join("\n"),
        merges = router_merges.join("\n"),
    );
}

/// Emit a `use` statement with `{}` braces only when there are multiple items.
fn write_use_stmt(code: &mut String, module: &str, items: &[&str]) {
    match items {
        [] => {}
        [single] => {
            let _ = writeln!(code, "use {module}::{single};");
        }
        many => {
            let _ = writeln!(code, "use {module}::{{{}}};", many.join(", "));
        }
    }
}
