use std::str::FromStr;

use reqwest::Url;
use thiserror::Error;

use crate::rpc::config::HttpRpcSettings;

const CHAIN_ETHEREUM: u64 = 1;
const CHAIN_OPTIMISM: u64 = 10;
const CHAIN_BNB_SMART_CHAIN: u64 = 56;
const CHAIN_POLYGON: u64 = 137;
const CHAIN_ARBITRUM: u64 = 42161;
const CHAIN_BASE: u64 = 8453;

const INFURA_CHAINS: &[(u64, &str)] = &[
    (CHAIN_ETHEREUM, "mainnet"),
    (CHAIN_ARBITRUM, "arbitrum-mainnet"),
    (CHAIN_BASE, "base-mainnet"),
    (CHAIN_OPTIMISM, "optimism-mainnet"),
    (CHAIN_POLYGON, "polygon-mainnet"),
    (CHAIN_BNB_SMART_CHAIN, "bsc-mainnet"),
];

const ANKR_CHAINS: &[(u64, &str)] = &[
    (CHAIN_ETHEREUM, "eth"),
    (CHAIN_ARBITRUM, "arbitrum"),
    (CHAIN_BASE, "base"),
    (CHAIN_OPTIMISM, "optimism"),
    (CHAIN_POLYGON, "polygon"),
    (CHAIN_BNB_SMART_CHAIN, "bsc"),
];

const ALCHEMY_CHAINS: &[(u64, &str)] = &[
    (CHAIN_ETHEREUM, "eth-mainnet"),
    (CHAIN_ARBITRUM, "arb-mainnet"),
    (CHAIN_BASE, "base-mainnet"),
    (CHAIN_OPTIMISM, "opt-mainnet"),
    (CHAIN_POLYGON, "polygon-mainnet"),
    (CHAIN_BNB_SMART_CHAIN, "bnb-mainnet"),
];

const QUICKNODE_CHAINS: &[(u64, &str)] = &[
    (CHAIN_ETHEREUM, "eth-mainnet"),
    (CHAIN_ARBITRUM, "arbitrum-mainnet"),
    (CHAIN_BASE, "base-mainnet"),
    (CHAIN_OPTIMISM, "optimism"),
    (CHAIN_POLYGON, "matic"),
    (CHAIN_BNB_SMART_CHAIN, "bsc"),
];

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ProviderUrlError {
    #[error("unsupported chain ID {chain_id} for provider {provider}")]
    UnsupportedChain {
        provider: &'static str,
        chain_id: u64,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
    Infura,
    Ankr,
    Alchemy,
    QuickNode,
}

impl FromStr for Provider {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let lower = s.to_ascii_lowercase();
        if lower.contains("infura.io") {
            Ok(Self::Infura)
        } else if lower.contains("ankr.com") {
            Ok(Self::Ankr)
        } else if lower.contains("alchemy.com") {
            Ok(Self::Alchemy)
        } else if lower.contains("quiknode.pro") || lower.contains("quicknode") {
            Ok(Self::QuickNode)
        } else {
            Err("unknown provider")
        }
    }
}

impl Provider {
    fn name(&self) -> &'static str {
        match self {
            Provider::Infura => "infura",
            Provider::Ankr => "ankr",
            Provider::Alchemy => "alchemy",
            Provider::QuickNode => "quicknode",
        }
    }

    pub fn rpc_settings(&self, url: impl AsRef<str>) -> HttpRpcSettings {
        let url: Url = url.as_ref().parse().expect("Invalid URL");
        match self {
            Provider::Ankr => HttpRpcSettings {
                url,
                max_concurrency: 16,
                max_batch_size: 5000,
                max_logs_per_request: 60_000,
                init_batch_size: 500,
            },
            Provider::Infura => HttpRpcSettings {
                url,
                max_concurrency: 8,
                max_batch_size: 1000,
                max_logs_per_request: 10_000,
                init_batch_size: 500,
            },
            Provider::Alchemy => HttpRpcSettings {
                url,
                max_concurrency: 8,
                max_batch_size: 1000,
                max_logs_per_request: 10_000,
                init_batch_size: 500,
            },
            Provider::QuickNode => HttpRpcSettings {
                url,
                max_concurrency: 8,
                max_batch_size: 1000,
                max_logs_per_request: 10_000,
                init_batch_size: 500,
            },
        }
    }

    pub fn http_url(&self, api_key: &str, chain_id: u64) -> Result<Url, ProviderUrlError> {
        self.build_url(api_key, chain_id, false)
    }

    pub fn ws_url(&self, api_key: &str, chain_id: u64) -> Result<Url, ProviderUrlError> {
        self.build_url(api_key, chain_id, true)
    }

    fn build_url(
        &self,
        api_key: &str,
        chain_id: u64,
        websocket: bool,
    ) -> Result<Url, ProviderUrlError> {
        let unsupported_chain_error = || ProviderUrlError::UnsupportedChain {
            provider: self.name(),
            chain_id,
        };

        match self {
            Provider::Infura => {
                let chain =
                    chain_name(chain_id, INFURA_CHAINS).ok_or_else(unsupported_chain_error)?;
                let scheme = if websocket { "wss" } else { "https" };
                let path = if websocket { "ws/v3" } else { "v3" };
                Ok(format!("{scheme}://{chain}.infura.io/{path}/{api_key}")
                    .parse()
                    .unwrap())
            }
            Provider::Ankr => {
                let chain =
                    chain_name(chain_id, ANKR_CHAINS).ok_or_else(unsupported_chain_error)?;
                let scheme = if websocket { "wss" } else { "https" };
                Ok(format!("{scheme}://rpc.ankr.com/{chain}/{api_key}")
                    .parse()
                    .unwrap())
            }
            Provider::Alchemy => {
                let chain =
                    chain_name(chain_id, ALCHEMY_CHAINS).ok_or_else(unsupported_chain_error)?;
                let scheme = if websocket { "wss" } else { "https" };
                let path = if websocket { "ws/v2" } else { "v2" };
                Ok(format!("{scheme}://{chain}.g.alchemy.com/{path}/{api_key}")
                    .parse()
                    .unwrap())
            }
            Provider::QuickNode => {
                let chain =
                    chain_name(chain_id, QUICKNODE_CHAINS).ok_or_else(unsupported_chain_error)?;
                let scheme = if websocket { "wss" } else { "https" };
                Ok(format!("{scheme}://{chain}.quiknode.pro/{api_key}")
                    .parse()
                    .unwrap())
            }
        }
    }

    pub fn detect(url: &str) -> Result<Self, &'static str> {
        url.parse()
    }
}

fn chain_name(chain_id: u64, supported_chains: &[(u64, &'static str)]) -> Option<&'static str> {
    supported_chains
        .iter()
        .find(|(id, _)| *id == chain_id)
        .map(|(_, name)| *name)
}

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

    // ==================== Detection tests ====================

    #[test]
    fn detect_from_url() {
        assert_eq!(
            Provider::detect("https://mainnet.infura.io/v3/abc123"),
            Ok(Provider::Infura)
        );
        assert_eq!(
            Provider::detect("https://rpc.ankr.com/eth"),
            Ok(Provider::Ankr)
        );
        assert_eq!(
            Provider::detect("https://eth-mainnet.g.alchemy.com/v2/abc"),
            Ok(Provider::Alchemy)
        );
        assert_eq!(
            Provider::detect("https://example.quiknode.pro/abc"),
            Ok(Provider::QuickNode)
        );
    }

    #[test]
    fn detect_unknown_provider() {
        let result = Provider::detect("https://example.com");
        assert!(result.is_err());
    }

    #[test]
    fn detect_case_insensitive() {
        assert_eq!(
            Provider::detect("HTTPS://INFURA.IO/V3/ABC"),
            Ok(Provider::Infura)
        );
        assert_eq!(Provider::detect("https://ANKR.COM/eth"), Ok(Provider::Ankr));
        assert_eq!(
            Provider::detect("https://ALCHEMY.COM/v2/abc"),
            Ok(Provider::Alchemy)
        );
    }

    #[test]
    fn detect_quicknode_variants() {
        assert_eq!(
            Provider::detect("https://example.quiknode.pro/abc"),
            Ok(Provider::QuickNode)
        );
        assert_eq!(
            Provider::detect("https://example.quicknode.pro/abc"),
            Ok(Provider::QuickNode)
        );
    }

    // ==================== HttpSettings tests ====================

    #[test]
    fn infura_settings() {
        let settings = Provider::Infura.rpc_settings("https://mainnet.infura.io/v3/test");
        assert_eq!(settings.max_concurrency, 8);
        assert_eq!(settings.max_batch_size, 1000);
        assert_eq!(settings.max_logs_per_request, 10_000);
        assert_eq!(settings.init_batch_size, 500);
    }

    #[test]
    fn ankr_settings() {
        let settings = Provider::Ankr.rpc_settings("https://rpc.ankr.com/eth");
        assert_eq!(settings.max_concurrency, 16);
        assert_eq!(settings.max_batch_size, 5000);
        assert_eq!(settings.max_logs_per_request, 60_000);
        assert_eq!(settings.init_batch_size, 500);
    }

    #[test]
    fn alchemy_settings() {
        let settings = Provider::Alchemy.rpc_settings("https://eth-mainnet.g.alchemy.com/v2/test");
        assert_eq!(settings.max_concurrency, 8);
        assert_eq!(settings.max_batch_size, 1000);
        assert_eq!(settings.max_logs_per_request, 10_000);
        assert_eq!(settings.init_batch_size, 500);
    }

    #[test]
    fn quicknode_settings() {
        let settings = Provider::QuickNode.rpc_settings("https://example.quiknode.pro/test");
        assert_eq!(settings.max_concurrency, 8);
        assert_eq!(settings.max_batch_size, 1000);
        assert_eq!(settings.max_logs_per_request, 10_000);
        assert_eq!(settings.init_batch_size, 500);
    }

    // ==================== URL Generation - Infura ====================

    #[test]
    fn infura_http_url_mainnet() {
        let url = Provider::Infura.http_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "https://mainnet.infura.io/v3/mykey");
    }

    #[test]
    fn infura_ws_url_mainnet() {
        let url = Provider::Infura.ws_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "wss://mainnet.infura.io/ws/v3/mykey");
    }

    #[test]
    fn infura_http_url_base() {
        let url = Provider::Infura.http_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "https://base-mainnet.infura.io/v3/mykey");
    }

    #[test]
    fn infura_ws_url_base() {
        let url = Provider::Infura.ws_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "wss://base-mainnet.infura.io/ws/v3/mykey");
    }

    #[test]
    fn infura_supports_tier1_and_tier2_chains() {
        let cases = [
            (CHAIN_ETHEREUM, "mainnet"),
            (CHAIN_ARBITRUM, "arbitrum-mainnet"),
            (CHAIN_BASE, "base-mainnet"),
            (CHAIN_OPTIMISM, "optimism-mainnet"),
            (CHAIN_POLYGON, "polygon-mainnet"),
            (CHAIN_BNB_SMART_CHAIN, "bsc-mainnet"),
        ];

        for (chain_id, network) in cases {
            let http_url = Provider::Infura.http_url("mykey", chain_id).unwrap();
            let ws_url = Provider::Infura.ws_url("mykey", chain_id).unwrap();
            let expected_http = format!("https://{network}.infura.io/v3/mykey");
            let expected_ws = format!("wss://{network}.infura.io/ws/v3/mykey");

            assert_eq!(http_url.as_str(), expected_http);
            assert_eq!(ws_url.as_str(), expected_ws);
        }
    }

    #[test]
    fn infura_unsupported_chain_fails() {
        let result = Provider::Infura.http_url("key", 999);
        assert_eq!(
            result,
            Err(ProviderUrlError::UnsupportedChain {
                provider: "infura",
                chain_id: 999
            })
        );
    }

    // ==================== URL Generation - Ankr ====================

    #[test]
    fn ankr_http_url_mainnet() {
        let url = Provider::Ankr.http_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "https://rpc.ankr.com/eth/mykey");
    }

    #[test]
    fn ankr_ws_url_mainnet() {
        let url = Provider::Ankr.ws_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "wss://rpc.ankr.com/eth/mykey");
    }

    #[test]
    fn ankr_http_url_base() {
        let url = Provider::Ankr.http_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "https://rpc.ankr.com/base/mykey");
    }

    #[test]
    fn ankr_ws_url_base() {
        let url = Provider::Ankr.ws_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "wss://rpc.ankr.com/base/mykey");
    }

    #[test]
    fn ankr_supports_tier1_and_tier2_chains() {
        let cases = [
            (CHAIN_ETHEREUM, "eth"),
            (CHAIN_ARBITRUM, "arbitrum"),
            (CHAIN_BASE, "base"),
            (CHAIN_OPTIMISM, "optimism"),
            (CHAIN_POLYGON, "polygon"),
            (CHAIN_BNB_SMART_CHAIN, "bsc"),
        ];

        for (chain_id, network) in cases {
            let http_url = Provider::Ankr.http_url("mykey", chain_id).unwrap();
            let ws_url = Provider::Ankr.ws_url("mykey", chain_id).unwrap();
            let expected_http = format!("https://rpc.ankr.com/{network}/mykey");
            let expected_ws = format!("wss://rpc.ankr.com/{network}/mykey");

            assert_eq!(http_url.as_str(), expected_http);
            assert_eq!(ws_url.as_str(), expected_ws);
        }
    }

    #[test]
    fn ankr_unsupported_chain_fails() {
        let result = Provider::Ankr.http_url("key", 999);
        assert_eq!(
            result,
            Err(ProviderUrlError::UnsupportedChain {
                provider: "ankr",
                chain_id: 999
            })
        );
    }

    // ==================== URL Generation - Alchemy ====================

    #[test]
    fn alchemy_http_url_mainnet() {
        let url = Provider::Alchemy.http_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "https://eth-mainnet.g.alchemy.com/v2/mykey");
    }

    #[test]
    fn alchemy_ws_url_mainnet() {
        let url = Provider::Alchemy.ws_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "wss://eth-mainnet.g.alchemy.com/ws/v2/mykey");
    }

    #[test]
    fn alchemy_http_url_base() {
        let url = Provider::Alchemy.http_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "https://base-mainnet.g.alchemy.com/v2/mykey");
    }

    #[test]
    fn alchemy_ws_url_base() {
        let url = Provider::Alchemy.ws_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "wss://base-mainnet.g.alchemy.com/ws/v2/mykey");
    }

    #[test]
    fn alchemy_supports_tier1_and_tier2_chains() {
        let cases = [
            (CHAIN_ETHEREUM, "eth-mainnet"),
            (CHAIN_ARBITRUM, "arb-mainnet"),
            (CHAIN_BASE, "base-mainnet"),
            (CHAIN_OPTIMISM, "opt-mainnet"),
            (CHAIN_POLYGON, "polygon-mainnet"),
            (CHAIN_BNB_SMART_CHAIN, "bnb-mainnet"),
        ];

        for (chain_id, network) in cases {
            let http_url = Provider::Alchemy.http_url("mykey", chain_id).unwrap();
            let ws_url = Provider::Alchemy.ws_url("mykey", chain_id).unwrap();
            let expected_http = format!("https://{network}.g.alchemy.com/v2/mykey");
            let expected_ws = format!("wss://{network}.g.alchemy.com/ws/v2/mykey");

            assert_eq!(http_url.as_str(), expected_http);
            assert_eq!(ws_url.as_str(), expected_ws);
        }
    }

    #[test]
    fn alchemy_unsupported_chain_fails() {
        let result = Provider::Alchemy.http_url("key", 999);
        assert_eq!(
            result,
            Err(ProviderUrlError::UnsupportedChain {
                provider: "alchemy",
                chain_id: 999
            })
        );
    }

    // ==================== URL Generation - QuickNode ====================

    #[test]
    fn quicknode_http_url_mainnet() {
        let url = Provider::QuickNode.http_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "https://eth-mainnet.quiknode.pro/mykey");
    }

    #[test]
    fn quicknode_ws_url_mainnet() {
        let url = Provider::QuickNode.ws_url("mykey", 1).unwrap();
        assert_eq!(url.as_str(), "wss://eth-mainnet.quiknode.pro/mykey");
    }

    #[test]
    fn quicknode_http_url_base() {
        let url = Provider::QuickNode.http_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "https://base-mainnet.quiknode.pro/mykey");
    }

    #[test]
    fn quicknode_ws_url_base() {
        let url = Provider::QuickNode.ws_url("mykey", 8453).unwrap();
        assert_eq!(url.as_str(), "wss://base-mainnet.quiknode.pro/mykey");
    }

    #[test]
    fn quicknode_supports_tier1_and_tier2_chains() {
        let cases = [
            (CHAIN_ETHEREUM, "eth-mainnet"),
            (CHAIN_ARBITRUM, "arbitrum-mainnet"),
            (CHAIN_BASE, "base-mainnet"),
            (CHAIN_OPTIMISM, "optimism"),
            (CHAIN_POLYGON, "matic"),
            (CHAIN_BNB_SMART_CHAIN, "bsc"),
        ];

        for (chain_id, network) in cases {
            let http_url = Provider::QuickNode.http_url("mykey", chain_id).unwrap();
            let ws_url = Provider::QuickNode.ws_url("mykey", chain_id).unwrap();
            let expected_http = format!("https://{network}.quiknode.pro/mykey");
            let expected_ws = format!("wss://{network}.quiknode.pro/mykey");

            assert_eq!(http_url.as_str(), expected_http);
            assert_eq!(ws_url.as_str(), expected_ws);
        }
    }

    #[test]
    fn quicknode_unsupported_chain_fails() {
        let result = Provider::QuickNode.http_url("key", 999);
        assert_eq!(
            result,
            Err(ProviderUrlError::UnsupportedChain {
                provider: "quicknode",
                chain_id: 999
            })
        );
    }

    // ==================== Enum properties ====================

    #[test]
    fn provider_is_copy() {
        let p1 = Provider::Infura;
        let p2 = p1;
        assert_eq!(p1, p2);
    }

    #[test]
    fn provider_is_hashable() {
        use std::collections::HashSet;
        let mut set = HashSet::new();
        set.insert(Provider::Infura);
        set.insert(Provider::Ankr);
        assert_eq!(set.len(), 2);
    }
}
