use crate::interpreter::{
    data::ArcisField,
    supported_type::{ArrayLen, IntegerInfo, RustType, SupportedType},
};
use ff::PrimeField;
use serde::Serialize;
use std::collections::HashMap;

#[derive(Debug, Serialize)]
pub(crate) struct ClientValInfo {
    pub(crate) name: String,
    pub(crate) val_type: ValType,
}

#[derive(Debug, Serialize, Clone)]
pub(crate) enum ValType {
    Integer(IntegerInfo),
    FullInteger,
    Float,
    Bool,
    Pubkey,
}

/// Represents a field after array collapsing for TypeScript interface generation
pub(crate) struct CollapsedField {
    pub(crate) name: String,
    pub(crate) field_type: ValType,
    pub(crate) is_array: bool,
}

impl ValType {
    fn from_rust_type(rt: &RustType) -> Result<Self, &'static str> {
        let res = match rt {
            RustType::Integer(Some(i)) => {
                if i.width > ArcisField::CAPACITY as usize {
                    ValType::FullInteger
                } else {
                    ValType::Integer(*i)
                }
            }
            RustType::Integer(None) => return Err("Integer literal not supported."),
            RustType::Float => ValType::Float,
            RustType::ArcisField => ValType::FullInteger,
        };
        Ok(res)
    }
}

fn make_client_val_info(
    supported_type: &SupportedType,
    name: String,
    acc: &mut Vec<ClientValInfo>,
) -> Result<(), &'static str> {
    match supported_type {
        SupportedType::Unit => {}
        SupportedType::FieldValue(r) => acc.push(ClientValInfo {
            name,
            val_type: ValType::from_rust_type(r)?,
        }),
        SupportedType::Bool => acc.push(ClientValInfo {
            name,
            val_type: ValType::Bool,
        }),
        SupportedType::Curve(_) => acc.push(ClientValInfo {
            name,
            val_type: ValType::Pubkey,
        }),
        SupportedType::Struct(s) => {
            let field_types = s.types();
            for (field_idx, t) in field_types.iter().enumerate() {
                let mut name = name.clone();

                name.push('.');
                name.push_str(&s.get_field_name(field_idx));
                make_client_val_info(t, name, acc)?;
            }
        }
        SupportedType::Ref(ty) => {
            make_client_val_info(ty, name, acc)?;
        }
        SupportedType::Array(ty, n) => {
            let ArrayLen::Computed(n) = n else {
                return Err("Array length is unknown.");
            };
            for i in 0..*n {
                let mut name = name.clone();
                name.push_str(&format!("[{}]", i));
                make_client_val_info(ty, name, acc)?;
            }
        }
        SupportedType::Tuple(v) => {
            for (field_idx, x) in v.iter().enumerate() {
                let mut name = name.clone();
                name.push_str(&format!(".{}", field_idx));
                make_client_val_info(x, name, acc)?;
            }
        }
        SupportedType::Slice(ty, len) => {
            let Ok(len) = len.value() else {
                return Err("Slices cannot be inputs.");
            };
            make_client_val_info(
                &SupportedType::Array(ty.clone(), ArrayLen::Computed(len)),
                name,
                acc,
            )?;
        }
        SupportedType::Fn => return Err("Functions cannot be inputs."),
        SupportedType::ImplAny => return Err("Uninferred types like `impl X` cannot be inputs."),
        SupportedType::Var(_) => {
            return Err("Uninferred types like generic types cannot be inputs.")
        }
    };
    Ok(())
}

#[derive(Debug, Default)]
pub struct ClientTypes {
    types: HashMap<SupportedType, Vec<ClientValInfo>>,
}
impl ClientTypes {
    pub fn insert(&mut self, ty: SupportedType) -> Result<(), &'static str> {
        if self.types.contains_key(&ty) {
            return Ok(());
        }
        let name = String::new();
        let mut acc: Vec<ClientValInfo> = Vec::new();
        make_client_val_info(&ty, name, &mut acc)?;
        self.types.insert(ty, acc);
        Ok(())
    }
    pub fn to_json(&self) -> String {
        let hash_map: HashMap<_, _> = self.types.iter().map(|(r, v)| (r.to_string(), v)).collect();
        serde_json::to_string(&hash_map).expect("Failed to serialize HashMap")
    }

    /// Generates TypeScript code with type-safe packer definitions.
    ///
    /// Produces interfaces for input/output types and a module export
    /// with `createPacker` calls for each type.
    pub fn to_typescript(&self, module_name: &str) -> String {
        let mut out = String::new();

        out.push_str("// Auto-generated by arcium build - DO NOT EDIT\n");
        out.push_str("// Schema version: 1\n");
        out.push_str("import { createPacker } from '@arcium-hq/client';\n\n");

        let hash_map: HashMap<_, _> = self.types.iter().map(|(r, v)| (r.to_string(), v)).collect();

        // Sort by type name for deterministic output ordering
        let mut sorted_types: Vec<_> = hash_map.iter().collect();
        sorted_types.sort_by(|a, b| a.0.cmp(b.0));

        // Generate interfaces for each type
        for (type_name, fields) in &sorted_types {
            let collapsed = Self::collapse_array_fields(fields);

            // Input interface (accepts number | bigint for integers)
            out.push_str(&format!("export interface {} {{\n", type_name));
            for field in &collapsed {
                let ts_type = Self::to_ts_input_type(&field.field_type, field.is_array);
                out.push_str(&format!("    {}: {};\n", field.name, ts_type));
            }
            out.push_str("}\n\n");

            // Output interface (integers always return as bigint)
            out.push_str(&format!("export interface {}Output {{\n", type_name));
            for field in &collapsed {
                let ts_type = Self::to_ts_output_type(&field.field_type, field.is_array);
                out.push_str(&format!("    {}: {};\n", field.name, ts_type));
            }
            out.push_str("}\n\n");
        }

        // Generate module export with createPacker calls
        out.push_str(&format!("export const {} = {{\n", module_name));
        for (type_name, fields) in &sorted_types {
            out.push_str(&format!(
                "    {}: createPacker<{}, {}Output>([\n",
                type_name, type_name, type_name
            ));

            // Output original (non-collapsed) fields for packer internals
            for field in fields.iter() {
                let clean_name = field.name.trim_start_matches('.');
                let type_str = Self::val_type_to_ts(&field.val_type);
                out.push_str(&format!(
                    "        {{ name: '{}', type: {} }},\n",
                    clean_name, type_str
                ));
            }
            // Use `as const` to preserve literal string types for compile-time validation
            out.push_str(&format!("    ] as const, '{}'),\n", type_name));
        }
        out.push_str("} as const;\n");

        out
    }

    /// Collapses consecutive array fields into single array entries.
    ///
    /// Converts fields like `bytes[0]`, `bytes[1]`, ... into a single
    /// `bytes` field marked as an array.
    pub(crate) fn collapse_array_fields(fields: &[ClientValInfo]) -> Vec<CollapsedField> {
        let mut result: Vec<CollapsedField> = Vec::new();
        let mut i = 0;

        while i < fields.len() {
            let field = &fields[i];
            let name = field.name.trim_start_matches('.');

            // Check if this starts an array sequence (ends with [0])
            if name.ends_with("[0]") {
                let base_name = name.trim_end_matches("[0]");
                let mut count = 1;

                // Count consecutive array elements
                while i + count < fields.len() {
                    let next_name = fields[i + count].name.trim_start_matches('.');
                    let expected = format!("{}[{}]", base_name, count);
                    if next_name != expected {
                        break;
                    }
                    count += 1;
                }

                result.push(CollapsedField {
                    name: base_name.to_string(),
                    field_type: field.val_type.clone(),
                    is_array: true,
                });
                i += count;
            } else {
                result.push(CollapsedField {
                    name: name.to_string(),
                    field_type: field.val_type.clone(),
                    is_array: false,
                });
                i += 1;
            }
        }

        result
    }

    /// Converts ValType to TypeScript input type string.
    fn to_ts_input_type(val_type: &ValType, is_array: bool) -> String {
        let base = match val_type {
            ValType::Integer(_) => "number | bigint",
            ValType::FullInteger => "bigint",
            ValType::Float => "number",
            ValType::Bool => "boolean",
            ValType::Pubkey => "Uint8Array",
        };
        if is_array {
            format!("({})[]", base)
        } else {
            base.to_string()
        }
    }

    /// Converts ValType to TypeScript output type string.
    fn to_ts_output_type(val_type: &ValType, is_array: bool) -> String {
        let base = match val_type {
            ValType::Integer(_) => "bigint",
            ValType::FullInteger => "bigint",
            ValType::Float => "number",
            ValType::Bool => "boolean",
            ValType::Pubkey => "Uint8Array",
        };
        if is_array {
            format!("{}[]", base)
        } else {
            base.to_string()
        }
    }

    /// Converts ValType to TypeScript type literal for createPacker field definitions.
    pub(crate) fn val_type_to_ts(val_type: &ValType) -> String {
        match val_type {
            ValType::Integer(info) => format!(
                "{{ Integer: {{ signed: {}, width: {} }} }}",
                info.signed, info.width
            ),
            ValType::FullInteger => "'FullInteger'".to_string(),
            ValType::Float => "'Float'".to_string(),
            ValType::Bool => "'Bool'".to_string(),
            ValType::Pubkey => "'Pubkey'".to_string(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_val_type_to_ts_bool() {
        assert_eq!(ClientTypes::val_type_to_ts(&ValType::Bool), "'Bool'");
    }

    #[test]
    fn test_val_type_to_ts_float() {
        assert_eq!(ClientTypes::val_type_to_ts(&ValType::Float), "'Float'");
    }

    #[test]
    fn test_val_type_to_ts_full_integer() {
        assert_eq!(
            ClientTypes::val_type_to_ts(&ValType::FullInteger),
            "'FullInteger'"
        );
    }

    #[test]
    fn test_val_type_to_ts_pubkey() {
        assert_eq!(ClientTypes::val_type_to_ts(&ValType::Pubkey), "'Pubkey'");
    }

    #[test]
    fn test_val_type_to_ts_integer() {
        let int_type = ValType::Integer(IntegerInfo {
            signed: true,
            width: 32,
        });
        assert_eq!(
            ClientTypes::val_type_to_ts(&int_type),
            "{ Integer: { signed: true, width: 32 } }"
        );
    }

    #[test]
    fn test_collapse_array_fields_single_array() {
        let fields = vec![
            ClientValInfo {
                name: ".bytes[0]".to_string(),
                val_type: ValType::Integer(IntegerInfo {
                    signed: false,
                    width: 8,
                }),
            },
            ClientValInfo {
                name: ".bytes[1]".to_string(),
                val_type: ValType::Integer(IntegerInfo {
                    signed: false,
                    width: 8,
                }),
            },
            ClientValInfo {
                name: ".bytes[2]".to_string(),
                val_type: ValType::Integer(IntegerInfo {
                    signed: false,
                    width: 8,
                }),
            },
        ];

        let collapsed = ClientTypes::collapse_array_fields(&fields);

        assert_eq!(collapsed.len(), 1);
        assert_eq!(collapsed[0].name, "bytes");
        assert!(collapsed[0].is_array);
    }

    #[test]
    fn test_collapse_array_fields_non_array() {
        let fields = vec![ClientValInfo {
            name: ".value".to_string(),
            val_type: ValType::Bool,
        }];

        let collapsed = ClientTypes::collapse_array_fields(&fields);

        assert_eq!(collapsed.len(), 1);
        assert_eq!(collapsed[0].name, "value");
        assert!(!collapsed[0].is_array);
    }
}
