use std::path::Path;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use chrono::Utc;
use serde_json::{json, Value};
use tokio::time::sleep;

use crate::adaptation::{
    apply_llm_profile_selection, apply_monitor_exit_adjustments, apply_regime_profile,
    effective_rules, profile_catalog,
};
use crate::agent::llm::{candidate_approved, OpenRouterClient, TraderLlmReview};
use crate::agent::paths::state_path;
use crate::agent::schedule::{
    self, should_run_monitor_review, should_run_overnight_digest, should_run_premarket_digest,
    should_use_web_research, AgentSession,
};
use crate::agent::state::{load_state, save_state, TraderState};
use crate::capital::{capital_check_to_json, compute_capital_check};
use crate::closure::process_closure_exits;
use crate::commands::scan_cmd::run_scan_inner;
use crate::config::TraderRuntime;
use crate::entry::{attempt_entry, EntryStatus};
use crate::journal;
use crate::market_ctx::MarketCtx;
use crate::learn::{
    adaptation_allowed, apply_rule_patches, build_learn_context, should_run_learn,
};
use crate::notify;
use crate::reconcile::reconcile_tick;
use crate::regime::detect_regime;
use crate::risk::{monitoring_metrics, update_drawdown};
use crate::rules::TraderRules;
use crate::sim::{compute_stats, snapshot_equity};
use crate::sources::{attach_feeds_to_context, fetch_feeds_for_phase};
use schwab_cli::notify::TelegramNotifier;

pub struct AgentRunOptions {
    pub once: bool,
}

struct TickOutcome {
    body: Value,
    next_sleep_seconds: u64,
    session: String,
}

pub async fn run_agent_loop(
    runtime: &TraderRuntime,
    rules_path: &Path,
    options: AgentRunOptions,
) -> Result<()> {
    let mut rules = TraderRules::load(rules_path)?;
    rules.log_validation_hints();
    let account = rules.primary_account()?.hash.clone();
    let api = runtime.build_api()?;
    let market = runtime.build_market_api()?;

    let mut state = load_state(rules_path, &rules.trader_id)?;

    let llm_client = if rules.llm.enabled {
        OpenRouterClient::from_env().ok()
    } else {
        None
    };

    let telegram = match notify::telegram_from_rules(&rules.notify) {
        Ok(t) => t,
        Err(err) => {
            tracing::warn!("telegram notifications disabled: {err:#}");
            None
        }
    };

    loop {
        state.tick_count += 1;
        state.last_tick = Some(Utc::now());
        let outcome = run_tick(
            runtime,
            rules_path,
            &mut rules,
            &account,
            &api,
            &market,
            &mut state,
            llm_client.as_ref(),
            telegram.as_ref(),
        )
        .await?;
        state.last_tick_result = Some(outcome.body.clone());
        save_state(rules_path, &state)?;
        let log_line = format!(
            "tick={} session={} open={} trades_today={} style={} dry_run={} simulate={}",
            state.tick_count,
            outcome.session,
            state.open_positions.len(),
            state.trades_today,
            rules.playbook.style,
            runtime.dry_run,
            runtime.simulate
        );
        let _ = crate::agent::paths::append_trader_log(rules_path, &log_line);
        runtime.emit(schwab_cli::output::ResponseEnvelope::ok(
            "trader agent tick",
            json!({
                "tick": state.tick_count,
                "session": outcome.session,
                "next_sleep_seconds": outcome.next_sleep_seconds,
                "summary": state.summary(),
                "tick_result": outcome.body,
            }),
        ));

        notify::notify_tick_summary(
            telegram.as_ref(),
            &rules,
            &outcome.session,
            state.open_positions.len(),
            state.trades_today,
        )
        .await;

        if options.once {
            break;
        }
        sleep(Duration::from_secs(outcome.next_sleep_seconds)).await;
    }

    Ok(())
}

async fn run_tick(
    runtime: &TraderRuntime,
    rules_path: &Path,
    rules: &mut TraderRules,
    account_hash: &str,
    api: &Arc<schwab_api::TraderApi>,
    market: &Arc<schwab_market_data::MarketDataApi>,
    state: &mut TraderState,
    llm_client: Option<&OpenRouterClient>,
    telegram: Option<&TelegramNotifier>,
) -> Result<TickOutcome> {
    state.reset_trades_day(&rules.schedule.timezone);
    let market_ctx = MarketCtx::for_rules(market.clone(), rules_path, rules);

    let transition =
        schedule::resolve_session(rules, state.last_session.as_deref());
    let session = transition.session;
    state.last_session = Some(session.as_str().to_string());

    let outcome = match session {
        AgentSession::Idle => {
            tick_idle(
                runtime, rules_path, rules, state, api, account_hash, &transition, telegram,
            )
            .await?
        }
        AgentSession::Overnight => {
            tick_overnight(
                runtime,
                rules_path,
                rules,
                state,
                api,
                account_hash,
                &market_ctx,
                llm_client,
                &transition,
                telegram,
            )
            .await?
        }
        AgentSession::Premarket => {
            tick_premarket(
                runtime,
                rules_path,
                rules,
                state,
                api,
                account_hash,
                &market_ctx,
                llm_client,
                &transition,
                telegram,
            )
            .await?
        }
        AgentSession::RegularHours => {
            tick_regular(
                runtime,
                rules_path,
                rules,
                state,
                api,
                account_hash,
                &market_ctx,
                llm_client,
                &transition,
                telegram,
            )
            .await?
        }
    };

    Ok(TickOutcome {
        body: outcome,
        next_sleep_seconds: transition.sleep_seconds,
        session: session.as_str().to_string(),
    })
}

async fn tick_idle(
    runtime: &TraderRuntime,
    rules_path: &Path,
    rules: &TraderRules,
    state: &mut TraderState,
    api: &Arc<schwab_api::TraderApi>,
    account_hash: &str,
    transition: &schedule::SessionTransition,
    _telegram: Option<&TelegramNotifier>,
) -> Result<Value> {
    let reconcile_report = reconcile_tick(
        runtime, rules_path, rules, state, api, account_hash,
    )
    .await?;

    Ok(json!({
        "session": "idle",
        "at_open": false,
        "market_clock": crate::market_session::market_clock_json(rules),
        "next_sleep_seconds": transition.sleep_seconds,
        "skipped": ["market closed — no LLM, no scan, no entries"],
        "reconcile_report": reconcile_report,
        "monitoring": monitoring_metrics(state, rules),
    }))
}

async fn tick_overnight(
    runtime: &TraderRuntime,
    rules_path: &Path,
    rules: &TraderRules,
    state: &mut TraderState,
    api: &Arc<schwab_api::TraderApi>,
    account_hash: &str,
    market: &MarketCtx,
    llm_client: Option<&OpenRouterClient>,
    transition: &schedule::SessionTransition,
    telegram: Option<&TelegramNotifier>,
) -> Result<Value> {
    let mut skipped = Vec::<String>::new();
    let reconcile_report = reconcile_tick(
        runtime, rules_path, rules, state, api, account_hash,
    )
    .await?;

    if rules.is_intraday() && !state.open_positions.is_empty() {
        let _ = process_closure_exits(
            runtime, rules_path, rules, state, api, market, account_hash,
        )
        .await?;
        skipped.push("intraday overnight flatten check".into());
    } else if state.open_positions.is_empty() {
        skipped.push("overnight — flat, no positions".into());
    } else {
        skipped.push(format!(
            "overnight — {} open position(s) (reconcile only)",
            state.open_positions.len()
        ));
    }

    let mut llm_summary = None;
    let digest_due =
        should_run_overnight_digest(state, &rules.schedule.overnight, Utc::now());

    if !digest_due {
        skipped.push(format!(
            "overnight digest next in ~{} min",
            rules.schedule.overnight.tick_interval_seconds / 60
        ));
    } else if let Some(client) = llm_client {
        let context = llm_context_with_feeds(
            rules,
            "overnight_digest",
            json!({
                "phase": "overnight_digest",
                "playbook_style": rules.playbook.style,
                "open_positions": state.open_positions,
                "open_playbook": state.open_playbook,
                "entries_blocked": "market closed",
            }),
        )
        .await;
        match client
            .review(
                &rules.llm,
                "overnight_digest",
                &rules.llm.web_model,
                &context,
                true,
            )
            .await
        {
            Ok(review) => {
                state.last_overnight_digest_at = Some(Utc::now());
                state.open_playbook = Some(json!({
                    "market_commentary": review.market_commentary,
                    "web_insights": review.web_insights,
                    "candidates": review.candidates,
                    "at": Utc::now(),
                }));
                llm_summary = Some(serde_json::to_value(&review)?);
                state.last_llm_summary = llm_summary.clone();
                notify::notify_llm_alerts(telegram, rules, &review).await;
            }
            Err(err) => {
                skipped.push(format!("overnight digest failed: {err}"));
            }
        }
    } else if rules.llm.enabled {
        skipped.push("overnight digest skipped (no LLM client)".into());
    }

    Ok(json!({
        "session": "overnight",
        "at_open": false,
        "market_clock": crate::market_session::market_clock_json(rules),
        "next_sleep_seconds": transition.sleep_seconds,
        "skipped": skipped,
        "reconcile_report": reconcile_report,
        "llm": llm_summary,
        "monitoring": monitoring_metrics(state, rules),
    }))
}

async fn tick_premarket(
    runtime: &TraderRuntime,
    rules_path: &Path,
    rules: &TraderRules,
    state: &mut TraderState,
    api: &Arc<schwab_api::TraderApi>,
    account_hash: &str,
    market: &MarketCtx,
    llm_client: Option<&OpenRouterClient>,
    transition: &schedule::SessionTransition,
    telegram: Option<&TelegramNotifier>,
) -> Result<Value> {
    let mut skipped = Vec::<String>::new();
    let reconcile_report = reconcile_tick(
        runtime, rules_path, rules, state, api, account_hash,
    )
    .await?;

    let scan = run_scan_inner(market, rules, state).await?;
    skipped.push("premarket — no entries until regular session".into());

    let mut llm_summary = None;
    let digest_due = should_run_premarket_digest(state, &rules.schedule, Utc::now());
    let mins_to_open =
        crate::market_session::minutes_until_equity_open(Utc::now(), &rules.schedule.timezone);

    if !digest_due {
        skipped.push(format!(
            "premarket digest next soon ({} min to open)",
            mins_to_open
        ));
    } else if let Some(client) = llm_client {
        let context = llm_context_with_feeds(
            rules,
            "premarket_digest",
            json!({
                "phase": "premarket_digest",
                "minutes_to_open": mins_to_open,
                "playbook_style": rules.playbook.style,
                "scan": scan,
                "open_positions": state.open_positions,
                "open_playbook": state.open_playbook,
                "entries_blocked": crate::closure::entry_block_reason(rules),
            }),
        )
        .await;
        match client
            .review(
                &rules.llm,
                "premarket_digest",
                &rules.llm.web_model,
                &context,
                true,
            )
            .await
        {
            Ok(review) => {
                state.last_premarket_digest_at = Some(Utc::now());
                apply_web_picks(state, rules, &review);
                state.open_playbook = Some(json!({
                    "market_commentary": review.market_commentary,
                    "web_insights": review.web_insights,
                    "candidates": review.candidates,
                    "at": Utc::now(),
                    "minutes_to_open": mins_to_open,
                }));
                llm_summary = Some(serde_json::to_value(&review)?);
                state.last_llm_summary = llm_summary.clone();
                notify::notify_llm_alerts(telegram, rules, &review).await;
            }
            Err(err) => {
                skipped.push(format!("premarket digest failed: {err}"));
            }
        }
    } else if rules.llm.enabled {
        skipped.push("premarket digest skipped (no LLM client)".into());
    }

    Ok(json!({
        "session": "premarket",
        "at_open": false,
        "minutes_to_open": mins_to_open,
        "market_clock": crate::market_session::market_clock_json(rules),
        "next_sleep_seconds": transition.sleep_seconds,
        "skipped": skipped,
        "reconcile_report": reconcile_report,
        "scan": scan,
        "llm": llm_summary,
        "monitoring": monitoring_metrics(state, rules),
    }))
}

async fn tick_regular(
    runtime: &TraderRuntime,
    rules_path: &Path,
    rules: &mut TraderRules,
    state: &mut TraderState,
    api: &Arc<schwab_api::TraderApi>,
    account_hash: &str,
    market: &MarketCtx,
    llm_client: Option<&OpenRouterClient>,
    transition: &schedule::SessionTransition,
    telegram: Option<&TelegramNotifier>,
) -> Result<Value> {
    let at_open = transition.just_opened;
    state.regular_tick_count += 1;
    let profile_before = state.active_profile.clone();
    let halted_before = state.trading_halted_reason.clone();
    let prior_playbook = if at_open {
        state.open_playbook.clone()
    } else {
        None
    };

    if at_open {
        // Overnight digest text often says "market closed" — drop it at the open.
        state.open_playbook = None;
        notify::notify_at_open(telegram, rules, prior_playbook.as_ref()).await;
    }

    let reconcile_report = reconcile_tick(
        runtime, rules_path, rules, state, api, account_hash,
    )
    .await?;
    notify::notify_reconcile_report(telegram, rules, &reconcile_report).await;

    let drawdown = update_drawdown(state, rules);

    let regime = detect_regime(market, rules).await.unwrap_or_else(|err| {
        tracing::warn!("regime detection failed: {err}");
        crate::regime::RegimeSnapshot {
            class: "neutral".into(),
            benchmark_symbol: rules.adaptation.regime.benchmark_symbol.clone(),
            vix_symbol: rules.adaptation.regime.vix_symbol.clone(),
            benchmark_last: 0.0,
            vix: None,
            above_sma_50: false,
            above_sma_200: false,
            realized_vol_annualized_pct: 0.0,
            realized_vol_percentile: 50.0,
            recommended_profile: rules.adaptation.default_profile.clone(),
            signals: json!({}),
        }
    });
    apply_regime_profile(state, rules, &regime);
    let mut tick_rules = effective_rules(rules, state);

    let closure_exits = process_closure_exits(
        runtime, rules_path, &tick_rules, state, api, market, account_hash,
    )
    .await?;
    notify::notify_closure_exits(telegram, rules, &closure_exits, runtime.simulate).await;
    if runtime.simulate {
        snapshot_equity(state, &tick_rules);
    }

    let mut scan = run_scan_inner(market, &tick_rules, state).await?;
    let capital = compute_capital_check(
        api,
        &tick_rules,
        state,
        account_hash,
        None,
        None,
        runtime.simulate,
        Some(rules_path),
    )
    .await?;

    let has_candidates = scan
        .get("candidates")
        .and_then(|v| v.as_array())
        .is_some_and(|a| !a.is_empty());
    let has_positions = !state.open_positions.is_empty();

    let mut llm_review: Option<TraderLlmReview> = None;
    let mut llm_summary = None;
    let mut monitor_adjustments = Vec::<Value>::new();
    let mut llm_phase: Option<&str> = None;
    let mut web_picks_added = 0u32;

    if let Some(client) = llm_client {
        if let Some((phase, model, use_web)) =
            resolve_regular_llm_phase(&tick_rules, state, has_candidates, has_positions)
        {
            llm_phase = Some(phase);
            let context = llm_context_with_feeds(
                &tick_rules,
                phase,
                json!({
                    "phase": phase,
                    "session": "regular",
                    "regular_tick": state.regular_tick_count,
                    "at_open": at_open,
                    "market_clock": crate::market_session::market_clock_json(&tick_rules),
                    "playbook_style": tick_rules.playbook.style,
                    "adaptable_playbook": crate::learn::adaptable_playbook_snapshot(&tick_rules),
                    "profile_catalog": profile_catalog(rules),
                    "active_profile": state.active_profile,
                    "active_profile_reason": state.active_profile_reason,
                    "regime": regime.to_json(),
                    "overnight_digest_snapshot": state.open_playbook,
                    "capital_check": capital_check_to_json(&capital),
                    "scan": scan,
                    "open_positions": state.open_positions,
                    "sim_stats": compute_stats(state),
                    "closure_exits_this_tick": closure_exits,
                    "entry_block_reason": state.entry_block_reason(&tick_rules),
                    "entries_blocked": crate::closure::entry_block_reason(&tick_rules),
                }),
            )
            .await;
            match client.review(&tick_rules.llm, phase, model, &context, use_web).await {
                Ok(review) => {
                    web_picks_added = apply_web_picks(state, &tick_rules, &review);
                    if apply_llm_profile_selection(state, rules, &review) {
                        tick_rules = effective_rules(rules, state);
                    }
                    if phase == "monitor" {
                        monitor_adjustments = apply_monitor_exit_adjustments(
                            runtime,
                            rules_path,
                            &tick_rules,
                            state,
                            api,
                            market,
                            account_hash,
                            &review,
                        )
                        .await
                        .unwrap_or_else(|err| {
                            tracing::warn!("monitor exit adjustments failed: {err}");
                            vec![]
                        });
                    }
                    llm_summary = Some(serde_json::to_value(&review)?);
                    state.last_llm_summary = llm_summary.clone();
                    state.last_llm_review_tick = Some(state.regular_tick_count);
                    state.llm_review_count += 1;
                    notify::notify_llm_alerts(telegram, &tick_rules, &review).await;
                    llm_review = Some(review);
                }
                Err(err) => {
                    llm_summary = Some(json!({ "error": err.to_string() }));
                }
            }
        }
    }

    if web_picks_added > 0 {
        scan = run_scan_inner(market, &tick_rules, state).await?;
    }

    let mut entry_attempts = Vec::new();
    if capital.passed && state.entry_block_reason(&tick_rules).is_none() {
        if let Some(candidates) = scan.get("candidates").and_then(|v| v.as_array()) {
            for candidate in candidates {
                let symbol = candidate
                    .get("symbol")
                    .and_then(|v| v.as_str())
                    .unwrap_or_default();
                if symbol.is_empty() {
                    continue;
                }

                let llm_ok = match &llm_review {
                    Some(review) => candidate_approved(review, symbol, tick_rules.llm.veto_entries),
                    None if rules.llm.enabled && !runtime.dry_run && !runtime.simulate => false,
                    None if rules.llm.enabled && runtime.dry_run => true,
                    None => true,
                };
                if !llm_ok {
                    entry_attempts.push(json!({
                        "status": "skipped",
                        "symbol": symbol,
                        "reason": "llm_veto_or_missing_review",
                    }));
                    continue;
                }

                let attempt = attempt_entry(
                    runtime,
                    rules_path,
                    &tick_rules,
                    state,
                    api,
                    market,
                    account_hash,
                    symbol,
                    None,
                    None,
                    true,
                    "agent",
                    None,
                )
                .await?;

                let status = match attempt.status {
                    EntryStatus::DryRun => "dry_run",
                    EntryStatus::Simulated => "simulated",
                    EntryStatus::Filled => "filled",
                    EntryStatus::Submitted => "submitted",
                    EntryStatus::Skipped => "skipped",
                };
                let done = matches!(
                    attempt.status,
                    EntryStatus::DryRun
                        | EntryStatus::Simulated
                        | EntryStatus::Filled
                        | EntryStatus::Submitted
                );
                entry_attempts.push(json!({
                    "status": status,
                    "attempt": attempt,
                }));

                if done {
                    break;
                }
            }
        }
    }

    for attempt in &entry_attempts {
        notify::notify_entry_attempt(telegram, rules, attempt).await;
    }

    let mut learn_result = None;
    if rules.llm.enabled && should_run_learn(rules, state, false) {
        if let Some(client) = llm_client {
            let learn_ctx = build_learn_context(rules, state, rules_path, false)?;
            let learn_ctx = llm_context_with_feeds(rules, "learn", learn_ctx).await;
            match client
                .review(
                    &rules.llm,
                    "learn",
                    &rules.llm.learn_model,
                    &learn_ctx,
                    false,
                )
                .await
            {
                Ok(review) => {
                    let mut applied = Vec::new();
                    if !review.rule_patches.is_empty() {
                        if adaptation_allowed(runtime.dry_run, runtime.simulate, rules, false) {
                            match apply_rule_patches(rules, &review.rule_patches) {
                                Ok(p) => {
                                    applied = p;
                                    rules.save(rules_path)?;
                                }
                                Err(err) => {
                                    learn_result = Some(json!({
                                        "error": err.to_string(),
                                        "patches_rejected": review.rule_patches,
                                    }));
                                }
                            }
                        }
                        let _ = journal::append_event(
                            rules_path,
                            if applied.is_empty() {
                                "rule_patch_proposed"
                            } else {
                                "rule_auto_applied"
                            },
                            json!({
                                "patches": review.rule_patches,
                                "applied": applied,
                                "dry_run": runtime.dry_run,
                                "live_auto_apply": rules.adaptation.live_auto_apply,
                            }),
                        );
                        if !applied.is_empty() {
                            notify::notify_rule_adaptation(telegram, rules, applied.len()).await;
                            state.closed_trades_since_learn = 0;
                            state.last_learn_tick = Some(state.tick_count);
                        }
                    }
                    if learn_result.is_none() {
                        learn_result = Some(json!({
                            "review": review,
                            "applied": applied,
                        }));
                    }
                    state.last_learn_tick = Some(state.tick_count);
                }
                Err(err) => {
                    learn_result = Some(json!({ "error": err.to_string() }));
                }
            }
        }
    }

    if state.active_profile != profile_before {
        notify::notify_profile_change(
            telegram,
            rules,
            &profile_before,
            &state.active_profile,
            &state.active_profile_reason,
        )
        .await;
    }
    if state.trading_halted_reason != halted_before {
        if let Some(reason) = &state.trading_halted_reason {
            notify::notify_trading_halted(telegram, rules, reason).await;
        }
    }

    let tick_result = json!({
        "session": "regular",
        "at_open": at_open,
        "regular_tick": state.regular_tick_count,
        "tick": state.tick_count,
        "market_clock": crate::market_session::market_clock_json(&tick_rules),
        "next_sleep_seconds": transition.sleep_seconds,
        "reconcile_report": reconcile_report,
        "drawdown": drawdown,
        "regime": regime.to_json(),
        "active_profile": state.active_profile,
        "active_profile_source": state.active_profile_source,
        "active_profile_reason": state.active_profile_reason,
        "llm_phase": llm_phase,
        "monitor_adjustments": monitor_adjustments,
        "monitoring": monitoring_metrics(state, &tick_rules),
        "scan": scan,
        "capital_check": capital_check_to_json(&capital),
        "llm": llm_summary,
        "learn": learn_result,
        "entry_attempts": entry_attempts,
        "closure_exits": closure_exits,
        "entry_block_reason": state.entry_block_reason(&tick_rules),
        "entry_limits": {
            "trades_today": state.trades_today,
            "max_new_entries_per_day": tick_rules.playbook.entry.max_new_entries_per_day,
            "max_trades_per_day": tick_rules.risk.max_trades_per_day,
            "max_positions": tick_rules.playbook.entry.max_positions,
            "active_profile": state.active_profile,
        },
        "sim_stats": compute_stats(state),
        "dry_run": runtime.dry_run,
        "simulate": runtime.simulate,
        "playbook_style": tick_rules.playbook.style,
        "effective_playbook": crate::learn::adaptable_playbook_snapshot(&tick_rules),
        "market_cache": market.cache_status(),
        "state_path": state_path(rules_path),
    });

    if runtime.simulate {
        if state.active_profile != profile_before {
            let _ = journal::append_event(
                rules_path,
                "profile_changed",
                json!({
                    "from": profile_before,
                    "to": state.active_profile,
                    "source": state.active_profile_source,
                    "reason": state.active_profile_reason,
                    "regime": regime.to_json(),
                }),
            );
        }
        let _ = journal::append_event(rules_path, "sim_tick_summary", tick_result.clone());
    }

    Ok(tick_result)
}

async fn llm_context_with_feeds(rules: &TraderRules, phase: &str, context: Value) -> Value {
    let feeds = fetch_feeds_for_phase(rules, phase).await;
    attach_feeds_to_context(context, rules, phase, &feeds)
}

/// Returns (phase, model, use_web) when an LLM call is warranted during regular hours.
fn resolve_regular_llm_phase<'a>(
    rules: &'a TraderRules,
    state: &TraderState,
    has_candidates: bool,
    has_positions: bool,
) -> Option<(&'static str, &'a str, bool)> {
    if !rules.llm.enabled {
        return None;
    }

    let entry_blocked = state.entry_block_reason(rules);

    if has_candidates && entry_blocked.is_none() {
        let use_web = should_use_web_research(
            state.llm_review_count,
            rules.llm.web_research_every_reviews,
        ) && rules.sources.web.enabled
            && !rules.is_intraday();
        let model = if use_web {
            &rules.llm.web_model
        } else {
            &rules.llm.selection_model
        };
        let phase = if use_web { "web" } else { "selection" };
        return Some((phase, model, use_web));
    }

    if has_positions
        && should_run_monitor_review(
            state.regular_tick_count,
            state.last_llm_review_tick,
            rules.llm.review_every_ticks,
        )
    {
        return Some(("monitor", &rules.llm.monitor_model, false));
    }

    None
}

fn apply_web_picks(state: &mut TraderState, rules: &TraderRules, review: &TraderLlmReview) -> u32 {
    if !rules.watchlists.dynamic || !rules.sources.web.enabled {
        return 0;
    }
    state.reset_web_picks_day(&rules.schedule.timezone);
    let budget = rules.sources.web.pick_budget_per_day;
    let max_dynamic = rules.watchlists.max_dynamic_symbols as usize;
    let mut added = 0u32;

    for candidate in &review.candidates {
        if state.web_picks_today >= budget {
            break;
        }
        if !candidate.recommendation.eq_ignore_ascii_case("proceed") {
            continue;
        }
        let sym = candidate.symbol.trim().to_uppercase();
        if sym.is_empty()
            || rules.is_core_holding(&sym)
            || rules.is_blocked_symbol(&sym)
            || state.dynamic_watchlist.iter().any(|s| s == &sym)
        {
            continue;
        }
        if state.dynamic_watchlist.len() >= max_dynamic {
            break;
        }
        state.dynamic_watchlist.push(sym);
        state.web_picks_today += 1;
        added += 1;
    }

    if !review.candidates.is_empty() {
        state.last_web_picks = Some(json!({
            "candidates": review.candidates,
            "web_insights": review.web_insights,
        }));
    }
    added
}
