use std::collections::HashMap;
use std::sync::LazyLock;

use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_primitives::{Address, U256, keccak256};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// A single decoded parameter from calldata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedParam {
    pub name: String,
    pub sol_type: String,
    pub value: String,
}

/// ERC20 token metadata, attached when the call targets a known token contract.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenMeta {
    pub symbol: String,
    pub decimals: u8,
}

/// Decoded calldata attached to a SigningRequest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedCalldata {
    pub selector: String,
    pub signature: String,
    pub function_name: String,
    pub params: Vec<DecodedParam>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<CalldataWarning>,
    /// True when parameter names came from a verified contract ABI (Sourcify).
    #[serde(default)]
    pub abi_verified: bool,
    /// ERC20 symbol/decimals for the target token, when applicable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_meta: Option<TokenMeta>,
}

/// Warning flag for dangerous token operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum CalldataWarning {
    #[serde(rename = "unlimited_approval")]
    UnlimitedApproval { spender: String },
    #[serde(rename = "approval_for_all")]
    ApprovalForAll { operator: String },
}

// Hardcoded selectors for common ERC20/ERC721/ERC1155 functions.
static KNOWN_SELECTORS: LazyLock<HashMap<[u8; 4], &'static str>> = LazyLock::new(|| {
    let mut m = HashMap::new();
    // ERC20
    m.insert([0xa9, 0x05, 0x9c, 0xbb], "transfer(address,uint256)");
    m.insert([0x09, 0x5e, 0xa7, 0xb3], "approve(address,uint256)");
    m.insert(
        [0x23, 0xb8, 0x72, 0xdd],
        "transferFrom(address,address,uint256)",
    );
    // ERC721
    m.insert(
        [0x42, 0x84, 0x2e, 0x0e],
        "safeTransferFrom(address,address,uint256)",
    );
    m.insert(
        [0xb8, 0x8d, 0x4f, 0xde],
        "safeTransferFrom(address,address,uint256,bytes)",
    );
    // ERC721/ERC1155
    m.insert([0xa2, 0x2c, 0xb4, 0x65], "setApprovalForAll(address,bool)");
    // ERC1155
    m.insert(
        [0xf2, 0x42, 0x43, 0x2a],
        "safeTransferFrom(address,address,uint256,uint256,bytes)",
    );
    m.insert(
        [0x2e, 0xb2, 0xc2, 0xd6],
        "safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)",
    );
    m
});

/// Attempt to decode calldata for a transaction.
///
/// `to` (the transaction target) and `chain_id` (hex, e.g. "0x1") enable two
/// best-effort enrichments: verified parameter names from the contract's
/// Sourcify ABI, and ERC20 symbol/decimals so amounts can be shown human-readable.
pub async fn decode_calldata(
    client: &reqwest::Client,
    selector_cache: &DashMap<[u8; 4], String>,
    rpc_url: &str,
    chain_id: Option<&str>,
    to: Option<&str>,
    calldata_hex: &str,
) -> Option<DecodedCalldata> {
    let raw = hex::decode(calldata_hex.strip_prefix("0x").unwrap_or(calldata_hex)).ok()?;
    if raw.len() < 4 {
        return None;
    }

    let selector: [u8; 4] = raw[..4].try_into().ok()?;
    let selector_hex = format!("0x{}", hex::encode(selector));
    let params_bytes = &raw[4..];

    // 1. Hardcoded → 2. Cache → 3. Remote lookup
    let signature = if let Some(sig) = KNOWN_SELECTORS.get(&selector) {
        sig.to_string()
    } else if let Some(sig) = selector_cache.get(&selector) {
        sig.clone()
    } else {
        match lookup_selector_remote(client, &selector_hex).await {
            Some(sig) => {
                selector_cache.insert(selector, sig.clone());
                sig
            }
            None => return None,
        }
    };

    // Parse signature: "transfer(address,uint256)" → name + types
    let open = signature.find('(')?;
    let close = signature.rfind(')')?;
    let function_name = signature[..open].to_string();
    let types_str = &signature[open + 1..close];

    let param_types: Vec<&str> = if types_str.is_empty() {
        vec![]
    } else {
        types_str.split(',').collect()
    };

    // Use alloy to decode params
    let tuple_type_str = format!("({})", types_str);
    let tuple_type = DynSolType::parse(&tuple_type_str).ok()?;
    let decoded = tuple_type.abi_decode_params(params_bytes).ok()?;

    let values = match decoded {
        DynSolValue::Tuple(vals) => vals,
        single => vec![single],
    };

    let params: Vec<DecodedParam> = values
        .into_iter()
        .enumerate()
        .map(|(i, val)| {
            let sol_type = param_types.get(i).unwrap_or(&"unknown").to_string();
            let value = format_sol_value(&val);
            DecodedParam {
                name: format!("param{}", i),
                sol_type,
                value,
            }
        })
        .collect();

    let warnings = detect_warnings(&function_name, &params);

    let mut decoded = DecodedCalldata {
        selector: selector_hex,
        signature,
        function_name,
        params,
        warnings,
        abi_verified: false,
        token_meta: None,
    };

    // Best-effort enrichment (verified ABI names + token metadata). Failures are
    // silently ignored — the base decode above is already useful on its own.
    if let Some(to) = to {
        let chain_dec = chain_id.and_then(hex_to_decimal);
        if let Some(chain_dec) = chain_dec.as_deref() {
            apply_verified_abi(client, chain_dec, to, selector, &mut decoded).await;
        }
        if matches!(
            decoded.function_name.as_str(),
            "transfer" | "approve" | "transferFrom"
        ) {
            decoded.token_meta = fetch_token_meta(client, rpc_url, to).await;
        }
    }

    Some(decoded)
}

/// Convert a hex chain id ("0x1") to its decimal string ("1").
fn hex_to_decimal(hex: &str) -> Option<String> {
    u64::from_str_radix(hex.trim_start_matches("0x"), 16)
        .ok()
        .map(|n| n.to_string())
}

/// Overwrite generic `paramN` names with real names from the contract's verified
/// Sourcify ABI, when the function selector matches.
async fn apply_verified_abi(
    client: &reqwest::Client,
    chain_dec: &str,
    to: &str,
    selector: [u8; 4],
    decoded: &mut DecodedCalldata,
) {
    let Some(abi) = fetch_sourcify_abi(client, chain_dec, to).await else {
        return;
    };
    let Some(functions) = abi.as_array() else {
        return;
    };

    for entry in functions {
        if entry.get("type").and_then(|t| t.as_str()) != Some("function") {
            continue;
        }
        let Some(name) = entry.get("name").and_then(|n| n.as_str()) else {
            continue;
        };
        let inputs = entry.get("inputs").and_then(|i| i.as_array());
        let canonical = format!("{name}({})", abi_input_types(inputs));
        if keccak256(canonical.as_bytes())[..4] != selector {
            continue;
        }

        // Match — copy the real argument names onto the decoded params.
        if let Some(inputs) = inputs {
            for (i, input) in inputs.iter().enumerate() {
                if let (Some(p), Some(n)) = (
                    decoded.params.get_mut(i),
                    input.get("name").and_then(|n| n.as_str()),
                ) && !n.is_empty()
                {
                    p.name = n.to_string();
                }
            }
        }
        decoded.abi_verified = true;
        return;
    }
}

/// Build the comma-joined canonical type list for a set of ABI inputs,
/// recursing into tuples (e.g. `(address,uint256)[]`).
fn abi_input_types(inputs: Option<&Vec<Value>>) -> String {
    let Some(inputs) = inputs else {
        return String::new();
    };
    inputs
        .iter()
        .map(|input| {
            let ty = input.get("type").and_then(|t| t.as_str()).unwrap_or("");
            if let Some(suffix) = ty.strip_prefix("tuple") {
                let components = input.get("components").and_then(|c| c.as_array());
                format!("({}){suffix}", abi_input_types(components))
            } else {
                ty.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(",")
}

/// Fetch the verified contract ABI from Sourcify (full then partial match).
async fn fetch_sourcify_abi(client: &reqwest::Client, chain_dec: &str, to: &str) -> Option<Value> {
    let address = to.parse::<Address>().ok()?.to_checksum(None);
    let timeout = std::time::Duration::from_secs(2);
    for match_type in ["full_match", "partial_match"] {
        let url = format!(
            "https://repo.sourcify.dev/contracts/{match_type}/{chain_dec}/{address}/metadata.json"
        );
        let Ok(resp) = client.get(&url).timeout(timeout).send().await else {
            continue;
        };
        if !resp.status().is_success() {
            continue;
        }
        if let Ok(meta) = resp.json::<Value>().await
            && let Some(abi) = meta.get("output").and_then(|o| o.get("abi"))
        {
            return Some(abi.clone());
        }
    }
    None
}

/// Fetch ERC20 `symbol()` and `decimals()` from the token contract via eth_call.
async fn fetch_token_meta(
    client: &reqwest::Client,
    rpc_url: &str,
    token: &str,
) -> Option<TokenMeta> {
    // symbol() = 0x95d89b41, decimals() = 0x313ce567
    let (symbol_raw, decimals_raw) = tokio::join!(
        eth_call(client, rpc_url, token, "0x95d89b41"),
        eth_call(client, rpc_url, token, "0x313ce567"),
    );
    let symbol = decode_string_return(symbol_raw?.as_str())?;
    let decimals = decode_u8_return(decimals_raw?.as_str())?;
    Some(TokenMeta { symbol, decimals })
}

/// Minimal `eth_call` returning the raw result hex string.
async fn eth_call(client: &reqwest::Client, rpc_url: &str, to: &str, data: &str) -> Option<String> {
    let body = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "eth_call",
        "params": [{ "to": to, "data": data }, "latest"],
    });
    let resp = client
        .post(rpc_url)
        .json(&body)
        .timeout(std::time::Duration::from_secs(2))
        .send()
        .await
        .ok()?;
    let v: Value = resp.json().await.ok()?;
    v.get("result")?.as_str().map(String::from)
}

/// Decode an ABI-encoded `string` return, falling back to `bytes32` (old tokens
/// like MKR return a fixed bytes32 symbol).
fn decode_string_return(hex: &str) -> Option<String> {
    let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)).ok()?;
    if let Ok(DynSolValue::String(s)) = DynSolType::String.abi_decode(&bytes) {
        let s = s.trim().to_string();
        if !s.is_empty() {
            return Some(s);
        }
    }
    if bytes.len() >= 32 {
        let s: String = String::from_utf8_lossy(&bytes[..32])
            .trim_matches(char::from(0))
            .trim()
            .to_string();
        if !s.is_empty() {
            return Some(s);
        }
    }
    None
}

/// Decode a uint return, keeping only the low byte (uint8 `decimals`).
fn decode_u8_return(hex: &str) -> Option<u8> {
    let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex)).ok()?;
    bytes.last().copied()
}

/// Look up a function signature by 4-byte selector.
/// Tries sourcify → openchain → 4byte.directory in order.
async fn lookup_selector_remote(client: &reqwest::Client, selector_hex: &str) -> Option<String> {
    let timeout = std::time::Duration::from_secs(2);

    // Sourcify (canonical, took over openchain + 4byte)
    if let Some(sig) = lookup_sourcify_format(
        client,
        &format!(
            "https://api.4byte.sourcify.dev/signature-database/v1/lookup?function={}&filter=true",
            selector_hex
        ),
        selector_hex,
        timeout,
    )
    .await
    {
        return Some(sig);
    }

    // Openchain (same format, may redirect to sourcify eventually)
    if let Some(sig) = lookup_sourcify_format(
        client,
        &format!(
            "https://api.openchain.xyz/signature-database/v1/lookup?function={}&filter=true",
            selector_hex
        ),
        selector_hex,
        timeout,
    )
    .await
    {
        return Some(sig);
    }

    // 4byte.directory (different response format)
    lookup_4byte(client, selector_hex, timeout).await
}

/// Shared parser for sourcify/openchain response format:
/// `{ "result": { "function": { "0x...": [{ "name": "..." }] } } }`
async fn lookup_sourcify_format(
    client: &reqwest::Client,
    url: &str,
    selector_hex: &str,
    timeout: std::time::Duration,
) -> Option<String> {
    let resp = client.get(url).timeout(timeout).send().await.ok()?;
    let body: serde_json::Value = resp.json().await.ok()?;
    body.get("result")?
        .get("function")?
        .get(selector_hex)?
        .as_array()?
        .first()?
        .get("name")?
        .as_str()
        .map(String::from)
}

async fn lookup_4byte(
    client: &reqwest::Client,
    selector_hex: &str,
    timeout: std::time::Duration,
) -> Option<String> {
    let url = format!(
        "https://www.4byte.directory/api/v1/signatures/?hex_signature={}",
        selector_hex
    );
    let resp = client.get(&url).timeout(timeout).send().await.ok()?;
    let body: serde_json::Value = resp.json().await.ok()?;
    body.get("results")?
        .as_array()?
        .first()?
        .get("text_signature")?
        .as_str()
        .map(String::from)
}

fn format_sol_value(val: &DynSolValue) -> String {
    match val {
        DynSolValue::Address(addr) => format!("{addr}"),
        DynSolValue::Uint(n, _) => n.to_string(),
        DynSolValue::Int(n, _) => n.to_string(),
        DynSolValue::Bool(b) => b.to_string(),
        DynSolValue::Bytes(b) => format!("0x{}", hex::encode(b)),
        DynSolValue::FixedBytes(w, _) => format!("0x{}", hex::encode(w.as_slice())),
        DynSolValue::String(s) => s.clone(),
        DynSolValue::Array(arr) | DynSolValue::FixedArray(arr) => {
            let items: Vec<String> = arr.iter().map(format_sol_value).collect();
            format!("[{}]", items.join(", "))
        }
        DynSolValue::Tuple(vals) => {
            let items: Vec<String> = vals.iter().map(format_sol_value).collect();
            format!("({})", items.join(", "))
        }
        other => format!("{:?}", other),
    }
}

fn detect_warnings(function_name: &str, params: &[DecodedParam]) -> Vec<CalldataWarning> {
    let mut warnings = Vec::new();

    match function_name {
        "approve" => {
            // approve(address spender, uint256 amount)
            if params.len() >= 2
                && let Ok(amount) = params[1].value.parse::<U256>()
                    && amount == U256::MAX {
                        warnings.push(CalldataWarning::UnlimitedApproval {
                            spender: params[0].value.clone(),
                        });
                    }
        }
        "setApprovalForAll"
            // setApprovalForAll(address operator, bool approved)
            if params.len() >= 2 && params[1].value == "true" => {
                warnings.push(CalldataWarning::ApprovalForAll {
                    operator: params[0].value.clone(),
                });
            }
        _ => {}
    }

    warnings
}
