use pylon_kernel::AppManifest;

/// Map a manifest field type to a Swift type.
fn swift_type(field_type: &str) -> &str {
    match field_type {
        "string" => "String",
        "int" => "Int64",
        "float" => "Double",
        "bool" => "Bool",
        "datetime" => "String",
        "richtext" => "String",
        _ if field_type.starts_with("id(") => "String",
        _ => "JSONValue",
    }
}

/// Generate a Swift bindings file from a manifest. Produces:
///
/// - `Codable` structs for each entity
/// - `Codable` structs for each action input
/// - A `PylonClient` extension with typed `list` / `create` / `update` /
///   `delete` helpers per entity, plus typed `call<ActionName>` helpers.
///
/// The output assumes `import PylonClient` is in scope. Apps drop this
/// file into their SPM package or Xcode target.
pub fn generate_client_swift(manifest: &AppManifest) -> String {
    let mut out = String::new();

    out.push_str("// Generated by `pylon codegen client --target swift`. Do not edit.\n");
    out.push_str("// Regenerate after every manifest change.\n\n");
    out.push_str("import Foundation\n");
    out.push_str("import PylonClient\n\n");

    // ---- Entities ----------------------------------------------------------
    if !manifest.entities.is_empty() {
        out.push_str("// MARK: - Entities\n\n");
    }
    for entity in &manifest.entities {
        out.push_str(&format!(
            "public struct {}: Sendable, Codable, Identifiable, Equatable, Hashable {{\n",
            entity.name
        ));
        out.push_str("    public var id: String\n");
        for field in &entity.fields {
            let ty = swift_type(&field.field_type);
            let opt = if field.optional { "?" } else { "" };
            out.push_str(&format!("    public var {}: {}{}\n", field.name, ty, opt));
        }
        // Memberwise init.
        out.push_str("\n    public init(id: String");
        for field in &entity.fields {
            let ty = swift_type(&field.field_type);
            let opt = if field.optional { "? = nil" } else { "" };
            out.push_str(&format!(", {}: {}{}", field.name, ty, opt));
        }
        out.push_str(") {\n");
        out.push_str("        self.id = id\n");
        for field in &entity.fields {
            out.push_str(&format!("        self.{} = {0}\n", field.name));
        }
        out.push_str("    }\n");
        out.push_str("}\n\n");

        // Insert payload (id-less variant for create calls).
        if !entity.fields.is_empty() {
            out.push_str(&format!(
                "public struct New{}: Sendable, Encodable {{\n",
                entity.name
            ));
            for field in &entity.fields {
                let ty = swift_type(&field.field_type);
                let opt = if field.optional { "?" } else { "" };
                out.push_str(&format!("    public var {}: {}{}\n", field.name, ty, opt));
            }
            out.push_str("\n    public init(");
            let inits: Vec<String> = entity
                .fields
                .iter()
                .map(|f| {
                    let ty = swift_type(&f.field_type);
                    let opt = if f.optional { "? = nil" } else { "" };
                    format!("{}: {}{}", f.name, ty, opt)
                })
                .collect();
            out.push_str(&inits.join(", "));
            out.push_str(") {\n");
            for field in &entity.fields {
                out.push_str(&format!("        self.{} = {0}\n", field.name));
            }
            out.push_str("    }\n}\n\n");
        }
    }

    // ---- Action inputs -----------------------------------------------------
    if !manifest.actions.is_empty() {
        out.push_str("// MARK: - Action inputs\n\n");
    }
    for action in &manifest.actions {
        if action.input.is_empty() {
            continue;
        }
        out.push_str(&format!(
            "public struct {}Input: Sendable, Encodable {{\n",
            capitalize(&action.name)
        ));
        for field in &action.input {
            let ty = swift_type(&field.field_type);
            let opt = if field.optional { "?" } else { "" };
            out.push_str(&format!("    public var {}: {}{}\n", field.name, ty, opt));
        }
        out.push_str("\n    public init(");
        let inits: Vec<String> = action
            .input
            .iter()
            .map(|f| {
                let ty = swift_type(&f.field_type);
                let opt = if f.optional { "? = nil" } else { "" };
                format!("{}: {}{}", f.name, ty, opt)
            })
            .collect();
        out.push_str(&inits.join(", "));
        out.push_str(") {\n");
        for field in &action.input {
            out.push_str(&format!("        self.{} = {0}\n", field.name));
        }
        out.push_str("    }\n}\n\n");
    }

    // ---- Query inputs ------------------------------------------------------
    if !manifest.queries.is_empty() {
        out.push_str("// MARK: - Query inputs\n\n");
    }
    for query in &manifest.queries {
        if query.input.is_empty() {
            continue;
        }
        out.push_str(&format!(
            "public struct {}Input: Sendable, Encodable {{\n",
            capitalize(&query.name)
        ));
        for field in &query.input {
            let ty = swift_type(&field.field_type);
            let opt = if field.optional { "?" } else { "" };
            out.push_str(&format!("    public var {}: {}{}\n", field.name, ty, opt));
        }
        out.push_str("\n    public init(");
        let inits: Vec<String> = query
            .input
            .iter()
            .map(|f| {
                let ty = swift_type(&f.field_type);
                let opt = if f.optional { "? = nil" } else { "" };
                format!("{}: {}{}", f.name, ty, opt)
            })
            .collect();
        out.push_str(&inits.join(", "));
        out.push_str(") {\n");
        for field in &query.input {
            out.push_str(&format!("        self.{} = {0}\n", field.name));
        }
        out.push_str("    }\n}\n\n");
    }

    // ---- Typed PylonClient extension --------------------------------------
    out.push_str("// MARK: - Typed PylonClient helpers\n\n");
    out.push_str("public extension PylonClient {\n");
    for entity in &manifest.entities {
        let name = &entity.name;
        let lower = lowercase_first(name);
        let new_type = if entity.fields.is_empty() {
            "[String: JSONValue]".to_string()
        } else {
            format!("New{}", name)
        };
        out.push_str(&format!(
            "    func list{name}s() async throws -> [{name}] {{ try await list(\"{name}\") }}\n"
        ));
        out.push_str(&format!(
            "    func get{name}(id: String) async throws -> {name} {{ try await get(\"{name}\", id: id) }}\n"
        ));
        out.push_str(&format!(
            "    func create{name}(_ {lower}: {new_type}) async throws -> {name} {{ try await create(\"{name}\", {lower}) }}\n"
        ));
        out.push_str(&format!(
            "    func update{name}(id: String, _ patch: [String: JSONValue]) async throws -> {name} {{ try await update(\"{name}\", id: id, patch) }}\n"
        ));
        out.push_str(&format!(
            "    func delete{name}(id: String) async throws {{ try await delete(\"{name}\", id: id) }}\n\n"
        ));
    }

    for action in &manifest.actions {
        let cap = capitalize(&action.name);
        let input_type = if action.input.is_empty() {
            "[String: JSONValue]".to_string()
        } else {
            format!("{cap}Input")
        };
        out.push_str(&format!(
            "    /// Call the `{name}` server action.\n",
            name = action.name
        ));
        out.push_str(&format!(
            "    func {name}<Result: Decodable>(_ input: {input_type}, as type: Result.Type = Result.self) async throws -> Result {{\n",
            name = action.name,
            input_type = input_type
        ));
        out.push_str(&format!(
            "        try await callFn(\"{}\", args: input)\n",
            action.name
        ));
        out.push_str("    }\n");
    }
    out.push_str("}\n\n");

    // ---- Entity name registry (handy for SyncEngine consumers) -------------
    out.push_str("// MARK: - Registry\n\n");
    out.push_str("public enum PylonEntities {\n");
    for entity in &manifest.entities {
        out.push_str(&format!(
            "    public static let {name} = \"{name}\"\n",
            name = entity.name
        ));
    }
    let names: Vec<String> = manifest
        .entities
        .iter()
        .map(|e| format!("\"{}\"", e.name))
        .collect();
    out.push_str(&format!(
        "    public static let all: [String] = [{}]\n",
        names.join(", ")
    ));
    out.push_str("}\n");

    out
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

fn lowercase_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pylon_kernel::{
        ManifestAction, ManifestEntity, ManifestField, ManifestIndex, ManifestQuery,
    };

    fn manifest() -> AppManifest {
        fn f(name: &str, ty: &str, optional: bool) -> ManifestField {
            ManifestField {
                name: name.into(),
                field_type: ty.into(),
                optional,
                unique: false,
                crdt: None,
            }
        }
        AppManifest {
            manifest_version: 1,
            name: "todo-app".into(),
            version: "0.1.0".into(),
            routes: vec![],
            policies: vec![],
            entities: vec![
                ManifestEntity {
                    name: "User".into(),
                    fields: vec![f("email", "string", false)],
                    indexes: vec![],
                    relations: vec![],
                    search: None,
                    crdt: false,
                },
                ManifestEntity {
                    name: "Todo".into(),
                    fields: vec![
                        f("title", "string", false),
                        f("done", "bool", false),
                        f("authorId", "id(User)", false),
                        f("score", "float", true),
                    ],
                    indexes: vec![ManifestIndex {
                        name: "by_author".into(),
                        fields: vec!["authorId".into()],
                        unique: false,
                    }],
                    relations: vec![],
                    search: None,
                    crdt: false,
                },
            ],
            queries: vec![ManifestQuery {
                name: "todosByAuthor".into(),
                input: vec![f("authorId", "id(User)", false)],
            }],
            actions: vec![ManifestAction {
                name: "createTodo".into(),
                input: vec![f("title", "string", false), f("done", "bool", true)],
            }],
            auth: Default::default(),
        }
    }

    #[test]
    fn emits_entity_struct_with_optional_field() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("public struct Todo: Sendable, Codable, Identifiable"));
        assert!(s.contains("public var title: String"));
        assert!(s.contains("public var done: Bool"));
        assert!(s.contains("public var authorId: String"));
        assert!(s.contains("public var score: Double?"));
    }

    #[test]
    fn emits_new_entity_payload() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("public struct NewTodo: Sendable, Encodable"));
    }

    #[test]
    fn emits_action_input() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("public struct CreateTodoInput: Sendable, Encodable"));
        assert!(s.contains("public var title: String"));
        assert!(s.contains("public var done: Bool?"));
    }

    #[test]
    fn emits_typed_client_helpers() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("func listTodos() async throws -> [Todo]"));
        assert!(s.contains("func createTodo(_ todo: NewTodo)"));
        assert!(s.contains("func deleteTodo(id: String) async throws"));
        assert!(s.contains("/// Call the `createTodo` server action."));
    }

    #[test]
    fn emits_entity_registry() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("public static let User = \"User\""));
        assert!(s.contains("public static let all: [String] = [\"User\", \"Todo\"]"));
    }

    #[test]
    fn emits_query_input() {
        let s = generate_client_swift(&manifest());
        assert!(s.contains("public struct TodosByAuthorInput: Sendable, Encodable"));
        assert!(s.contains("public var authorId: String"));
    }

    #[test]
    fn output_is_deterministic() {
        let m = manifest();
        assert_eq!(generate_client_swift(&m), generate_client_swift(&m));
    }

    #[test]
    fn swift_type_mapping() {
        assert_eq!(swift_type("string"), "String");
        assert_eq!(swift_type("int"), "Int64");
        assert_eq!(swift_type("float"), "Double");
        assert_eq!(swift_type("bool"), "Bool");
        assert_eq!(swift_type("datetime"), "String");
        assert_eq!(swift_type("id(User)"), "String");
        assert_eq!(swift_type("unknown_type"), "JSONValue");
    }
}
