use std::fs::read;

use cached::proc_macro::once;
use clap::{Parser, Subcommand};
use serde::Deserialize;
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_signer::Signer;

#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about=None)]
pub struct Config {
    /// The signer to sign transactions with
    #[arg(long, env, global = true)]
    signer: Option<String>,

    /// The RPC URL to use
    #[arg(long = "rpc", env, global = true)]
    rpc_url: Option<String>,

    /// The commitment level to use
    #[arg(long, env, global = true)]
    commitment: Option<CommitmentLevel>,

    /// Whether to disable stdout decoration
    #[arg(long, env, global = true, default_value = "false")]
    pub no_decoration: bool,

    /// Whether to enable debug logging
    #[arg(long, env, global = true, default_value = "false")]
    pub debug: bool,

    /// Whether to skip simulation
    #[arg(long, env, global = true, default_value = "false")]
    pub skip_simulation: bool,

    /// Whether to skip confirmation prompts
    #[arg(long, env, global = true, default_value = "false")]
    pub yes: bool,

    /// The command to execute
    #[command(subcommand)]
    pub command: Command,
}

#[derive(Subcommand, Debug, Clone)]
pub enum Command {
    /// Print the version of the CLI
    Version,

    /// Commands to interact with a program
    #[command(group = clap::ArgGroup::new("amount").required(true).multiple(false))]
    Swap {
        /// Market account address
        #[arg(long)]
        market: Pubkey,
        /// Amount of token to send (ExactIn)
        #[arg(long, group = "amount")]
        amount_in: Option<u64>,
        /// Amount of token to receive (ExactOut)
        #[arg(long, group = "amount")]
        amount_out: Option<u64>,
        /// Whether the specified amount is token A
        #[arg(long)]
        amount_is_token_a: bool,
    },

    /// Commands to interact with a strategy
    Market {
        /// The subcommand to execute
        #[command(subcommand)]
        command: MarketCommand,
    },

    #[command(hide = true)]
    Oracle {
        /// The subcommand to execute
        #[command(subcommand)]
        command: OracleCommand,
    },
}

#[derive(Subcommand, Debug, Clone)]
pub enum MarketCommand {
    /// List all market accounts
    List,

    /// Initialize a new strategy
    Init {
        /// Oracle account address for token A
        #[arg(long)]
        mint_a: Pubkey,

        /// Oracle account address for token B
        #[arg(long)]
        mint_b: Pubkey,
    },

    /// Deposit tokens into a market
    Deposit {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Amount of token A to deposit
        #[arg(long)]
        amount_a: u64,

        /// Amount of token B to deposit
        #[arg(long)]
        amount_b: u64,
    },

    /// Withdraw tokens from a market
    Withdraw {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Amount of token A to withdraw
        #[arg(long)]
        amount_a: u64,

        /// Amount of token B to withdraw
        #[arg(long)]
        amount_b: u64,
    },

    /// Update the authority of a market
    Set {
        /// The subcommand to execute
        #[command(subcommand)]
        command: MarketSetCommand,
    },

    /// Close a market
    Close {
        /// Market account address
        #[arg(long)]
        market: Pubkey,
    },
}

#[derive(Subcommand, Debug, Clone)]
pub enum MarketSetCommand {
    Authority {
        /// Market account address
        #[arg(long)]
        market: Pubkey,
        /// Authority account address
        #[arg(long)]
        new_authority: Pubkey,
    },
    Updater {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Updater account address
        #[arg(long)]
        new_updater: Pubkey,
    },
    MaxInventoryImbalancePerCent {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Max inventory imbalance guard per cent
        #[arg(long)]
        new_max_inventory_imbalance_guard_per_cent: u8,
    },
    MinSpreadPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Min spread guard per M
        #[arg(long)]
        new_min_spread_guard_per_m: i32,
    },
    ArbPenaltyPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Arb penalty per M
        #[arg(long)]
        new_arb_penalty_per_m: u32,
    },
    JitodontfrontPenaltyPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Jitodontfront penalty per M
        #[arg(long)]
        new_jitodontfront_penalty_per_m: u32,
    },
    MinOraclePrice {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Min oracle price guard
        #[arg(long)]
        new_min_oracle_price_guard: u128,
    },
    MaxOraclePrice {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Max oracle price guard
        #[arg(long)]
        new_max_oracle_price_guard: u128,
    },
    SkewCliffMinPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Lower skew cliff in per_m. Must be ≤ 0 and > -1_000_000.
        /// Set both cliffs to 0 to disable the dead band.
        #[arg(long, allow_negative_numbers = true)]
        new_skew_cliff_min_per_m: i32,
    },
    SkewCliffMaxPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Upper skew cliff in per_m. Must be ≥ 0 and < 1_000_000.
        /// Set both cliffs to 0 to disable the dead band.
        #[arg(long)]
        new_skew_cliff_max_per_m: i32,
    },
    MaxAInventoryPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Max A-side signed-imbalance threshold in per_m. 0 disables the cap.
        #[arg(long)]
        new_max_a_inventory_per_m: u32,
    },
    MaxBInventoryPer1M {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Max B-side signed-imbalance threshold in per_m. 0 disables the cap.
        #[arg(long)]
        new_max_b_inventory_per_m: u32,
    },
}

#[derive(Subcommand, Debug, Clone)]
pub enum OracleCommand {
    /// Update the oracle account data
    Update {
        /// Market account address
        #[arg(long)]
        market: Pubkey,

        /// Token price in B per A
        #[arg(long)]
        price: f64,
    },
}

#[once]
fn solana_config() -> SolanaConfig {
    let config_path = std::env::var("HOME").unwrap() + "/.config/solana/cli/config.yml";
    let config = std::fs::read_to_string(config_path).unwrap();
    let config: SolanaConfig = serde_yaml::from_str(&config).unwrap();
    config
}

#[once]
fn solana_config_keypair() -> String {
    let config = solana_config();
    let keypair_file = std::fs::read(config.keypair_path).unwrap();
    let keypair_bytes: Vec<u8> = serde_json::from_slice(&keypair_file).unwrap();
    bs58::encode(keypair_bytes).into_string()
}

impl Config {
    pub fn rpc_client(&self) -> RpcClient {
        let config = solana_config();
        let commitment = CommitmentConfig {
            commitment: self.commitment.unwrap_or(config.commitment),
        };
        if let Some(rpc_url) = &self.rpc_url {
            RpcClient::new_with_commitment(rpc_url.clone(), commitment)
        } else {
            RpcClient::new_with_commitment(config.json_rpc_url.clone(), commitment)
        }
    }

    pub fn keypair(&self) -> Option<Keypair> {
        if let Some(keypair) = &self.signer {
            let bytes = read(keypair)
                .ok()
                .and_then(|x| serde_json::from_slice::<Vec<u8>>(&x).ok())
                .map(|x| bs58::encode(x).into_string())
                .unwrap_or(keypair.clone());
            if bytes.len() > 44 {
                Some(Keypair::from_base58_string(&bytes))
            } else {
                None
            }
        } else {
            let bytes = solana_config_keypair();
            Some(Keypair::from_base58_string(&bytes))
        }
    }

    pub fn authority(&self, authority: &Pubkey) -> Option<Keypair> {
        self.keypair().filter(|x| x.pubkey() == *authority)
    }
}

#[derive(Debug, Clone, Deserialize)]
struct SolanaConfig {
    json_rpc_url: String,
    keypair_path: String,
    commitment: CommitmentLevel,
}
