//! OpenAPI spec generation from a [`ServiceManifest`].
//!
//! This module is used by both the platform (server-side) and the CLI
//! (local generation without deployment).

use crate::{ColumnType, ServiceManifest, TableDefinition};
use serde_json::{json, Map, Value};

/// Generate a complete OpenAPI 3.1.0 specification from a service manifest.
///
/// `tenant_slug` is used to build the URL paths (`/svc/{tenant}/{service}/{table}`).
/// For local generation (no deployment), pass any placeholder like `"local"`.
pub fn generate_openapi(manifest: &ServiceManifest, tenant_slug: &str) -> Value {
    let mut paths = Map::new();
    let mut schemas = Map::new();

    for table in &manifest.tables {
        let base_path = format!("/svc/{}/{}/{}", tenant_slug, manifest.name, table.name);
        let item_path = format!("{}/:id", base_path);
        let schema_name = to_pascal_case(&table.name);
        let create_schema_name = format!("{}Create", schema_name);

        // Generate schema for the table
        schemas.insert(schema_name.clone(), table_to_schema(table));
        schemas.insert(create_schema_name.clone(), table_to_create_schema(table));

        // List + Create endpoint
        paths.insert(
            base_path.clone(),
            json!({
                "get": {
                    "summary": format!("List {}", table.name),
                    "operationId": format!("list_{}", table.name),
                    "tags": [table.name],
                    "parameters": list_parameters(table),
                    "responses": {
                        "200": {
                            "description": "Paginated list",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "results": {
                                                "type": "array",
                                                "items": { "$ref": format!("#/components/schemas/{}", schema_name) }
                                            },
                                            "pagination": { "$ref": "#/components/schemas/Pagination" }
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "post": {
                    "summary": format!("Create {}", singular(&table.name)),
                    "operationId": format!("create_{}", singular(&table.name)),
                    "tags": [table.name],
                    "requestBody": {
                        "required": true,
                        "content": {
                            "application/json": {
                                "schema": { "$ref": format!("#/components/schemas/{}", create_schema_name) }
                            }
                        }
                    },
                    "responses": {
                        "200": {
                            "description": "Created",
                            "content": {
                                "application/json": {
                                    "schema": { "$ref": format!("#/components/schemas/{}", schema_name) }
                                }
                            }
                        }
                    }
                }
            }),
        );

        // Get, Update, Delete endpoint
        paths.insert(
            item_path,
            json!({
                "get": {
                    "summary": format!("Get {} by ID", singular(&table.name)),
                    "operationId": format!("get_{}", singular(&table.name)),
                    "tags": [table.name],
                    "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
                    "responses": {
                        "200": {
                            "description": "Found",
                            "content": {
                                "application/json": {
                                    "schema": { "$ref": format!("#/components/schemas/{}", schema_name) }
                                }
                            }
                        },
                        "404": { "description": "Not found" }
                    }
                },
                "put": {
                    "summary": format!("Update {}", singular(&table.name)),
                    "operationId": format!("update_{}", singular(&table.name)),
                    "tags": [table.name],
                    "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
                    "requestBody": {
                        "required": true,
                        "content": {
                            "application/json": {
                                "schema": { "$ref": format!("#/components/schemas/{}", create_schema_name) }
                            }
                        }
                    },
                    "responses": {
                        "200": {
                            "description": "Updated",
                            "content": {
                                "application/json": {
                                    "schema": { "$ref": format!("#/components/schemas/{}", schema_name) }
                                }
                            }
                        }
                    }
                },
                "delete": {
                    "summary": format!("Delete {}", singular(&table.name)),
                    "operationId": format!("delete_{}", singular(&table.name)),
                    "tags": [table.name],
                    "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}}],
                    "responses": {
                        "200": { "description": "Deleted" }
                    }
                }
            }),
        );
    }

    // Add custom WASM handler routes
    for route in &manifest.custom_routes {
        let full_path = format!(
            "/svc/{}/{}/_fn/{}",
            tenant_slug, manifest.name, route.handler
        );
        let method = route.method.to_lowercase();
        let operation_id = route.handler.clone();

        // Extract path parameters from :param segments
        let mut parameters = Vec::new();
        for segment in route.path.split('/') {
            if let Some(param) = segment.strip_prefix(':') {
                parameters.push(json!({
                    "name": param,
                    "in": "path",
                    "required": true,
                    "schema": { "type": "string" }
                }));
            }
        }

        let mut operation = json!({
            "summary": format!("Custom: {}", route.handler),
            "operationId": operation_id,
            "tags": ["custom"],
            "parameters": parameters,
            "responses": {
                "200": {
                    "description": "Success",
                    "content": {
                        "application/json": {
                            "schema": { "type": "object" }
                        }
                    }
                }
            }
        });

        // Add request body for POST/PUT/PATCH methods
        if matches!(method.as_str(), "post" | "put" | "patch") {
            operation["requestBody"] = json!({
                "required": false,
                "content": {
                    "application/json": {
                        "schema": { "type": "object" }
                    }
                }
            });
        }

        let path_entry = paths.entry(full_path).or_insert_with(|| json!({}));
        if let Some(obj) = path_entry.as_object_mut() {
            obj.insert(method, operation);
        }
    }

    // Add Pagination schema
    schemas.insert(
        "Pagination".into(),
        json!({
            "type": "object",
            "properties": {
                "current_page": { "type": "integer" },
                "per_page": { "type": "integer" },
                "total_records": { "type": "integer" },
                "total_pages": { "type": "integer" }
            }
        }),
    );

    json!({
        "openapi": "3.1.0",
        "info": {
            "title": manifest.name,
            "version": manifest.version.as_deref().unwrap_or("0.1.0")
        },
        "paths": Value::Object(paths),
        "components": {
            "schemas": Value::Object(schemas)
        }
    })
}

fn column_to_openapi_type(col_type: &ColumnType) -> Value {
    match col_type {
        ColumnType::Uuid => json!({"type": "string", "format": "uuid"}),
        ColumnType::Text => json!({"type": "string"}),
        ColumnType::Integer => json!({"type": "integer", "format": "int32"}),
        ColumnType::BigInteger => json!({"type": "integer", "format": "int64"}),
        ColumnType::Float => json!({"type": "number", "format": "float"}),
        ColumnType::Double => json!({"type": "number", "format": "double"}),
        ColumnType::Boolean => json!({"type": "boolean"}),
        ColumnType::Timestamp => json!({"type": "string", "format": "date-time"}),
        ColumnType::Date => json!({"type": "string", "format": "date"}),
        ColumnType::Jsonb => json!({"type": "object"}),
    }
}

fn table_to_schema(table: &TableDefinition) -> Value {
    let mut properties = Map::new();
    let mut required = Vec::new();

    for col in &table.columns {
        let mut prop = column_to_openapi_type(&col.column_type);
        if col.nullable {
            if let Some(obj) = prop.as_object_mut() {
                obj.insert("nullable".into(), json!(true));
            }
        }
        properties.insert(col.name.clone(), prop);
        if !col.nullable {
            required.push(json!(col.name));
        }
    }

    json!({
        "type": "object",
        "properties": Value::Object(properties),
        "required": required
    })
}

fn table_to_create_schema(table: &TableDefinition) -> Value {
    let mut properties = Map::new();
    let mut required = Vec::new();

    for col in &table.columns {
        if col.auto_generate {
            continue;
        }
        let mut prop = column_to_openapi_type(&col.column_type);
        if col.nullable {
            if let Some(obj) = prop.as_object_mut() {
                obj.insert("nullable".into(), json!(true));
            }
        }
        properties.insert(col.name.clone(), prop);
        if !col.nullable && col.default_value.is_none() {
            required.push(json!(col.name));
        }
    }

    json!({
        "type": "object",
        "properties": Value::Object(properties),
        "required": required
    })
}

fn list_parameters(table: &TableDefinition) -> Vec<Value> {
    let mut params = vec![
        json!({"name": "page", "in": "query", "schema": {"type": "integer"}, "description": "Page number (default: 1)"}),
        json!({"name": "per_page", "in": "query", "schema": {"type": "integer"}, "description": "Items per page (default: 50, max: 200)"}),
        json!({"name": "sort", "in": "query", "schema": {"type": "string"}, "description": "Sort by column (prefix - for DESC)"}),
        json!({"name": "filter", "in": "query", "schema": {"type": "string"}, "description": "JSON filter array"}),
    ];

    // Add query params for each filterable column
    for col in &table.columns {
        params.push(json!({
            "name": col.name,
            "in": "query",
            "schema": column_to_openapi_type(&col.column_type),
            "description": format!("Filter by {}", col.name)
        }));
    }

    params
}

fn to_pascal_case(s: &str) -> String {
    s.split('_')
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
                None => String::new(),
            }
        })
        .collect()
}

fn singular(s: &str) -> String {
    if s.ends_with('s') && s.len() > 1 {
        s[..s.len() - 1].to_string()
    } else {
        s.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        ColumnDefinition, ColumnType, CustomRouteDefinition, ServiceManifest, ServiceMode,
        TableDefinition,
    };

    fn test_manifest() -> ServiceManifest {
        ServiceManifest {
            name: "test-service".into(),
            version: Some("1.0.0".into()),
            tables: vec![TableDefinition {
                name: "todos".into(),
                columns: vec![
                    ColumnDefinition {
                        name: "id".into(),
                        column_type: ColumnType::Uuid,
                        primary_key: true,
                        nullable: false,
                        auto_generate: true,
                        default_value: None,
                        references: None,
                        on_delete: None,
                        unique: false,
                        validations: vec![],
                    },
                    ColumnDefinition {
                        name: "title".into(),
                        column_type: ColumnType::Text,
                        primary_key: false,
                        nullable: false,
                        auto_generate: false,
                        default_value: None,
                        references: None,
                        on_delete: None,
                        unique: false,
                        validations: vec![],
                    },
                    ColumnDefinition {
                        name: "done".into(),
                        column_type: ColumnType::Boolean,
                        primary_key: false,
                        nullable: false,
                        auto_generate: false,
                        default_value: Some("false".into()),
                        references: None,
                        on_delete: None,
                        unique: false,
                        validations: vec![],
                    },
                ],
                indexes: vec![],
                soft_delete: false,
                owner_field: None,
                auth_required: false,
                permission_area: None,
                owner_self_service: false,
                hooks: None,
            }],
            cells: vec![],
            events: vec![],
            subscriptions: vec![],
            custom_routes: vec![],
            mode: ServiceMode::Crud,
            authorization: None,
            on_migrate: None,
        }
    }

    #[test]
    fn test_generates_valid_openapi() {
        let manifest = test_manifest();
        let spec = generate_openapi(&manifest, "test-tenant");

        assert_eq!(spec["openapi"], "3.1.0");
        assert_eq!(spec["info"]["title"], "test-service");
        assert_eq!(spec["info"]["version"], "1.0.0");
    }

    #[test]
    fn test_generates_crud_paths() {
        let manifest = test_manifest();
        let spec = generate_openapi(&manifest, "acme");

        let paths = spec["paths"].as_object().unwrap();
        assert!(paths.contains_key("/svc/acme/test-service/todos"));
        assert!(paths.contains_key("/svc/acme/test-service/todos/:id"));
    }

    #[test]
    fn test_generates_schemas() {
        let manifest = test_manifest();
        let spec = generate_openapi(&manifest, "acme");

        let schemas = spec["components"]["schemas"].as_object().unwrap();
        assert!(schemas.contains_key("Todos"));
        assert!(schemas.contains_key("TodosCreate"));
        assert!(schemas.contains_key("Pagination"));
    }

    #[test]
    fn test_create_schema_excludes_auto_generated() {
        let manifest = test_manifest();
        let spec = generate_openapi(&manifest, "acme");

        let create_props = spec["components"]["schemas"]["TodosCreate"]["properties"]
            .as_object()
            .unwrap();
        // "id" is auto_generate=true, should be excluded from create schema
        assert!(!create_props.contains_key("id"));
        assert!(create_props.contains_key("title"));
    }

    #[test]
    fn test_custom_routes() {
        let mut manifest = test_manifest();
        manifest.custom_routes.push(CustomRouteDefinition {
            method: "POST".into(),
            path: "/complete/:id".into(),
            handler: "complete_todo".into(),
            job: None,
        });

        let spec = generate_openapi(&manifest, "acme");
        let paths = spec["paths"].as_object().unwrap();
        assert!(paths.contains_key("/svc/acme/test-service/_fn/complete_todo"));
    }

    #[test]
    fn test_default_version() {
        let mut manifest = test_manifest();
        manifest.version = None;
        let spec = generate_openapi(&manifest, "acme");
        assert_eq!(spec["info"]["version"], "0.1.0");
    }

    #[test]
    fn test_local_slug() {
        let manifest = test_manifest();
        let spec = generate_openapi(&manifest, "local");

        let paths = spec["paths"].as_object().unwrap();
        assert!(paths.contains_key("/svc/local/test-service/todos"));
    }
}
