use crate::ir::definitions::{CallbackKind, ParamDef, ParamPassing, ReturnDef};
use crate::ir::types::TypeExpr;

use super::super::ast::{
    CSharpClassName, CSharpLocalName, CSharpParamName, CSharpType, CSharpTypeReference,
};
use super::super::plan::{CSharpParamKind, CSharpParamPlan, CSharpWireWriterPlan};
use super::lowerer::CSharpLowerer;

impl<'a> CSharpLowerer<'a> {
    /// Lowers a Rust param to a [`CSharpParamPlan`]. Returns `None` for
    /// non-by-value passing, unsupported types, or wire-encoded params
    /// whose `wire_writer` isn't pre-registered.
    pub(super) fn lower_param(
        &self,
        param: &ParamDef,
        wire_writers: &[CSharpWireWriterPlan],
    ) -> Option<CSharpParamPlan> {
        if param.passing != ParamPassing::Value {
            if let TypeExpr::Callback(id) = &param.type_expr {
                return self.lower_callback_param(param, id);
            }
            // `&mut [T]` is only safe when T uses an in-place array
            // representation. Wire-encoded element types would mutate a
            // temporary buffer with no writeback to the managed caller
            // (#345).
            if !matches!(
                (&param.passing, &param.type_expr),
                (ParamPassing::RefMut, TypeExpr::Vec(inner))
                    if self.is_blittable_vec_element(inner)
            ) {
                return None;
            }
        }

        let csharp_type = self.lower_type(&param.type_expr)?;
        let csharp_param_name: CSharpParamName = (&param.name).into();
        if let TypeExpr::Callback(id) = &param.type_expr {
            return self.lower_callback_param(param, id);
        }
        if let TypeExpr::Option(inner) = &param.type_expr
            && let TypeExpr::Callback(id) = inner.as_ref()
        {
            return Some(CSharpParamPlan {
                name: csharp_param_name,
                csharp_type,
                kind: CSharpParamKind::CallbackHandle {
                    bridge_class: self.callback_bridge_class_name(id),
                },
            });
        }
        // The macro exposes Custom-typed params (and Vec<Custom<_>>
        // params) as wire-buffer C ABIs regardless of repr — Custom is
        // treated as opaque on the boundary, so even Vec<Custom<i64>>
        // ships as a length-prefixed buffer, not a raw `long[]`. Force
        // the wire-encoded path here so the call site uses the
        // `_xBytes` local rather than the raw param.
        let is_custom_boundary = match &param.type_expr {
            TypeExpr::Custom(_) => true,
            TypeExpr::Vec(inner) => matches!(inner.as_ref(), TypeExpr::Custom(_)),
            _ => false,
        };
        if is_custom_boundary {
            return Some(CSharpParamPlan {
                name: csharp_param_name.clone(),
                csharp_type,
                kind: wire_encoded_kind(wire_writers, &param.name)?,
            });
        }
        let normalized = self.normalize_custom_type_expr(&param.type_expr);
        let kind = match &normalized {
            TypeExpr::String => CSharpParamKind::Utf8Bytes,
            TypeExpr::Builtin(_) => wire_encoded_kind(wire_writers, &param.name)?,
            TypeExpr::Record(id) if !self.is_blittable_record(id) => {
                wire_encoded_kind(wire_writers, &param.name)?
            }
            TypeExpr::Enum(id) if self.is_data_enum(id) => {
                wire_encoded_kind(wire_writers, &param.name)?
            }
            TypeExpr::Vec(inner) if matches!(inner.as_ref(), TypeExpr::Primitive(_)) => {
                CSharpParamKind::DirectArray
            }
            TypeExpr::Vec(inner) if self.is_blittable_vec_element(inner) => {
                // Primitive arrays can use the CLR's built-in direct-array
                // path. Record arrays are less predictable once the element
                // type stops being blittable to the marshaller, e.g. because
                // it contains `bool` or `char`: P/Invoke may marshal through
                // a temporary native buffer rather than exposing the managed
                // array in place. `fixed` keeps this path zero-copy and
                // makes the ABI contract explicit: Rust reads the actual
                // managed element buffer, not a marshaled surrogate.
                let element_type = match inner.as_ref() {
                    TypeExpr::Record(id) => {
                        CSharpType::Record(CSharpTypeReference::Plain(id.into()))
                    }
                    other => todo!(
                        "C# backend pinned-array param support not yet implemented for {other:?}"
                    ),
                };
                CSharpParamKind::PinnedArray {
                    element_type,
                    ptr_local: CSharpLocalName::for_pinned_ptr(&csharp_param_name),
                }
            }
            TypeExpr::Vec(inner) if self.is_supported_vec_element(inner) => {
                // Vec<String> and Vec<Vec<_>> carry variable-width elements, so
                // the param travels wire-encoded rather than as a pinned T[].
                wire_encoded_kind(wire_writers, &param.name)?
            }
            TypeExpr::Option(_) => {
                // Options are always wire-encoded: the 1-byte tag plus an
                // optional payload does not line up with any CLR
                // primitive layout.
                wire_encoded_kind(wire_writers, &param.name)?
            }
            // Primitives, bools, blittable records, and C-style enums
            // pass directly. The CLR marshals them across P/Invoke with
            // no extra setup.
            _ => CSharpParamKind::Direct,
        };

        Some(CSharpParamPlan {
            name: csharp_param_name,
            csharp_type,
            kind,
        })
    }

    /// Lowers a return signature to its C# type. For `Result<T, E>`
    /// the C# return type is `T`: errors travel as exceptions, so the
    /// public method signature only carries the success type. Returns
    /// `None` if the ok or err type isn't admissible.
    pub(super) fn lower_return(&self, return_def: &ReturnDef) -> Option<CSharpType> {
        match return_def {
            ReturnDef::Void => Some(CSharpType::Void),
            ReturnDef::Value(type_expr) => self.lower_return_value(type_expr),
            ReturnDef::Result { ok, err } => {
                if !self.is_supported_result_type(ok) || !self.is_supported_result_type(err) {
                    return None;
                }
                match ok {
                    TypeExpr::Void => Some(CSharpType::Void),
                    other => self.lower_type(other),
                }
            }
        }
    }

    fn lower_return_value(&self, type_expr: &TypeExpr) -> Option<CSharpType> {
        match type_expr {
            TypeExpr::Callback(id) => Some(CSharpType::Record(
                self.callback_proxy_class_name(id).into(),
            )),
            other => self.lower_type(other),
        }
    }

    /// Maps a Rust [`TypeExpr`] to its C# equivalent. Returns `None` if
    /// the type isn't admitted by the backend (callbacks, streams,
    /// records/enums outside the supported sets). `Custom` resolves to
    /// its `repr` so foreign code only sees the wire type.
    pub(super) fn lower_type(&self, type_expr: &TypeExpr) -> Option<CSharpType> {
        match type_expr {
            TypeExpr::Void => Some(CSharpType::Void),
            TypeExpr::Primitive(primitive) => Some(CSharpType::from(*primitive)),
            TypeExpr::String => Some(CSharpType::String),
            TypeExpr::Builtin(id) => Some(CSharpType::Builtin(id.clone())),
            TypeExpr::Record(id) if self.supported_records.contains(id) => {
                let class_name: CSharpClassName = id.into();
                Some(CSharpType::Record(class_name.into()))
            }
            TypeExpr::Enum(id) if self.supported_enums.contains(id) => {
                let enum_def = self.ffi.catalog.resolve_enum(id)?;
                Some(CSharpType::for_enum(enum_def))
            }
            TypeExpr::Custom(id) => self.lower_type(self.custom_repr_type(id)),
            TypeExpr::Vec(inner) if self.is_supported_vec_element(inner) => {
                let inner_type = self.lower_type(inner)?;
                Some(CSharpType::Array(Box::new(inner_type)))
            }
            TypeExpr::Option(inner) => {
                let inner_type = self.lower_type(inner)?;
                Some(CSharpType::Nullable(Box::new(inner_type)))
            }
            TypeExpr::Callback(id) => Some(CSharpType::Record(
                self.callback_public_class_name(id).into(),
            )),
            _ => None,
        }
    }

    fn lower_callback_param(
        &self,
        param: &ParamDef,
        id: &crate::ir::ids::CallbackId,
    ) -> Option<CSharpParamPlan> {
        let callback = self.ffi.catalog.resolve_callback(id)?;
        let name: CSharpParamName = (&param.name).into();
        let csharp_type = CSharpType::Record(self.callback_public_class_name(id).into());
        let kind = match callback.kind {
            CallbackKind::Closure if param.passing == ParamPassing::ImplTrait => {
                CSharpParamKind::InlineClosure {
                    bridge_class: self.callback_bridge_class_name(id),
                    scope_local: CSharpLocalName::for_inline_callback_scope(&name),
                    context_param_name: CSharpParamName::new(format!("{}Ud", name.as_str())),
                }
            }
            _ => CSharpParamKind::CallbackHandle {
                bridge_class: self.callback_bridge_class_name(id),
            },
        };
        Some(CSharpParamPlan {
            name,
            csharp_type,
            kind,
        })
    }
}

/// Builds the [`CSharpParamKind::WireEncoded`] variant by finding the
/// matching pre-registered wire writer. Returns `None` if no writer with
/// the given param name exists (caller propagates).
fn wire_encoded_kind(
    wire_writers: &[CSharpWireWriterPlan],
    param_name: impl Into<CSharpParamName>,
) -> Option<CSharpParamKind> {
    let csharp_name = param_name.into();
    let writer = wire_writers
        .iter()
        .find(|w| w.param_name.as_str() == csharp_name.as_str())?;
    Some(CSharpParamKind::WireEncoded {
        binding_name: writer.bytes_binding_name.clone(),
    })
}
