//! Protocol Buffers (proto3) output backend.
//!
//! Emits a `.proto` file (syntax = "proto3") from the IR.
//!
//! Mapping:
//! - Structs → `message Foo { … }`
//! - String-literal enums → `enum Status { STATUS_UNSPECIFIED = 0; STATUS_ACTIVE = 1; … }`
//! - Int-literal enums → `enum Status { … }` using the declared integer values
//!   (a `0`-valued entry is inserted if none exists, per proto3 requirements)
//! - Tagged unions → a `message` with `oneof` containing one message per variant
//! - Type aliases → a comment; proto3 has no native type aliases
//!
//! Field numbers are assigned sequentially starting from 1 within each message.
//!
//! `optional` fields use the `optional` keyword (proto3 explicit presence, proto 3.15+).
//! `repeated` is used for array types.

use crate::ir::{EnumKind, Field, Schema, Type, TypeDef, TypeDefKind};
use crate::traits::{Backend, BackendCategory};

/// Generate proto3 source from an IR schema.
pub fn generate_proto(schema: &Schema, package: Option<&str>) -> String {
    let mut out = String::new();
    out.push_str("// Auto-generated by normalize-typegen\n");
    out.push_str("syntax = \"proto3\";\n\n");

    if let Some(pkg) = package {
        out.push_str("package ");
        out.push_str(pkg);
        out.push_str(";\n\n");
    }

    // First pass: emit any nested helper messages for tagged unions.
    let mut helpers: Vec<String> = Vec::new();

    for (i, def) in schema.definitions.iter().enumerate() {
        if i > 0 {
            out.push('\n');
        }
        generate_typedef(&mut out, def, &mut helpers);
    }

    for helper in &helpers {
        out.push('\n');
        out.push_str(helper);
    }

    out
}

fn generate_typedef(out: &mut String, def: &TypeDef, helpers: &mut Vec<String>) {
    if let Some(docs) = &def.docs {
        out.push_str("// ");
        out.push_str(docs);
        out.push('\n');
    }

    match &def.kind {
        TypeDefKind::Struct(s) => {
            out.push_str("message ");
            out.push_str(&def.name);
            out.push_str(" {\n");
            for (idx, field) in s.fields.iter().enumerate() {
                generate_field(out, field, idx + 1, "  ");
            }
            out.push_str("}\n");
        }

        TypeDefKind::Enum(e) => match &e.kind {
            EnumKind::StringLiteral(variants) => {
                let prefix = to_screaming_snake(&def.name);
                out.push_str("enum ");
                out.push_str(&def.name);
                out.push_str(" {\n");
                // proto3 requires first value = 0.
                out.push_str("  ");
                out.push_str(&prefix);
                out.push_str("_UNSPECIFIED = 0;\n");
                for (idx, variant) in variants.iter().enumerate() {
                    if let Some(docs) = &variant.docs {
                        out.push_str("  // ");
                        out.push_str(docs);
                        out.push('\n');
                    }
                    out.push_str("  ");
                    out.push_str(&prefix);
                    out.push('_');
                    out.push_str(&variant.value.to_uppercase().replace('-', "_"));
                    out.push_str(" = ");
                    out.push_str(&(idx + 1).to_string());
                    out.push_str(";\n");
                }
                out.push_str("}\n");
            }
            EnumKind::IntLiteral(variants) => {
                let prefix = to_screaming_snake(&def.name);
                out.push_str("enum ");
                out.push_str(&def.name);
                out.push_str(" {\n");

                // proto3 requires a 0-valued entry.
                let has_zero = variants.iter().any(|v| v.value == 0);
                if !has_zero {
                    out.push_str("  ");
                    out.push_str(&prefix);
                    out.push_str("_UNSPECIFIED = 0;\n");
                }

                for variant in variants {
                    if let Some(docs) = &variant.docs {
                        out.push_str("  // ");
                        out.push_str(docs);
                        out.push('\n');
                    }
                    let name = variant
                        .name
                        .as_deref()
                        .map(|n| format!("{}_{}", prefix, n.to_uppercase()))
                        .unwrap_or_else(|| format!("{}_V{}", prefix, variant.value));
                    out.push_str("  ");
                    out.push_str(&name);
                    out.push_str(" = ");
                    out.push_str(&variant.value.to_string());
                    out.push_str(";\n");
                }
                out.push_str("}\n");
            }
            EnumKind::Tagged(tagged) => {
                // Emit a message with a `oneof` for the discriminated union.
                out.push_str("message ");
                out.push_str(&def.name);
                out.push_str(" {\n");
                out.push_str("  oneof ");
                out.push_str(&tagged.discriminator);
                out.push_str(" {\n");

                for (idx, variant) in tagged.variants.iter().enumerate() {
                    let variant_type_name = format!("{}{}", def.name, to_pascal_case(&variant.tag));
                    let field_name = to_snake_case(&variant.tag);
                    out.push_str("    ");
                    out.push_str(&variant_type_name);
                    out.push(' ');
                    out.push_str(&field_name);
                    out.push_str(" = ");
                    out.push_str(&(idx + 1).to_string());
                    out.push_str(";\n");

                    // Emit helper message for this variant.
                    let mut helper = String::new();
                    if let Some(docs) = &variant.docs {
                        helper.push_str("// ");
                        helper.push_str(docs);
                        helper.push('\n');
                    }
                    helper.push_str("message ");
                    helper.push_str(&variant_type_name);
                    helper.push_str(" {\n");
                    for (fidx, field) in variant.fields.iter().enumerate() {
                        generate_field(&mut helper, field, fidx + 1, "  ");
                    }
                    helper.push_str("}\n");
                    helpers.push(helper);
                }

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

        TypeDefKind::Alias(ty) => {
            // proto3 has no type aliases; emit as a comment.
            out.push_str("// type ");
            out.push_str(&def.name);
            out.push_str(" = ");
            out.push_str(&type_to_proto(ty));
            out.push('\n');
        }
    }
}

fn generate_field(out: &mut String, field: &Field, number: usize, indent: &str) {
    if let Some(docs) = &field.docs {
        out.push_str(indent);
        out.push_str("// ");
        out.push_str(docs);
        out.push('\n');
    }

    let (repeated, inner_ty) = match &field.ty {
        Type::Array(inner) => (true, inner.as_ref()),
        other => (false, other),
    };

    // Unwrap Optional wrapper (we use the `optional` keyword instead).
    let inner_ty = match inner_ty {
        Type::Optional(inner) => inner.as_ref(),
        other => other,
    };

    let proto_ty = type_to_proto(inner_ty);

    out.push_str(indent);

    if repeated {
        out.push_str("repeated ");
    } else if !field.required {
        out.push_str("optional ");
    }

    out.push_str(&proto_ty);
    out.push(' ');
    out.push_str(&to_snake_case(&field.name));
    out.push_str(" = ");
    out.push_str(&number.to_string());
    out.push_str(";\n");
}

fn type_to_proto(ty: &Type) -> String {
    match ty {
        Type::String => "string".to_string(),
        Type::Integer { bits, signed } => match (bits, signed) {
            (32, true) => "int32".to_string(),
            (32, false) => "uint32".to_string(),
            (64, true) => "int64".to_string(),
            (64, false) => "uint64".to_string(),
            (_, true) => "int64".to_string(),
            (_, false) => "uint64".to_string(),
        },
        Type::Float { bits } => {
            if *bits <= 32 {
                "float".to_string()
            } else {
                "double".to_string()
            }
        }
        Type::Boolean => "bool".to_string(),
        Type::Null => "google.protobuf.Empty".to_string(),
        Type::Any => "google.protobuf.Any".to_string(),
        Type::Array(inner) => format!("repeated {}", type_to_proto(inner)),
        Type::Map { key, value } => {
            format!("map<{}, {}>", type_to_proto(key), type_to_proto(value))
        }
        Type::Optional(inner) => type_to_proto(inner), // handled at field level
        Type::Ref(name) => name.clone(),
        Type::Union(_) => "google.protobuf.Any".to_string(), // No native union in proto3
        Type::StringLiteral(_) => "string".to_string(),
        Type::IntLiteral(_) => "int64".to_string(),
        Type::BoolLiteral(_) => "bool".to_string(),
    }
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() {
            if i > 0 {
                result.push('_');
            }
            // normalize-syntax-allow: rust/unwrap-in-impl - to_lowercase() always yields chars
            result.push(c.to_lowercase().next().unwrap());
        } else {
            result.push(c);
        }
    }
    result
}

fn to_screaming_snake(s: &str) -> String {
    to_snake_case(s).to_uppercase()
}

fn to_pascal_case(s: &str) -> String {
    let mut result = String::new();
    let mut cap = true;
    for c in s.chars() {
        if c == '_' || c == '-' || c == '.' {
            cap = true;
        } else if cap {
            // normalize-syntax-allow: rust/unwrap-in-impl - to_uppercase() always yields chars
            result.push(c.to_uppercase().next().unwrap());
            cap = false;
        } else {
            result.push(c);
        }
    }
    result
}

/// Static backend instance.
pub static PROTO_BACKEND: ProtoBackend = ProtoBackend { package: None };

/// Protobuf (proto3) backend.
pub struct ProtoBackend {
    /// Optional proto package name (e.g. `"com.example.types"`).
    pub package: Option<&'static str>,
}

impl ProtoBackend {
    pub fn new(package: Option<&'static str>) -> Self {
        Self { package }
    }
}

impl Backend for ProtoBackend {
    fn name(&self) -> &'static str {
        "proto"
    }

    fn language(&self) -> &'static str {
        "proto"
    }

    fn extension(&self) -> &'static str {
        "proto"
    }

    fn category(&self) -> BackendCategory {
        BackendCategory::Types
    }

    fn generate(&self, schema: &Schema) -> String {
        generate_proto(schema, self.package)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::{Field, TypeDef};

    #[test]
    fn simple_message() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "User",
            vec![
                Field::required("id", Type::String),
                Field::optional("email", Type::String),
            ],
        ));
        let out = generate_proto(&schema, None);
        assert!(out.contains("syntax = \"proto3\""));
        assert!(out.contains("message User {"));
        assert!(out.contains("string id = 1;"));
        assert!(out.contains("optional string email = 2;"));
    }

    #[test]
    fn string_enum() {
        let mut schema = Schema::default();
        schema.add(TypeDef::string_enum("Status", vec!["active", "inactive"]));
        let out = generate_proto(&schema, None);
        assert!(out.contains("enum Status {"));
        assert!(out.contains("STATUS_UNSPECIFIED = 0;"));
        assert!(out.contains("STATUS_ACTIVE = 1;"));
        assert!(out.contains("STATUS_INACTIVE = 2;"));
    }

    #[test]
    fn array_field() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure(
            "List",
            vec![Field::required(
                "items",
                Type::Array(Box::new(Type::String)),
            )],
        ));
        let out = generate_proto(&schema, None);
        assert!(out.contains("repeated string items = 1;"));
    }

    #[test]
    fn package() {
        let mut schema = Schema::default();
        schema.add(TypeDef::structure("Empty", vec![]));
        let out = generate_proto(&schema, Some("com.example"));
        assert!(out.contains("package com.example;"));
    }
}
