use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{default_cookies_path, print_json, resolve_cookies_path, Ctx};

#[derive(Subcommand)]
pub enum TradingViewAction {
    /// Fetch OHLCV bars for a symbol at a given resolution.
    ///
    /// Symbol format is `EXCHANGE:TICKER` (e.g. `TVC:DXY`, `BINANCE:BTCUSDT`,
    /// `NASDAQ:AAPL`). Resolution: `1` / `5` / `15` / `60` / `D` / `W` / `M`.
    History {
        /// Fully-qualified TradingView symbol, e.g. `TVC:DXY`.
        symbol: String,
        /// Bar resolution: `1`, `5`, `15`, `60`, `D`, `W`, `M`.
        #[arg(long, default_value = "D")]
        resolution: String,
        /// Maximum bars to return.
        #[arg(long, default_value_t = 300)]
        count: usize,
        /// Compute a technical indicator client-side from the bars.
        /// Format: `NAME[:input1,input2,...]`. Repeat for multiple.
        /// Supported: `RSI`, `EMA`, `SMA`, `MACD`, `BB`, `ATR`.
        /// Examples: `--indicator RSI:14 --indicator MACD:12,26,9 --indicator BB:20,2`.
        #[arg(long = "indicator", action = clap::ArgAction::Append)]
        indicators: Vec<String>,
    },
    /// Stream bars live: emits the initial `--count` historical bars
    /// as JSONL to stdout, then keeps the WebSocket open and emits
    /// every subsequent `timescale_update` from TradingView. Runs
    /// indefinitely until SIGINT or a `--max-updates` cutoff.
    ///
    /// For sub-daily resolutions the current in-progress bar is
    /// re-emitted as it updates — callers wanting "latest snapshot
    /// per timestamp" should dedup on `time`.
    Stream {
        /// Fully-qualified TradingView symbol, e.g. `TVC:DXY`.
        symbol: String,
        /// Bar resolution: `1`, `5`, `15`, `60`, `D`, `W`, `M`.
        #[arg(long, default_value = "1")]
        resolution: String,
        /// Initial historical bars to fetch before live updates.
        #[arg(long, default_value_t = 50)]
        count: usize,
        /// Exit after emitting this many bars total (0 = run forever).
        #[arg(long, default_value_t = 0)]
        max_updates: usize,
    },
    /// Single-symbol snapshot: last price, change %, bid/ask, session
    /// OHLC, volume, and metadata (exchange, currency, description).
    ///
    /// Note: guest-mode quotes are delayed 10-15 minutes for most
    /// exchanges. DXY and other indices are effectively real-time.
    Quote {
        /// Fully-qualified TradingView symbol, e.g. `TVC:DXY`.
        symbol: String,
    },
    /// Search TradingView's symbol directory.
    ///
    /// Returns matches from any exchange by default. Use `--exchange` to
    /// narrow (e.g. `BINANCE`, `NASDAQ`, `TVC`).
    Search {
        /// Free-text query — ticker or description fragment.
        query: String,
        /// Narrow to a specific exchange; empty = search all.
        #[arg(long, default_value = "")]
        exchange: String,
        /// Maximum results to return.
        #[arg(long, default_value_t = 25)]
        limit: usize,
    },
    /// Scrape popular community-published Pine Scripts.
    ///
    /// Walks `/scripts/` listing pages (sorted by popularity by default),
    /// extracts each script's PUB; id from its detail page, and
    /// downloads the source from pine-facade. Open-source entries are
    /// saved as `.pine` files under `--out/<category>/`; author-locked
    /// scripts are recorded in the catalog but no source is written.
    #[command(name = "pine-public")]
    PinePublic {
        /// Listing category: `default` (hot), `editors-picks`,
        /// `indicators`, `strategies`.
        #[arg(default_value = "default")]
        category: String,
        /// How many listing pages to walk (24 scripts per page).
        #[arg(long, default_value_t = 2)]
        pages: usize,
        /// First listing page number (1-based).
        #[arg(long, default_value_t = 1)]
        start_page: usize,
        /// Output root directory.
        #[arg(long, default_value = "./tradingview-public-pine")]
        out: std::path::PathBuf,
        /// Concurrent downloads. Each script makes 2 requests (the
        /// detail page + facade source) against two origins, so
        /// the default is conservative.
        #[arg(long, default_value_t = 4)]
        concurrency: usize,
        /// Politeness delay (ms) between per-script requests inside
        /// the semaphore. Adds up to a ~N×delay throttle at
        /// concurrency N.
        #[arg(long, default_value_t = 150)]
        delay_ms: u64,
        /// Re-use existing `.pine` files if present.
        #[arg(long)]
        skip_existing: bool,
    },
    /// Scrape TradingView's built-in Pine Script catalog.
    ///
    /// Hits `pine-facade.tradingview.com/pine-facade/list/?filter=all`
    /// to list every built-in study, then downloads each script's
    /// Pine source as a `.pine` file under `--out/<category>/`.
    /// Public sources (`scriptAccess: "open_no_auth"`) are saved;
    /// `closed_no_auth` third-party scripts get recorded in the
    /// catalog index but the source stays gated.
    #[command(name = "pine-scrape")]
    PineScrape {
        /// Which category: `technical` (RSI/MACD/...),
        /// `candlestick`, `fundamental`, `third-party`, `all`.
        #[arg(default_value = "technical")]
        category: String,
        /// Output root directory (a subdir per category is created).
        #[arg(long, default_value = "./tradingview-pine")]
        out: std::path::PathBuf,
        /// Concurrent downloads against the facade endpoint.
        #[arg(long, default_value_t = 6)]
        concurrency: usize,
        /// Politeness delay (ms) between per-script requests inside
        /// the semaphore.
        #[arg(long, default_value_t = 100)]
        delay_ms: u64,
        /// Re-use existing `.pine` files if present.
        #[arg(long)]
        skip_existing: bool,
        /// Cap downloads (useful for quick smoke tests).
        #[arg(long)]
        limit: Option<usize>,
    },
    /// Batch fetch history for many symbols, write to disk.
    ///
    /// Reads a symbol-per-line file (with `#` comments), fetches
    /// OHLCV for each concurrently, writes each to its own file
    /// under `--out` named `<exchange>_<ticker>.<ext>`.
    Batch {
        /// Path to the symbol list (one `EXCHANGE:TICKER` per line).
        symbols_file: std::path::PathBuf,
        /// Bar resolution: `1`, `5`, `15`, `60`, `D`, `W`, `M`.
        #[arg(long, default_value = "D")]
        resolution: String,
        /// Maximum bars per symbol (TradingView truncates to its
        /// catalog depth — 99999 grabs everything available).
        #[arg(long, default_value_t = 5000)]
        count: usize,
        /// Output directory (created if missing).
        #[arg(long, default_value = "./tradingview-data")]
        out: std::path::PathBuf,
        /// File format: `csv` (default), `json`, `jsonl`.
        #[arg(long, default_value = "csv")]
        format: String,
        /// Simultaneous WebSocket connections.
        #[arg(long, default_value_t = 4)]
        concurrency: usize,
        /// Skip symbols whose output file already exists.
        #[arg(long)]
        skip_existing: bool,
    },
    /// Fetch scheduled economic events + earnings releases.
    ///
    /// Dates are ISO 8601 (`YYYY-MM-DD`). Countries is a comma-separated
    /// list of 2-letter ISO codes (`US,EU,JP,CN`) — omit for all.
    #[command(name = "economic-calendar")]
    EconomicCalendar {
        /// ISO 8601 date (YYYY-MM-DD) for the window's start.
        #[arg(long)]
        from: String,
        /// ISO 8601 date for the window's end.
        #[arg(long)]
        to: String,
        /// Comma-separated ISO country codes. Empty = all.
        #[arg(long, default_value = "")]
        countries: String,
    },
    /// Fetch recent news headlines for a symbol.
    ///
    /// Returns id / title / source / published timestamp / urgency /
    /// related symbols / story URL for each headline. Article body
    /// requires a separate fetch (not yet implemented).
    News {
        /// Fully-qualified TradingView symbol, e.g. `NASDAQ:TSLA`.
        symbol: String,
        /// ISO language code (`en`, `zh-CN`, ...).
        #[arg(long, default_value = "en")]
        lang: String,
        /// Maximum headlines to return.
        #[arg(long, default_value_t = 25)]
        limit: usize,
    },
    /// Bulk symbol filtering via TradingView's screener API.
    ///
    /// Use a named preset (`us-large-caps`, `us-top-gainers`,
    /// `crypto-top-volume`) or pass `--query <path>` to load a custom
    /// filter JSON (full scan body as TradingView accepts it).
    Screener {
        /// Named preset: `us-large-caps`, `us-top-gainers`,
        /// `crypto-top-volume`. Ignored when `--query` is set.
        #[arg(default_value = "us-large-caps")]
        preset: String,
        /// Path to a JSON file containing the scanner body verbatim.
        /// Overrides `preset` when set.
        #[arg(long)]
        query: Option<std::path::PathBuf>,
        /// Which market to screen: `america` (US equities), `global`,
        /// `crypto`, `forex`, `bond`, `futures`.
        #[arg(long, default_value = "america")]
        market: String,
        /// Maximum rows to return.
        #[arg(long, default_value_t = 50)]
        limit: usize,
    },
}

pub async fn run(action: TradingViewAction, ctx: &Ctx) -> Result<(), TailFinError> {
    // If --cookies was passed, extract an authenticated token once and
    // use it for all protocol calls in this command. Guest token is the
    // fallback so the CLI works without login. --connect is ignored —
    // TradingView's WebSocket protocol is browser-independent.
    let auth_token = match ctx.cookies.as_deref() {
        Some(cookies_flag) => {
            let path = if cookies_flag == "auto" {
                default_cookies_path("tradingview")
            } else {
                resolve_cookies_path(cookies_flag, "tradingview")
            };
            eprintln!(
                "Using TradingView cookies from {} (authenticated mode)",
                path.display()
            );
            Some(
                tail_fin_tradingview::fetch_auth_token(&path)
                    .await
                    .map_err(|e| TailFinError::Api(format!("tradingview auth: {e}")))?,
            )
        }
        None => None,
    };
    let token = auth_token.as_deref().unwrap_or("unauthorized_user_token");

    match action {
        TradingViewAction::History {
            symbol,
            resolution,
            count,
            indicators,
        } => {
            let history =
                tail_fin_tradingview::fetch_history_with_auth(&symbol, &resolution, count, token)
                    .await
                    .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;

            if indicators.is_empty() {
                print_json(&history)?;
            } else {
                // Parse every --indicator spec up-front so bad syntax
                // fails before we compute anything.
                let specs: Vec<tail_fin_tradingview::IndicatorSpec> = indicators
                    .iter()
                    .map(|s| {
                        tail_fin_tradingview::parse_indicator_spec(s)
                            .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))
                    })
                    .collect::<Result<_, _>>()?;
                let computed: Vec<tail_fin_tradingview::IndicatorValues> = specs
                    .iter()
                    .map(|spec| {
                        tail_fin_tradingview::compute(&history.bars, spec)
                            .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))
                    })
                    .collect::<Result<_, _>>()?;
                let resp = tail_fin_tradingview::HistoryWithIndicators {
                    symbol: history.symbol,
                    resolution: history.resolution,
                    bars: history.bars,
                    indicators: computed,
                };
                print_json(&resp)?;
            }
        }
        TradingViewAction::Stream {
            symbol,
            resolution,
            count,
            max_updates,
        } => {
            let mut rx =
                tail_fin_tradingview::stream_history_with_auth(&symbol, &resolution, count, token)
                    .await
                    .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            let mut emitted = 0usize;
            while let Some(bar) = rx.recv().await {
                let line = serde_json::to_string(&bar).map_err(TailFinError::from)?;
                println!("{line}");
                emitted += 1;
                if max_updates > 0 && emitted >= max_updates {
                    break;
                }
            }
        }
        TradingViewAction::Quote { symbol } => {
            let q = tail_fin_tradingview::fetch_quote_with_auth(&symbol, token)
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&q)?;
        }
        TradingViewAction::PinePublic {
            category,
            pages,
            start_page,
            out,
            concurrency,
            delay_ms,
            skip_existing,
        } => {
            let cat = tail_fin_tradingview::PublicCategory::parse(&category)
                .map_err(|e| TailFinError::Api(format!("tradingview pine-public: {e}")))?;
            let summary =
                tail_fin_tradingview::scrape_public(tail_fin_tradingview::PublicScrapeOptions {
                    category: cat,
                    out_dir: &out,
                    pages,
                    start_page,
                    concurrency,
                    skip_existing,
                    delay_ms,
                })
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&summary)?;
        }
        TradingViewAction::PineScrape {
            category,
            out,
            concurrency,
            delay_ms,
            skip_existing,
            limit,
        } => {
            let cat = tail_fin_tradingview::PineCategory::parse(&category)
                .map_err(|e| TailFinError::Api(format!("tradingview pine-scrape: {e}")))?;
            let summary = tail_fin_tradingview::scrape_pine(tail_fin_tradingview::ScrapeOptions {
                category: cat,
                out_dir: &out,
                concurrency,
                skip_existing,
                limit,
                delay_ms,
            })
            .await
            .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&summary)?;
        }
        TradingViewAction::Batch {
            symbols_file,
            resolution,
            count,
            out,
            format,
            concurrency,
            skip_existing,
        } => {
            let format = tail_fin_tradingview::BatchFormat::parse(&format)
                .map_err(|e| TailFinError::Api(format!("tradingview batch: {e}")))?;
            let raw = std::fs::read_to_string(&symbols_file).map_err(|e| {
                TailFinError::Api(format!(
                    "tradingview batch: read {}: {e}",
                    symbols_file.display()
                ))
            })?;
            let symbols = tail_fin_tradingview::parse_symbols_file(&raw);
            if symbols.is_empty() {
                return Err(TailFinError::Api(format!(
                    "tradingview batch: no symbols found in {}",
                    symbols_file.display()
                )));
            }
            eprintln!("batch: {} symbols → {}", symbols.len(), out.display());
            let summary = tail_fin_tradingview::run_batch(
                symbols,
                tail_fin_tradingview::BatchOptions {
                    resolution: &resolution,
                    count,
                    out_dir: &out,
                    format,
                    concurrency,
                    skip_existing,
                    auth_token: token,
                },
            )
            .await
            .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&summary)?;
        }
        TradingViewAction::EconomicCalendar {
            from,
            to,
            countries,
        } => {
            // Normalize bare YYYY-MM-DD dates to full ISO 8601 that
            // the endpoint accepts — its parser rejects the short form.
            let from_iso = if from.contains('T') {
                from.clone()
            } else {
                format!("{from}T00:00:00.000Z")
            };
            let to_iso = if to.contains('T') {
                to.clone()
            } else {
                format!("{to}T23:59:59.000Z")
            };
            let feed = tail_fin_tradingview::fetch_calendar(&from_iso, &to_iso, &countries)
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&feed)?;
        }
        TradingViewAction::News {
            symbol,
            lang,
            limit,
        } => {
            let feed = tail_fin_tradingview::fetch_news(&symbol, &lang, limit)
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&feed)?;
        }
        TradingViewAction::Screener {
            preset,
            query,
            market,
            limit,
        } => {
            let body = if let Some(path) = query {
                tail_fin_tradingview::load_query(&path)
                    .map_err(|e| TailFinError::Api(format!("tradingview screener: {e}")))?
            } else {
                match preset.as_str() {
                    "us-large-caps" => tail_fin_tradingview::us_large_caps(limit),
                    "us-top-gainers" => tail_fin_tradingview::us_top_gainers(limit),
                    "crypto-top-volume" => tail_fin_tradingview::crypto_top_volume(limit),
                    other => {
                        return Err(TailFinError::Api(format!(
                            "tradingview screener: unknown preset '{other}' \
                             (known: us-large-caps, us-top-gainers, \
                             crypto-top-volume; or pass --query <path>)"
                        )));
                    }
                }
            };
            let resp = tail_fin_tradingview::screen(&market, &body)
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            print_json(&resp)?;
        }
        TradingViewAction::Search {
            query,
            exchange,
            limit,
        } => {
            let results = tail_fin_tradingview::search_symbols(&query, &exchange, limit)
                .await
                .map_err(|e| TailFinError::Api(format!("tradingview: {e}")))?;
            let count = results.len();
            print_json(&serde_json::json!({
                "query": query,
                "exchange": exchange,
                "count": count,
                "results": results,
            }))?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "tradingview"
    }

    fn about(&self) -> &'static str {
        "TradingView OHLCV data (WebSocket protocol, no browser required)"
    }

    fn command(&self) -> clap::Command {
        <TradingViewAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("tradingview").about(self.about()),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <TradingViewAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}
