use anyhow::{anyhow, Result};
use config::Config;
use futures_util::{SinkExt, StreamExt};
use log::{debug, error, info, warn};
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    system_instruction,
    transaction::Transaction,
};
use spl_associated_token_account::get_associated_token_address;
use spl_token::instruction as token_instruction;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
use tokio::time;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};

const WSOL_MINT: &str = "So11111111111111111111111111111111111111112";

#[derive(Debug, Deserialize, Clone)]
pub struct BotConfig {
    pub rpc_url: String,
    pub helius_websocket_url: String,
    pub keypair_path: String,
    pub wsol_account: String,
    pub snipe_amount: f64,
    pub max_slippage: f64,
    pub priority_fee: u64,
    pub min_sol_balance: f64,
    pub min_wsol_balance: f64,
    pub helius_api_key: String,
    pub profit_target_percent: f64,
    pub stop_loss_percent: f64,
}

#[derive(Clone)]
pub struct Bot {
    config: BotConfig,
    rpc_client: Arc<RpcClient>,
    http_client: HttpClient,
    keypair: Arc<Keypair>,
    wsol_account: Pubkey,
    active_trades: Arc<Mutex<HashMap<String, Trade>>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Trade {
    token_address: String,
    token_mint: String,
    token_account: String,
    amount: u64,
    buy_price: f64,
    take_profit_price: f64,
    stop_loss_price: f64,
}

impl Bot {
    pub fn new(config_path: &str) -> Result<Self> {
        let settings = Config::builder()
            .add_source(config::File::with_name(config_path))
            .build()?;
        let config: BotConfig = settings.try_deserialize()?;

        // Validate essential configuration parameters
        if config.rpc_url.is_empty() {
            return Err(anyhow!("RPC URL is missing in configuration"));
        }
        if config.helius_websocket_url.is_empty() {
            return Err(anyhow!("Helius WebSocket URL is missing in configuration"));
        }
        if config.keypair_path.is_empty() {
            return Err(anyhow!("Keypair path is missing in configuration"));
        }
        if config.wsol_account.is_empty() {
            return Err(anyhow!("WSOL account is missing in configuration"));
        }
        if config.helius_api_key.is_empty() {
            return Err(anyhow!("Helius API key is missing in configuration"));
        }

        let rpc_client = Arc::new(RpcClient::new(config.rpc_url.clone()));
        let http_client = HttpClient::new();

        let keypair_json = std::fs::read_to_string(&config.keypair_path)?;
        let keypair_bytes: Vec<u8> = serde_json::from_str(&keypair_json)?;
        if keypair_bytes.len() != 64 {
            return Err(anyhow!("Invalid keypair length. Expected 64 bytes."));
        }
        let keypair = Arc::new(Keypair::from_bytes(&keypair_bytes)?);

        let wsol_account = Pubkey::from_str(&config.wsol_account)?;

        Ok(Self {
            config,
            rpc_client,
            http_client,
            keypair,
            wsol_account,
            active_trades: Arc::new(Mutex::new(HashMap::new())),
        })
    }

    pub async fn run(&self) -> Result<()> {
        info!("Starting Raydium Sniper Bot");
        self.ensure_balances().await?;

        loop {
            info!(
                "Connecting to WebSocket: {}",
                self.config.helius_websocket_url
            );
            match connect_async(&self.config.helius_websocket_url).await {
                Ok((ws_stream, _)) => {
                    let (mut write, mut read) = ws_stream.split();

                    let subscribe_message = serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": 1,
                        "method": "subscribePoolCreations",
                        "params": []
                    })
                    .to_string();

                    if let Err(e) = write.send(Message::Text(subscribe_message)).await {
                        error!("Failed to send subscribe message: {}", e);
                        continue;
                    }

                    info!("Subscribed to pool creations");

                    let mut interval = time::interval(Duration::from_secs(60));

                    loop {
                        tokio::select! {
                            Some(msg) = read.next() => {
                                match msg {
                                    Ok(Message::Text(text)) => {
                                        if let Ok(event) = serde_json::from_str::<serde_json::Value>(&text) {
                                            if let Err(e) = self.handle_event(event).await {
                                                error!("Error handling event: {}", e);
                                            }
                                        }
                                    }
                                    Ok(Message::Ping(_)) => {
                                        if let Err(e) = write.send(Message::Pong(vec![])).await {
                                            error!("Failed to send pong: {}", e);
                                            break;
                                        }
                                    }
                                    Err(e) => {
                                        warn!("WebSocket error: {}", e);
                                        break;
                                    }
                                    _ => {}
                                }
                            }
                            _ = interval.tick() => {
                                if let Err(e) = self.check_active_trades().await {
                                    warn!("Failed to check active trades: {}", e);
                                }
                                if let Err(e) = self.ensure_balances().await {
                                    warn!("Failed to ensure balances: {}", e);
                                }
                                // Send a ping to keep the connection alive
                                if let Err(e) = write.send(Message::Ping(vec![])).await {
                                    error!("Failed to send ping: {}", e);
                                    break;
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    error!("WebSocket connect error: {}", e);
                }
            }

            // Wait before attempting to reconnect with exponential backoff
            for i in 1..=5 {
                let wait_duration = Duration::from_secs(5 * i);
                info!("Reconnecting in {} seconds...", wait_duration.as_secs());
                time::sleep(wait_duration).await;
                match connect_async(&self.config.helius_websocket_url).await {
                    Ok(_) => break,
                    Err(e) => {
                        error!("Reconnection attempt {} failed: {}", i, e);
                        if i == 5 {
                            return Err(anyhow!("Failed to reconnect after multiple attempts"));
                        }
                    }
                }
            }
        }
    }

    async fn handle_event(&self, event: serde_json::Value) -> Result<()> {
        if let Some(params) = event.get("params") {
            let token_address = params
                .get("token_address")
                .and_then(|v| v.as_str())
                .ok_or_else(|| anyhow!("Token address not found in event"))?;

            let pool_address = params
                .get("pool_address")
                .and_then(|v| v.as_str())
                .ok_or_else(|| anyhow!("Pool address not found in event"))?;

            info!(
                "New pool detected: Token {}, Pool {}",
                token_address, pool_address
            );

            if self.is_valid_pool(token_address).await? {
                self.execute_trade(token_address).await?;
            }
        }

        Ok(())
    }

    async fn is_valid_pool(&self, token_address: &str) -> Result<bool> {
        let url = format!(
            "https://api.helius.xyz/v0/tokens/getAsset?api-key={}",
            self.config.helius_api_key
        );
        let response = self
            .http_client
            .post(&url)
            .json(&serde_json::json!({ "id": token_address }))
            .send()
            .await?
            .json::<serde_json::Value>()
            .await?;

        if let Some(result) = response.get("result") {
            let mutable = result
                .get("mutable")
                .and_then(|v| v.as_bool())
                .unwrap_or(true);
            let burnt = result
                .get("burnt")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let created_at = result
                .get("created_at")
                .and_then(|v| v.as_i64())
                .ok_or_else(|| anyhow!("Created time not found"))?;

            let is_raydium = result
                .get("grouping")
                .and_then(|g| g.as_array())
                .and_then(|arr| {
                    arr.iter().find(|item| {
                        item.get("group_key").and_then(|k| k.as_str()) == Some("dex")
                            && item.get("group_value").and_then(|v| v.as_str()) == Some("raydium")
                    })
                })
                .is_some();

            let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
            let time_since_creation = now - created_at;

            let is_valid = !mutable && burnt && is_raydium && time_since_creation <= 180; // 180 seconds = 3 minutes

            debug!(
                "Pool validation - Mutable: {}, Burnt: {}, Raydium: {}, Time since creation: {}s",
                !mutable, burnt, is_raydium, time_since_creation
            );

            Ok(is_valid)
        } else {
            Err(anyhow!("Failed to get asset information"))
        }
    }

    async fn execute_trade(&self, new_token_mint: &str) -> Result<()> {
        info!("Executing trade for token: {}", new_token_mint);
        let url = format!(
            "https://api.helius.xyz/v0/tokens/swap?api-key={}",
            self.config.helius_api_key
        );

        let snipe_amount_lamports = (self.config.snipe_amount * 1e9) as u64;

        // Create or get the associated token account for the new token
        let new_token_account = get_associated_token_address(
            &self.keypair.pubkey(),
            &Pubkey::from_str(new_token_mint)?,
        );

        let response = self
            .http_client
            .post(&url)
            .json(&serde_json::json!({
                "fromToken": WSOL_MINT,
                "fromTokenAccount": self.wsol_account.to_string(),
                "toToken": new_token_mint,
                "toTokenAccount": new_token_account.to_string(),
                "amount": snipe_amount_lamports,
                "slippage": self.config.max_slippage,
                "priorityFee": self.config.priority_fee
            }))
            .send()
            .await?
            .json::<serde_json::Value>()
            .await?;

        if let Some(signature) = response.get("signature").and_then(|v| v.as_str()) {
            info!("Trade executed successfully. Signature: {}", signature);

            let current_price = self.get_token_price(new_token_mint).await?;

            let take_profit_price =
                current_price * (1.0 + self.config.profit_target_percent / 100.0);
            let stop_loss_price = current_price * (1.0 - self.config.stop_loss_percent / 100.0);

            let trade = Trade {
                token_address: new_token_mint.to_string(),
                token_mint: new_token_mint.to_string(),
                token_account: new_token_account.to_string(),
                amount: snipe_amount_lamports,
                buy_price: current_price,
                take_profit_price,
                stop_loss_price,
            };

            {
                let mut active_trades = self.active_trades.lock().await;
                active_trades.insert(new_token_mint.to_string(), trade.clone());
            }

            info!(
                "Added trade to active trades. Take profit: {}, Stop loss: {}",
                take_profit_price, stop_loss_price
            );

            // Start a new task to check the price every second
            let bot_clone = self.clone();
            let new_token_mint_clone = new_token_mint.to_string();
            tokio::spawn(async move {
                let mut interval = time::interval(Duration::from_secs(1));
                loop {
                    interval.tick().await;
                    match bot_clone
                        .check_and_sell_if_needed(&new_token_mint_clone, &trade)
                        .await
                    {
                        Ok(true) => {
                            info!("Token {} sold successfully", new_token_mint_clone);
                            break;
                        }
                        Ok(false) => continue,
                        Err(e) => {
                            error!(
                                "Error checking price for token {}: {}",
                                new_token_mint_clone, e
                            );
                            break;
                        }
                    }
                }
            });

            Ok(())
        } else {
            Err(anyhow!("Failed to execute trade: {:?}", response))
        }
    }

    async fn check_and_sell_if_needed(&self, token_mint: &str, trade: &Trade) -> Result<bool> {
        let current_price = self.get_token_price(token_mint).await?;

        if current_price >= trade.take_profit_price || current_price <= trade.stop_loss_price {
            info!(
                "Executing sell for token {}. Current price: {}, Buy price: {}",
                token_mint, current_price, trade.buy_price
            );
            self.execute_sell(token_mint, trade.amount).await?;

            let mut active_trades = self.active_trades.lock().await;
            active_trades.remove(token_mint);

            Ok(true)
        } else {
            Ok(false)
        }
    }

    async fn check_active_trades(&self) -> Result<()> {
        let mut trades_to_remove = Vec::new();
        let active_trades = self.active_trades.lock().await.clone();

        for (token_address, trade) in active_trades.iter() {
            let current_price = self.get_token_price(token_address).await?;

            if current_price >= trade.take_profit_price || current_price <= trade.stop_loss_price {
                info!(
                    "Executing sell for token {}. Current price: {}, Buy price: {}",
                    token_address, current_price, trade.buy_price
                );
                self.execute_sell(token_address, trade.amount).await?;
                trades_to_remove.push(token_address.clone());
            }
        }

        if !trades_to_remove.is_empty() {
            let mut active_trades = self.active_trades.lock().await;
            for token_address in trades_to_remove {
                active_trades.remove(&token_address);
                info!("Removed trade for token {}", token_address);
            }
        }

        Ok(())
    }

    async fn execute_sell(&self, token_address: &str, amount: u64) -> Result<()> {
        info!("Executing sell for token: {}", token_address);
        let url = format!(
            "https://api.helius.xyz/v0/tokens/swap?api-key={}",
            self.config.helius_api_key
        );

        let associated_token_account =
            get_associated_token_address(&self.keypair.pubkey(), &Pubkey::from_str(token_address)?);

        let response = self
            .http_client
            .post(&url)
            .json(&serde_json::json!({
                "fromToken": token_address,
                "toToken": WSOL_MINT,
                "fromTokenAccount": associated_token_account.to_string(),
                "amount": amount,
                "slippage": self.config.max_slippage,
                "priorityFee": self.config.priority_fee
            }))
            .send()
            .await?
            .json::<serde_json::Value>()
            .await?;

        if let Some(signature) = response.get("signature").and_then(|v| v.as_str()) {
            info!("Sell executed successfully. Signature: {}", signature);
            Ok(())
        } else {
            Err(anyhow!("Failed to execute sell: {:?}", response))
        }
    }

    async fn get_token_price(&self, token_address: &str) -> Result<f64> {
        let url = format!(
            "https://api.helius.xyz/v0/token-metadata?api-key={}",
            self.config.helius_api_key
        );
        let response = self
            .http_client
            .post(&url)
            .json(&serde_json::json!({
                "mintAccounts": [token_address]
            }))
            .send()
            .await?
            .json::<Vec<serde_json::Value>>()
            .await?;

        if let Some(token_data) = response.first() {
            if let Some(price) = token_data.get("price").and_then(|p| p.as_f64()) {
                Ok(price)
            } else {
                Err(anyhow!("Price not found for token {}", token_address))
            }
        } else {
            Err(anyhow!("No data found for token {}", token_address))
        }
    }

    async fn ensure_balances(&self) -> Result<()> {
        let sol_balance = self.rpc_client.get_balance(&self.keypair.pubkey())?;
        let wsol_balance = self
            .rpc_client
            .get_token_account_balance(&self.wsol_account)?;

        let min_sol_lamports = (self.config.min_sol_balance * 1e9) as u64;
        let min_wsol_lamports = (self.config.min_wsol_balance * 1e9) as u64;

        if sol_balance < min_sol_lamports {
            warn!("SOL balance too low: {} SOL", sol_balance as f64 / 1e9);
        }

        let wsol_amount_lamports: u64 = wsol_balance.amount.parse()?;
        if wsol_amount_lamports < min_wsol_lamports {
            warn!(
                "WSOL balance too low: {} WSOL",
                wsol_amount_lamports as f64 / 1e9
            );
            let amount_to_wrap = min_wsol_lamports - wsol_amount_lamports;
            self.wrap_sol(amount_to_wrap).await?;
        }

        Ok(())
    }

    async fn wrap_sol(&self, amount: u64) -> Result<()> {
        info!("Wrapping {} SOL to WSOL", amount as f64 / 1e9);
        let instructions = vec![
            system_instruction::transfer(&self.keypair.pubkey(), &self.wsol_account, amount),
            token_instruction::sync_native(&spl_token::id(), &self.wsol_account)?,
        ];

        let recent_blockhash = self.rpc_client.get_latest_blockhash()?;
        let transaction = Transaction::new_signed_with_payer(
            &instructions,
            Some(&self.keypair.pubkey()),
            &[&self.keypair],
            recent_blockhash,
        );

        let signature = self
            .rpc_client
            .send_and_confirm_transaction_with_spinner(&transaction)?;
        info!("SOL wrapped successfully. Signature: {}", signature);
        Ok(())
    }

    async fn run_price_checker(&self) {
        let mut interval = time::interval(Duration::from_secs(60)); // Check every minute
        loop {
            interval.tick().await;
            if let Err(e) = self.check_active_trades().await {
                error!("Error checking active trades: {}", e);
            }
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

    let bot = Bot::new("config.toml")?;
    info!("Bot initialized with public key: {}", bot.keypair.pubkey());

    // Spawn the price checker task
    let price_checker_bot = bot.clone();
    tokio::spawn(async move {
        price_checker_bot.run_price_checker().await;
    });

    // Run the main bot loop
    bot.run().await
}