//! OpenAPI 3.1 specification auto-generation from DeclarativeModel definitions
//!
//! Generates a complete OpenAPI spec at runtime using `ModelSpec` metadata
//! extracted from registered models. Each model produces CRUD endpoints
//! with proper schemas, types, and permission annotations.

use crate::schema::{FieldConstraints, ModelSpec};
use serde_json::{json, Value};

/// Information about a registered model needed for OpenAPI generation
pub struct OpenApiModelInfo {
    /// Model name (e.g. "Product")
    pub name: String,
    /// Base API path (e.g. "/api/products")
    pub base_path: String,
    /// Schema specification with field constraints and types
    pub spec: ModelSpec,
}

/// Map a Rust type string to OpenAPI type + format
fn rust_type_to_openapi(rust_type: &str) -> (String, Option<String>) {
    // Strip Option<> wrapper
    let inner = rust_type
        .strip_prefix("Option<")
        .and_then(|s| s.strip_suffix('>'))
        .unwrap_or(rust_type);

    match inner {
        "String" | "string" => ("string".into(), None),
        "Uuid" | "uuid::Uuid" => ("string".into(), Some("uuid".into())),
        "i8" | "i16" | "i32" | "u8" | "u16" | "u32" => ("integer".into(), Some("int32".into())),
        "i64" | "u64" | "isize" | "usize" => ("integer".into(), Some("int64".into())),
        "i128" | "u128" => ("integer".into(), Some("int128".into())),
        "f32" => ("number".into(), Some("float".into())),
        "f64" => ("number".into(), Some("double".into())),
        "bool" => ("boolean".into(), None),
        "DateTime"
        | "NaiveDateTime"
        | "chrono::DateTime<Utc>"
        | "chrono::DateTime<chrono::Utc>" => ("string".into(), Some("date-time".into())),
        "NaiveDate" | "chrono::NaiveDate" => ("string".into(), Some("date".into())),
        "Vec<u8>" => ("string".into(), Some("byte".into())),
        _ if inner.starts_with("Vec<") => ("array".into(), None),
        _ if inner.starts_with("HashMap<") => ("object".into(), None),
        _ => ("string".into(), None),
    }
}

/// Extract the inner type of a Vec<T> for OpenAPI items schema
fn vec_items_schema(rust_type: &str) -> Option<Value> {
    let inner = rust_type
        .strip_prefix("Option<")
        .and_then(|s| s.strip_suffix('>'))
        .unwrap_or(rust_type);

    if let Some(item_type) = inner.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
        if item_type == "u8" {
            return None; // Vec<u8> is "string"/"byte", not array
        }
        let (t, f) = rust_type_to_openapi(item_type);
        let mut schema = json!({ "type": t });
        if let Some(fmt) = f {
            schema["format"] = json!(fmt);
        }
        Some(schema)
    } else {
        None
    }
}

/// Build the JSON schema for a single field
fn field_to_schema(field_name: &str, constraints: &FieldConstraints) -> Value {
    let (type_str, format) = constraints
        .field_type
        .as_ref()
        .map(|t| rust_type_to_openapi(t))
        .unwrap_or_else(|| ("string".into(), None));

    let is_nullable = constraints.nullable
        || constraints
            .field_type
            .as_ref()
            .map(|t| t.starts_with("Option<"))
            .unwrap_or(false);

    // OpenAPI 3.1: nullable via type union ["type", "null"]
    let mut schema = if is_nullable {
        json!({ "type": [&type_str, "null"] })
    } else {
        json!({ "type": type_str })
    };

    if let Some(fmt) = format {
        schema["format"] = json!(fmt);
    }

    // Add items schema for array types
    if type_str == "array" {
        if let Some(ref ft) = constraints.field_type {
            if let Some(items) = vec_items_schema(ft) {
                schema["items"] = items;
            }
        }
    }

    // Add validation hints
    for rule in &constraints.validation_rules {
        if let Some(min) = rule.strip_prefix("min_length:") {
            if let Ok(n) = min.parse::<u64>() {
                schema["minLength"] = json!(n);
            }
        } else if let Some(max) = rule.strip_prefix("max_length:") {
            if let Ok(n) = max.parse::<u64>() {
                schema["maxLength"] = json!(n);
            }
        } else if rule == "email" {
            schema["format"] = json!("email");
        } else if rule == "url" || rule == "uri" {
            schema["format"] = json!("uri");
        }
    }

    // Add description for special fields
    if constraints.primary_key {
        schema["description"] = json!(format!("Primary key for {}", field_name));
        schema["readOnly"] = json!(true);
    }

    schema
}

/// Build the component schema for a model
fn model_to_schema(info: &OpenApiModelInfo) -> Value {
    let mut properties = serde_json::Map::new();
    let mut required = Vec::new();

    // Sort fields for deterministic output
    let mut fields: Vec<_> = info.spec.fields.iter().collect();
    fields.sort_by_key(|(name, _)| (*name).clone());

    for (field_name, constraints) in &fields {
        properties.insert(field_name.to_string(), field_to_schema(field_name, constraints));

        // Non-nullable and non-primary-key fields are required for creation
        if !constraints.nullable
            && !constraints.primary_key
            && !constraints
                .field_type
                .as_ref()
                .map(|t| t.starts_with("Option<"))
                .unwrap_or(false)
        {
            required.push(json!(field_name));
        }
    }

    let mut schema = json!({
        "type": "object",
        "properties": properties,
    });

    if !required.is_empty() {
        schema["required"] = json!(required);
    }

    schema
}

/// Derive the OpenAPI schema for the model's primary key (for path params)
fn id_param_schema(info: &OpenApiModelInfo) -> Value {
    let (id_type, id_format) = info
        .spec
        .fields
        .iter()
        .find(|(_, c)| c.primary_key)
        .and_then(|(_, c)| c.field_type.as_deref())
        .map(rust_type_to_openapi)
        .unwrap_or_else(|| ("string".into(), None));

    let mut schema = json!({ "type": id_type });
    if let Some(fmt) = id_format {
        schema["format"] = json!(fmt);
    }
    schema
}

/// Build CRUD path operations for a model
fn model_to_paths(info: &OpenApiModelInfo) -> Vec<(String, Value)> {
    let model_name = &info.name;
    let base = &info.base_path;
    let schema_ref = format!("#/components/schemas/{}", model_name);
    let tag = model_name.clone();

    // Deterministic read permission: sort and take first
    let read_perm: Option<String> = {
        let mut perms: Vec<String> = info
            .spec
            .fields
            .values()
            .filter_map(|c| c.permissions.read_permission.clone())
            .collect();
        perms.sort();
        perms.into_iter().next()
    };

    let perm_note = read_perm
        .as_ref()
        .map(|p| format!(" Requires `{}` permission.", p))
        .unwrap_or_default();

    let id_schema = id_param_schema(info);
    let mut paths = Vec::new();

    // Collection endpoint: GET (list) + POST (create)
    let collection_path = base.clone();
    paths.push((
        collection_path,
        json!({
            "get": {
                "tags": [&tag],
                "summary": format!("List all {}", model_name),
                "description": format!("Returns all {} items.{}", model_name, perm_note),
                "operationId": format!("list{}", model_name),
                "parameters": [
                    { "name": "skip", "in": "query", "schema": { "type": "integer", "default": 0 }, "description": "Number of items to skip" },
                    { "name": "take", "in": "query", "schema": { "type": "integer" }, "description": "Maximum number of items to return" },
                    { "name": "sort", "in": "query", "schema": { "type": "string" }, "description": "Sort field (prefix with - for descending)" }
                ],
                "responses": {
                    "200": {
                        "description": "Paginated list",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": { "type": "array", "items": { "$ref": &schema_ref } },
                                        "total": { "type": "integer" },
                                        "skip": { "type": "integer" },
                                        "take": { "type": ["integer", "null"] },
                                        "has_more": { "type": "boolean" }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "tags": [&tag],
                "summary": format!("Create a new {}", model_name),
                "operationId": format!("create{}", model_name),
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": { "$ref": &schema_ref }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Created",
                        "content": {
                            "application/json": {
                                "schema": { "$ref": &schema_ref }
                            }
                        }
                    },
                    "400": { "description": "Invalid input" },
                    "403": { "description": "Insufficient permissions" },
                    "409": { "description": "Unique constraint violation" }
                }
            }
        }),
    ));

    // Item endpoint: GET + PUT + PATCH + DELETE
    let item_path = format!("{}/{{id}}", base);
    paths.push((
        item_path,
        json!({
            "get": {
                "tags": [&tag],
                "summary": format!("Get {} by ID", model_name),
                "operationId": format!("get{}", model_name),
                "parameters": [
                    { "name": "id", "in": "path", "required": true, "schema": id_schema.clone() }
                ],
                "responses": {
                    "200": {
                        "description": "Found",
                        "content": {
                            "application/json": {
                                "schema": { "$ref": &schema_ref }
                            }
                        }
                    },
                    "404": { "description": "Not found" }
                }
            },
            "put": {
                "tags": [&tag],
                "summary": format!("Update {}", model_name),
                "operationId": format!("update{}", model_name),
                "parameters": [
                    { "name": "id", "in": "path", "required": true, "schema": id_schema.clone() }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": { "$ref": &schema_ref }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Updated",
                        "content": {
                            "application/json": {
                                "schema": { "$ref": &schema_ref }
                            }
                        }
                    },
                    "400": { "description": "Invalid input or immutable field modified" },
                    "404": { "description": "Not found" },
                    "409": { "description": "Unique constraint violation" }
                }
            },
            "patch": {
                "tags": [&tag],
                "summary": format!("Partially update {}", model_name),
                "description": format!("Merge-patch: only provided fields are updated. Immutable fields cannot be changed."),
                "operationId": format!("patch{}", model_name),
                "parameters": [
                    { "name": "id", "in": "path", "required": true, "schema": id_schema.clone() }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": { "$ref": &schema_ref }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Patched",
                        "content": {
                            "application/json": {
                                "schema": { "$ref": &schema_ref }
                            }
                        }
                    },
                    "400": { "description": "Invalid input or immutable field modified" },
                    "404": { "description": "Not found" },
                    "409": { "description": "Unique constraint violation" }
                }
            },
            "delete": {
                "tags": [&tag],
                "summary": format!("Delete {}", model_name),
                "operationId": format!("delete{}", model_name),
                "parameters": [
                    { "name": "id", "in": "path", "required": true, "schema": id_schema.clone() }
                ],
                "responses": {
                    "204": { "description": "Deleted" },
                    "404": { "description": "Not found" }
                }
            }
        }),
    ));

    paths
}

/// Generate a complete OpenAPI 3.1 specification from registered models
pub fn generate_openapi_spec(models: &[OpenApiModelInfo]) -> Value {
    let mut paths = serde_json::Map::new();
    let mut schemas = serde_json::Map::new();

    for model in models {
        // Generate schema component
        schemas.insert(model.name.clone(), model_to_schema(model));

        // Generate path operations
        for (path, operations) in model_to_paths(model) {
            paths.insert(path, operations);
        }
    }

    json!({
        "openapi": "3.1.0",
        "info": {
            "title": "Lithair API",
            "description": "Auto-generated API documentation from DeclarativeModel definitions",
            "version": env!("CARGO_PKG_VERSION")
        },
        "paths": paths,
        "components": {
            "schemas": schemas
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::{FieldPermissions, ModelSpec};
    use std::collections::HashMap;

    fn sample_model() -> OpenApiModelInfo {
        let mut fields = HashMap::new();
        fields.insert(
            "id".to_string(),
            FieldConstraints {
                primary_key: true,
                unique: true,
                indexed: true,
                foreign_key: None,
                nullable: false,
                immutable: true,
                audited: false,
                versioned: 0,
                retention: 0,
                snapshot_only: false,
                pinned: false,
                validation_rules: vec![],
                permissions: FieldPermissions {
                    read_permission: Some("Public".to_string()),
                    write_permission: None,
                    owner_field: false,
                },
                default_value: None,
                field_type: Some("Uuid".to_string()),
            },
        );
        fields.insert(
            "title".to_string(),
            FieldConstraints {
                primary_key: false,
                unique: false,
                indexed: false,
                foreign_key: None,
                nullable: false,
                immutable: false,
                audited: false,
                versioned: 0,
                retention: 0,
                snapshot_only: false,
                pinned: false,
                validation_rules: vec!["min_length:1".to_string()],
                permissions: FieldPermissions {
                    read_permission: Some("Public".to_string()),
                    write_permission: None,
                    owner_field: false,
                },
                default_value: None,
                field_type: Some("String".to_string()),
            },
        );
        fields.insert(
            "done".to_string(),
            FieldConstraints {
                primary_key: false,
                unique: false,
                indexed: false,
                foreign_key: None,
                nullable: false,
                immutable: false,
                audited: false,
                versioned: 0,
                retention: 0,
                snapshot_only: false,
                pinned: false,
                validation_rules: vec![],
                permissions: FieldPermissions {
                    read_permission: None,
                    write_permission: None,
                    owner_field: false,
                },
                default_value: None,
                field_type: Some("bool".to_string()),
            },
        );

        OpenApiModelInfo {
            name: "Todo".to_string(),
            base_path: "/api/todos".to_string(),
            spec: ModelSpec {
                model_name: "Todo".to_string(),
                version: 1,
                fields,
                indexes: vec![],
                foreign_keys: vec![],
            },
        }
    }

    #[test]
    fn test_generate_openapi_spec() {
        let spec = generate_openapi_spec(&[sample_model()]);
        assert_eq!(spec["openapi"], "3.1.0");
        assert!(spec["paths"]["/api/todos"]["get"].is_object());
        assert!(spec["paths"]["/api/todos"]["post"].is_object());
        assert!(spec["paths"]["/api/todos/{id}"]["get"].is_object());
        assert!(spec["paths"]["/api/todos/{id}"]["put"].is_object());
        assert!(spec["paths"]["/api/todos/{id}"]["delete"].is_object());
        assert!(spec["components"]["schemas"]["Todo"].is_object());
    }

    #[test]
    fn test_rust_type_mapping() {
        assert_eq!(rust_type_to_openapi("String"), ("string".into(), None));
        assert_eq!(rust_type_to_openapi("Uuid"), ("string".into(), Some("uuid".into())));
        assert_eq!(rust_type_to_openapi("i64"), ("integer".into(), Some("int64".into())));
        assert_eq!(rust_type_to_openapi("bool"), ("boolean".into(), None));
        assert_eq!(rust_type_to_openapi("Option<String>"), ("string".into(), None));
        assert_eq!(rust_type_to_openapi("f64"), ("number".into(), Some("double".into())));
    }

    #[test]
    fn test_field_schema_nullable() {
        let constraints = FieldConstraints {
            primary_key: false,
            unique: false,
            indexed: false,
            foreign_key: None,
            nullable: true,
            immutable: false,
            audited: false,
            versioned: 0,
            retention: 0,
            snapshot_only: false,
            pinned: false,
            validation_rules: vec![],
            permissions: FieldPermissions {
                read_permission: None,
                write_permission: None,
                owner_field: false,
            },
            default_value: None,
            field_type: Some("Option<String>".to_string()),
        };

        let schema = field_to_schema("notes", &constraints);
        // OpenAPI 3.1: type union instead of nullable
        assert_eq!(schema["type"], json!(["string", "null"]));
    }

    #[test]
    fn test_id_param_uses_model_pk_type() {
        let model = sample_model();
        let id_schema = id_param_schema(&model);
        // The sample model has Uuid PK
        assert_eq!(id_schema["type"], "string");
        assert_eq!(id_schema["format"], "uuid");
    }

    #[test]
    fn test_vec_items_schema() {
        let items = vec_items_schema("Vec<String>");
        assert_eq!(items, Some(json!({ "type": "string" })));

        let items_i64 = vec_items_schema("Vec<i64>");
        assert_eq!(items_i64, Some(json!({ "type": "integer", "format": "int64" })));

        // Vec<u8> is byte string, no items
        assert_eq!(vec_items_schema("Vec<u8>"), None);
    }
}
