use anyhow::Result;
use clap::Subcommand;
use serde_json::Value;
use std::time::Duration;

use crate::client;
use crate::config::ResolvedConfig;
use crate::output::watch as out;
use crate::output::OutputFormat;
use crate::retry::{self, RetryConfig};

#[derive(Debug, Subcommand)]
pub enum WatchCommand {
    /// Stream position updates
    Positions {
        /// Poll interval in seconds
        #[arg(long, default_value = "5")]
        interval: u64,
        /// Filter by symbol
        #[arg(long)]
        symbol: Option<String>,
        /// Only emit when data changes
        #[arg(long)]
        changes_only: bool,
        /// Exit after N events
        #[arg(long)]
        count: Option<u64>,
    },

    /// Stream open order updates
    Orders {
        /// Poll interval in seconds
        #[arg(long, default_value = "5")]
        interval: u64,
        /// Filter by symbol
        #[arg(long)]
        symbol: Option<String>,
        /// Only emit when data changes
        #[arg(long)]
        changes_only: bool,
        /// Exit after N events
        #[arg(long)]
        count: Option<u64>,
    },

    /// Stream balance updates
    Balances {
        /// Poll interval in seconds
        #[arg(long, default_value = "5")]
        interval: u64,
        /// Filter by symbol (asset)
        #[arg(long)]
        symbol: Option<String>,
        /// Only emit when data changes
        #[arg(long)]
        changes_only: bool,
        /// Exit after N events
        #[arg(long)]
        count: Option<u64>,
    },

    /// Stream funding rate and mark price updates
    Funding {
        /// Poll interval in seconds
        #[arg(long, default_value = "5")]
        interval: u64,
        /// Filter by symbol
        #[arg(long)]
        symbol: Option<String>,
        /// Only emit when data changes
        #[arg(long)]
        changes_only: bool,
        /// Exit after N events
        #[arg(long)]
        count: Option<u64>,
    },

    /// Stream all account data (positions, orders, balances, funding)
    All {
        /// Poll interval in seconds
        #[arg(long, default_value = "5")]
        interval: u64,
        /// Filter by symbol
        #[arg(long)]
        symbol: Option<String>,
        /// Only emit when data changes
        #[arg(long)]
        changes_only: bool,
        /// Exit after N events
        #[arg(long)]
        count: Option<u64>,
    },
}

impl WatchCommand {
    fn interval(&self) -> u64 {
        match self {
            Self::Positions { interval, .. }
            | Self::Orders { interval, .. }
            | Self::Balances { interval, .. }
            | Self::Funding { interval, .. }
            | Self::All { interval, .. } => *interval,
        }
    }

    fn symbol(&self) -> Option<&str> {
        match self {
            Self::Positions { symbol, .. }
            | Self::Orders { symbol, .. }
            | Self::Balances { symbol, .. }
            | Self::Funding { symbol, .. }
            | Self::All { symbol, .. } => symbol.as_deref(),
        }
    }

    fn changes_only(&self) -> bool {
        match self {
            Self::Positions { changes_only, .. }
            | Self::Orders { changes_only, .. }
            | Self::Balances { changes_only, .. }
            | Self::Funding { changes_only, .. }
            | Self::All { changes_only, .. } => *changes_only,
        }
    }

    fn count(&self) -> Option<u64> {
        match self {
            Self::Positions { count, .. }
            | Self::Orders { count, .. }
            | Self::Balances { count, .. }
            | Self::Funding { count, .. }
            | Self::All { count, .. } => *count,
        }
    }

    fn targets(&self) -> &[&str] {
        match self {
            Self::Positions { .. } => &["positions"],
            Self::Orders { .. } => &["orders"],
            Self::Balances { .. } => &["balances"],
            Self::Funding { .. } => &["funding"],
            Self::All { .. } => &["positions", "orders", "balances", "funding"],
        }
    }
}

pub async fn run(
    cmd: &WatchCommand,
    config: &ResolvedConfig,
    secret: Option<&str>,
    base_url: Option<&str>,
    _format: OutputFormat, // always NDJSON
    retry_config: &RetryConfig,
) -> Result<()> {
    let c = client::authenticated_client(config, secret, base_url)?;
    let interval = Duration::from_secs(cmd.interval());
    let changes_only = cmd.changes_only();
    let max_count = cmd.count();
    let targets = cmd.targets();
    let symbol = cmd.symbol().map(|s| s.to_uppercase());

    let mut sequence: u64 = 0;
    let mut emitted: u64 = 0;
    let mut previous_states: std::collections::HashMap<String, Value> =
        std::collections::HashMap::new();

    loop {
        for &target in targets {
            let result = fetch_target(&c, target, symbol.as_deref(), retry_config).await;

            match result {
                Ok(data) => {
                    sequence += 1;
                    let prev = previous_states.get(target);
                    let changed = prev != Some(&data);

                    if changed {
                        if prev.is_some() {
                            out::emit_change(target, sequence, data.clone());
                        } else {
                            out::emit_event("snapshot", Some(target), sequence, Some(data.clone()));
                        }
                        previous_states.insert(target.to_string(), data);
                        emitted += 1;
                    } else if !changes_only {
                        out::emit_heartbeat(sequence);
                        emitted += 1;
                    }
                }
                Err(e) => {
                    sequence += 1;
                    out::emit_error(sequence, &e.to_string());
                    emitted += 1;
                }
            }

            if let Some(max) = max_count {
                if emitted >= max {
                    sequence += 1;
                    out::emit_shutdown(sequence);
                    return Ok(());
                }
            }
        }

        // Wait for the interval or ctrl-c
        tokio::select! {
            _ = tokio::time::sleep(interval) => {}
            _ = tokio::signal::ctrl_c() => {
                sequence += 1;
                out::emit_shutdown(sequence);
                return Ok(());
            }
        }
    }
}

async fn fetch_target(
    c: &bpx_api_client::BpxClient,
    target: &str,
    symbol: Option<&str>,
    retry_config: &RetryConfig,
) -> Result<Value> {
    match target {
        "positions" => {
            let positions = retry::with_retry(retry_config, true, || {
                let client = c;
                async move {
                    client
                        .get_open_future_positions()
                        .await
                        .map_err(|e| anyhow::anyhow!("{e}"))
                }
            })
            .await?;
            let filtered: Vec<_> = if let Some(sym) = symbol {
                positions.into_iter().filter(|p| p.symbol == sym).collect()
            } else {
                positions
            };
            Ok(serde_json::to_value(filtered)?)
        }
        "orders" => {
            let sym = symbol.map(|s| s.to_string());
            let orders = retry::with_retry(retry_config, true, || {
                let client = c;
                let s = sym.as_deref();
                async move {
                    client
                        .get_open_orders(s)
                        .await
                        .map_err(|e| anyhow::anyhow!("{e}"))
                }
            })
            .await?;
            Ok(serde_json::to_value(orders)?)
        }
        "balances" => {
            let balances = retry::with_retry(retry_config, true, || {
                let client = c;
                async move {
                    client
                        .get_balances()
                        .await
                        .map_err(|e| anyhow::anyhow!("{e}"))
                }
            })
            .await?;
            if let Some(sym) = symbol {
                let filtered: Vec<_> = balances.into_iter().filter(|b| b.0 == sym).collect();
                Ok(serde_json::to_value(filtered)?)
            } else {
                Ok(serde_json::to_value(balances)?)
            }
        }
        "funding" => {
            let prices = retry::with_retry(retry_config, true, || {
                let client = c;
                async move {
                    client
                        .get_all_mark_prices()
                        .await
                        .map_err(|e| anyhow::anyhow!("{e}"))
                }
            })
            .await?;
            if let Some(sym) = symbol {
                let filtered: Vec<_> = prices.into_iter().filter(|p| p.symbol == sym).collect();
                Ok(serde_json::to_value(filtered)?)
            } else {
                Ok(serde_json::to_value(prices)?)
            }
        }
        _ => anyhow::bail!("Unknown watch target: {target}"),
    }
}
