use anyhow::{anyhow, Context, Result};
use headless_chrome::protocol::cdp::Page;
use headless_chrome::{Browser, Tab};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::io::{BufRead, BufReader, Cursor, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{mpsc, Arc, Mutex as StdMutex};
use std::time::{Duration, Instant};
use url::Url;

#[derive(Debug, Default, Clone)]
struct NbcTraceState {
    friendship_ready: bool,
    adobe_session_ready: bool,
    adobe_authorized: bool,
    short_authorized: bool,
    token_verified: bool,
    mt_session_ready: bool,
    background_login_complete: bool,
    media_activity_seen: bool,
    last_media_activity_at: Option<Instant>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NbcVideoState {
    title: String,
    page_url: String,
    document_ready_state: String,
    has_video: bool,
    current_time: f64,
    ready_state: i64,
    paused: bool,
    width: u64,
    height: u64,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NbcPageClues {
    body_text: String,
    ctas: Vec<String>,
    iframes: Vec<String>,
    inputs: Vec<String>,
    provider_candidates: Vec<String>,
    video_count: u64,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NbcAuthAdvanceResult {
    page_url: String,
    title: String,
    actions: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct BootstrapResult {
    pub title: String,
    pub page_url: String,
    pub interactive_auth_required: bool,
    pub authorized: bool,
    pub video_ready: bool,
    pub current_time: f64,
    pub width: u64,
    pub height: u64,
    pub screenshot_path: Option<PathBuf>,
}

struct WaitOutcome {
    tab: Arc<Tab>,
    state: NbcVideoState,
    trace: NbcTraceState,
    interactive_auth_required: bool,
    screenshot_path: Option<PathBuf>,
}

#[derive(Clone)]
struct BrowserTabState {
    tab: Arc<Tab>,
    state: NbcVideoState,
}

enum AuthMode {
    Forbidden,
    AllowInteractive { timeout: Duration },
}

pub struct FramePipeReader {
    rx: mpsc::Receiver<Vec<u8>>,
    current: Cursor<Vec<u8>>,
}

impl FramePipeReader {
    fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
        Self {
            rx,
            current: Cursor::new(Vec::new()),
        }
    }
}

impl Read for FramePipeReader {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        loop {
            let read = self.current.read(buf)?;
            if read > 0 {
                return Ok(read);
            }
            match self.rx.recv() {
                Ok(frame) => self.current = Cursor::new(frame),
                Err(_) => return Ok(0),
            }
        }
    }
}

// Keep Linux/Xvfb Chrome launch aligned with the manual forge smoke that survives
// under a virtual display; the headless_chrome-managed launcher crashes there.
const NBC_CHROME_BASE_ARGS: &[&str] = &[
    "--disable-background-networking",
    "--enable-features=NetworkService,NetworkServiceInProcess",
    "--disable-background-timer-throttling",
    "--disable-backgrounding-occluded-windows",
    "--disable-breakpad",
    "--disable-client-side-phishing-detection",
    "--disable-component-extensions-with-background-pages",
    "--disable-default-apps",
    "--disable-dev-shm-usage",
    "--disable-extensions",
    "--disable-features=TranslateUI,BlinkGenPropertyTrees,GlobalMediaControls",
    "--disable-hang-monitor",
    "--disable-ipc-flooding-protection",
    "--disable-popup-blocking",
    "--disable-prompt-on-repost",
    "--disable-renderer-backgrounding",
    "--disable-session-crashed-bubble",
    "--disable-sync",
    "--force-color-profile=srgb",
    "--hide-crash-restore-bubble",
    "--metrics-recording-only",
    "--enable-automation",
    "--password-store=basic",
    "--use-mock-keychain",
    "--autoplay-policy=no-user-gesture-required",
    "--mute-audio",
];

struct ChromeSession {
    browser: Browser,
    child: Child,
}

impl ChromeSession {
    fn browser(&self) -> &Browser {
        &self.browser
    }
}

impl Drop for ChromeSession {
    fn drop(&mut self) {
        let pid = self.child.id();
        tracing::info!(pid, "shutting down external Chrome for NBC worker");
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

pub fn nbc_capture_timeout() -> Duration {
    env::var("EVERY_CHANNEL_NBC_CAPTURE_TIMEOUT_SECS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_secs)
        .unwrap_or_else(|| Duration::from_secs(300))
}

pub fn nbc_capture_fps() -> u64 {
    env::var("EVERY_CHANNEL_NBC_CAPTURE_FPS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(6)
}

pub fn nbc_capture_quality() -> u32 {
    env::var("EVERY_CHANNEL_NBC_CAPTURE_QUALITY")
        .ok()
        .and_then(|value| value.parse::<u32>().ok())
        .map(|value| value.clamp(1, 100))
        .unwrap_or(80)
}

fn nbc_bootstrap_timeout() -> Duration {
    env::var("EVERY_CHANNEL_NBC_BOOTSTRAP_TIMEOUT_SECS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_secs)
        .unwrap_or_else(|| Duration::from_secs(1800))
}

fn nbc_profile_signin_gate_timeout() -> Duration {
    env::var("EVERY_CHANNEL_NBC_PROFILE_SIGNIN_GATE_TIMEOUT_SECS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .map(Duration::from_secs)
        .unwrap_or_else(|| Duration::from_secs(8))
}

fn nbc_env_flag(name: &str) -> Option<bool> {
    env::var(name).ok().map(|value| {
        let value = value.trim().to_ascii_lowercase();
        matches!(value.as_str(), "1" | "true" | "yes" | "on")
    })
}

fn nbc_disable_chrome_sandbox() -> bool {
    nbc_env_flag("EVERY_CHANNEL_NBC_NO_SANDBOX").unwrap_or(cfg!(target_os = "linux"))
}

fn nbc_enable_chrome_gpu() -> bool {
    nbc_env_flag("EVERY_CHANNEL_NBC_ENABLE_GPU").unwrap_or(!cfg!(target_os = "linux"))
}

fn nbc_proxy_server() -> Option<String> {
    env::var("EVERY_CHANNEL_NBC_PROXY_SERVER")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn nbc_proxy_bypass_list() -> Option<String> {
    env::var("EVERY_CHANNEL_NBC_PROXY_BYPASS_LIST")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn nbc_mvpd_provider_name() -> String {
    env::var("EVERY_CHANNEL_NBC_MVPD_PROVIDER")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "Verizon Fios".to_string())
}

fn nbc_secret_value(name: &str) -> Option<String> {
    let file_name = format!("{name}_FILE");
    if let Ok(path) = env::var(&file_name) {
        let path = PathBuf::from(path);
        match fs::read_to_string(&path) {
            Ok(value) => {
                let value = value.trim_end_matches(['\r', '\n']).to_string();
                if !value.trim().is_empty() {
                    return Some(value);
                }
            }
            Err(err) => {
                tracing::debug!(path = %path.display(), "failed to read NBC secret file: {err:#}")
            }
        }
    }

    env::var(name).ok().filter(|value| !value.trim().is_empty())
}

fn nbc_mvpd_username() -> Option<String> {
    nbc_secret_value("EVERY_CHANNEL_NBC_MVPD_USERNAME")
}

fn nbc_mvpd_password() -> Option<String> {
    nbc_secret_value("EVERY_CHANNEL_NBC_MVPD_PASSWORD")
}

pub fn resolve_nbc_chrome_path(override_path: Option<&Path>) -> Result<PathBuf> {
    if let Some(path) = override_path {
        if path.exists() {
            return Ok(path.to_path_buf());
        }
        return Err(anyhow!(
            "configured Chrome path does not exist: {}",
            path.display()
        ));
    }

    if let Ok(path) =
        env::var("EVERY_CHANNEL_NBC_CHROME_PATH").or_else(|_| env::var("EVERY_CHANNEL_CHROME_PATH"))
    {
        let path = PathBuf::from(path);
        if path.exists() {
            return Ok(path);
        }
        return Err(anyhow!(
            "configured Chrome path does not exist: {}",
            path.display()
        ));
    }

    let candidates = [
        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
        "/Applications/Chromium.app/Contents/MacOS/Chromium",
        "/run/current-system/sw/bin/google-chrome-stable",
        "/run/current-system/sw/bin/google-chrome",
        "/run/current-system/sw/bin/chromium",
        "/run/current-system/sw/bin/chromium-browser",
    ];
    for candidate in candidates {
        let path = PathBuf::from(candidate);
        if path.exists() {
            return Ok(path);
        }
    }

    for binary in [
        "google-chrome-stable",
        "google-chrome",
        "chromium",
        "chromium-browser",
    ] {
        let output = Command::new("bash")
            .arg("-lc")
            .arg(format!("command -v {binary}"))
            .output();
        if let Ok(output) = output {
            if output.status.success() {
                let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
                if !path.is_empty() {
                    return Ok(PathBuf::from(path));
                }
            }
        }
    }

    Err(anyhow!(
        "Chrome not found; set EVERY_CHANNEL_NBC_CHROME_PATH to a Google Chrome binary"
    ))
}

pub fn resolve_nbc_profile_dir(override_dir: Option<&Path>) -> Result<PathBuf> {
    if let Some(path) = override_dir {
        fs::create_dir_all(path).with_context(|| format!("failed to create {}", path.display()))?;
        return Ok(path.to_path_buf());
    }

    if let Ok(path) = env::var("EVERY_CHANNEL_NBC_PROFILE_DIR") {
        let path = PathBuf::from(path);
        fs::create_dir_all(&path)
            .with_context(|| format!("failed to create {}", path.display()))?;
        return Ok(path);
    }

    let base = env::var_os("XDG_STATE_HOME")
        .map(PathBuf::from)
        .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/state")))
        .unwrap_or_else(|| PathBuf::from(".cache"));
    let path = base.join("every-channel/nbc-chrome-profile");
    fs::create_dir_all(&path).with_context(|| format!("failed to create {}", path.display()))?;
    Ok(path)
}

fn cleanup_nbc_profile_lockfiles(profile_dir: &Path) {
    for name in ["SingletonLock", "SingletonCookie", "SingletonSocket"] {
        let path = profile_dir.join(name);
        if !path.exists() {
            continue;
        }
        match fs::remove_file(&path) {
            Ok(()) => tracing::info!(
                path = %path.display(),
                "removed stale Chrome profile lockfile before NBC launch"
            ),
            Err(err) if err.kind() == std::io::ErrorKind::IsADirectory => {
                if let Err(err) = fs::remove_dir_all(&path) {
                    tracing::debug!(
                        path = %path.display(),
                        "failed to remove stale Chrome profile lock directory: {err:#}"
                    );
                }
            }
            Err(err) => tracing::debug!(
                path = %path.display(),
                "failed to remove stale Chrome profile lockfile: {err:#}"
            ),
        }
    }
}

pub fn bootstrap_nbc_auth(
    chrome_path: PathBuf,
    profile_dir: PathBuf,
    url: String,
    screenshot_out: Option<PathBuf>,
) -> Result<BootstrapResult> {
    let chrome = launch_browser(chrome_path, profile_dir)?;
    let tab = chrome.browser().new_tab()?;
    let _ = tab.enable_stealth_mode();
    let trace = Arc::new(StdMutex::new(NbcTraceState::default()));
    register_nbc_trace_handlers(&tab, trace.clone())?;
    tab.navigate_to(&url)?;
    tab.wait_until_navigated()?;

    let outcome = wait_for_nbc_playback(
        chrome.browser(),
        &tab,
        &url,
        &trace,
        AuthMode::AllowInteractive {
            timeout: nbc_bootstrap_timeout(),
        },
        screenshot_out,
    )?;

    Ok(BootstrapResult {
        video_ready: nbc_video_state_has_decoded_frame(&outcome.state),
        current_time: outcome.state.current_time,
        width: outcome.state.width,
        height: outcome.state.height,
        title: outcome.state.title,
        page_url: outcome.state.page_url,
        interactive_auth_required: outcome.interactive_auth_required,
        authorized: nbc_trace_is_authorized(&outcome.trace),
        screenshot_path: outcome.screenshot_path,
    })
}

pub fn spawn_nbc_frame_reader(
    chrome_path: PathBuf,
    profile_dir: PathBuf,
    url: String,
) -> Result<FramePipeReader> {
    let (frame_tx, frame_rx) = mpsc::sync_channel(8);
    let (ready_tx, ready_rx) = mpsc::sync_channel(1);
    std::thread::spawn(move || {
        let result = run_nbc_capture_loop(chrome_path, profile_dir, url, frame_tx, ready_tx);
        if let Err(err) = result {
            tracing::warn!("NBC capture loop failed: {err:#}");
        }
    });

    match ready_rx.recv_timeout(nbc_capture_timeout()) {
        Ok(Ok(())) => Ok(FramePipeReader::new(frame_rx)),
        Ok(Err(err)) => Err(err),
        Err(_) => Err(anyhow!(
            "timed out waiting for the NBC browser session to become ready"
        )),
    }
}

fn reserve_devtools_port() -> Result<u16> {
    let listener = TcpListener::bind(("127.0.0.1", 0))
        .context("failed to reserve a local Chrome DevTools port")?;
    let port = listener
        .local_addr()
        .context("failed to read reserved Chrome DevTools port")?
        .port();
    drop(listener);
    Ok(port)
}

fn fetch_devtools_ws_url(port: u16) -> Result<Option<String>> {
    let mut stream = match TcpStream::connect(("127.0.0.1", port)) {
        Ok(stream) => stream,
        Err(_) => return Ok(None),
    };
    let _ = stream.set_read_timeout(Some(Duration::from_millis(500)));
    let _ = stream.set_write_timeout(Some(Duration::from_millis(500)));
    stream
        .write_all(
            format!(
                "GET /json/version HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n"
            )
            .as_bytes(),
        )
        .context("failed to query Chrome DevTools version endpoint")?;
    let mut response = Vec::new();
    let mut chunk = [0_u8; 4096];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) => break,
            Ok(read) => {
                response.extend_from_slice(&chunk[..read]);
                let text = String::from_utf8_lossy(&response);
                if let Some(payload) = text.split("\r\n\r\n").nth(1) {
                    if let Ok(value) = serde_json::from_str::<serde_json::Value>(payload.trim()) {
                        return Ok(value
                            .get("webSocketDebuggerUrl")
                            .and_then(|value| value.as_str())
                            .map(|value| normalize_devtools_ws_url(value, port))
                            .transpose()?);
                    }
                }
            }
            Err(err) if is_retryable_devtools_io(&err) => {
                break;
            }
            Err(err) => return Err(err).context("failed reading Chrome DevTools version response"),
        }
    }
    let text = String::from_utf8_lossy(&response);
    let payload = text.split("\r\n\r\n").nth(1).unwrap_or_default().trim();
    if payload.is_empty() {
        return Ok(None);
    }
    let value: serde_json::Value =
        serde_json::from_str(payload).context("failed to decode Chrome DevTools version JSON")?;
    Ok(value
        .get("webSocketDebuggerUrl")
        .and_then(|value| value.as_str())
        .map(|value| normalize_devtools_ws_url(value, port))
        .transpose()?)
}

fn is_retryable_devtools_io(err: &std::io::Error) -> bool {
    matches!(
        err.kind(),
        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
    ) || matches!(err.raw_os_error(), Some(11) | Some(35))
}

fn normalize_devtools_ws_url(url: &str, port: u16) -> Result<String> {
    let mut parsed = Url::parse(url).context("failed to parse Chrome DevTools websocket URL")?;
    if parsed.port().is_none() {
        parsed
            .set_port(Some(port))
            .map_err(|_| anyhow!("failed to restore Chrome DevTools websocket port"))?;
    }
    Ok(parsed.to_string())
}

fn wait_for_devtools_ws_url(child: &mut Child, port: u16) -> Result<String> {
    let deadline = Instant::now() + Duration::from_secs(15);
    loop {
        if let Some(status) = child
            .try_wait()
            .context("failed to poll Chrome process state")?
        {
            return Err(anyhow!(
                "Chrome exited with {status} before DevTools became ready"
            ));
        }

        let now = Instant::now();
        if now >= deadline {
            return Err(anyhow!("timed out waiting for Chrome DevTools websocket"));
        }
        if let Some(url) = fetch_devtools_ws_url(port)? {
            return Ok(url);
        }
        std::thread::sleep(
            deadline
                .checked_duration_since(now)
                .unwrap_or_else(|| Duration::from_millis(0))
                .min(Duration::from_millis(250)),
        );
    }
}

fn launch_browser(chrome_path: PathBuf, profile_dir: PathBuf) -> Result<ChromeSession> {
    let enable_gpu = nbc_enable_chrome_gpu();
    let debug_port = reserve_devtools_port()?;
    cleanup_nbc_profile_lockfiles(&profile_dir);
    let mut cmd = Command::new(&chrome_path);
    cmd.arg(format!("--remote-debugging-port={debug_port}"));
    cmd.arg("--verbose");
    cmd.arg("--log-level=0");
    cmd.arg("--no-first-run");
    cmd.arg(format!("--user-data-dir={}", profile_dir.display()));
    cmd.arg("--window-size=1440,960");
    cmd.arg("--ignore-certificate-errors");
    cmd.args(NBC_CHROME_BASE_ARGS);
    if let Some(proxy_server) = nbc_proxy_server() {
        cmd.arg(format!("--proxy-server={proxy_server}"));
        if let Some(proxy_bypass_list) = nbc_proxy_bypass_list() {
            cmd.arg(format!("--proxy-bypass-list={proxy_bypass_list}"));
        }
    }
    if nbc_disable_chrome_sandbox() {
        cmd.arg("--no-sandbox");
        cmd.arg("--disable-setuid-sandbox");
    }
    if !enable_gpu {
        cmd.arg("--disable-gpu");
        cmd.arg("--disable-software-rasterizer");
    }
    cmd.arg("about:blank");
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::piped());

    tracing::info!(binary = %chrome_path.display(), "launching external Chrome for NBC worker");
    let mut child = cmd
        .spawn()
        .with_context(|| format!("failed to spawn Chrome at {}", chrome_path.display()))?;
    tracing::info!(pid = child.id(), "started external Chrome for NBC worker");

    let stderr = child
        .stderr
        .take()
        .ok_or_else(|| anyhow!("Chrome stderr unavailable"))?;
    std::thread::spawn(move || {
        for line in BufReader::new(stderr).lines() {
            match line {
                Ok(line) => tracing::debug!(target: "ec_node::nbc.chrome", "{line}"),
                Err(err) => {
                    tracing::debug!("failed reading Chrome stderr: {err:#}");
                    break;
                }
            }
        }
    });

    let ws_url = wait_for_devtools_ws_url(&mut child, debug_port)?;
    let browser = Browser::connect_with_timeout(ws_url, nbc_capture_timeout())
        .context("failed to attach to Chrome DevTools for NBC worker")?;
    Ok(ChromeSession { browser, child })
}

fn run_nbc_capture_loop(
    chrome_path: PathBuf,
    profile_dir: PathBuf,
    url: String,
    frame_tx: mpsc::SyncSender<Vec<u8>>,
    ready_tx: mpsc::SyncSender<Result<()>>,
) -> Result<()> {
    let ready_tx_err = ready_tx.clone();
    let result: Result<()> = (|| {
        let chrome = launch_browser(chrome_path, profile_dir)?;
        let tab = chrome.browser().new_tab()?;
        let _ = tab.enable_stealth_mode();
        let trace = Arc::new(StdMutex::new(NbcTraceState::default()));
        register_nbc_trace_handlers(&tab, trace.clone())?;
        tab.navigate_to(&url)?;
        tab.wait_until_navigated()?;
        let outcome = wait_for_nbc_playback(
            chrome.browser(),
            &tab,
            &url,
            &trace,
            AuthMode::Forbidden,
            None,
        )?;
        let capture_tab = outcome.tab;

        let frame_interval = Duration::from_millis(1000 / nbc_capture_fps().max(1));
        let quality = nbc_capture_quality();
        let mut first_frame = true;

        loop {
            kick_nbc_player(&capture_tab).ok();
            let state = probe_nbc_video(&capture_tab).unwrap_or_default();
            if !nbc_video_state_has_decoded_frame(&state) {
                return Err(anyhow!(
                    "NBC capture tab lost decoded video (title='{}', page_url='{}', current_time={}, ready_state={}, has_video={})",
                    state.title,
                    state.page_url,
                    state.current_time,
                    state.ready_state,
                    state.has_video,
                ));
            }
            let video = capture_tab
                .find_element("video")
                .context("NBC capture tab has no video element after playback readiness")?;
            let frame = video.parent.capture_screenshot(
                Page::CaptureScreenshotFormatOption::Jpeg,
                Some(quality),
                Some(video.get_box_model()?.content_viewport()),
                true,
            )?;

            if first_frame {
                first_frame = false;
                let _ = ready_tx.send(Ok(()));
            }

            if frame_tx.send(frame).is_err() {
                break;
            }
            std::thread::sleep(frame_interval);
        }

        Ok(())
    })();

    if let Err(err) = &result {
        let _ = ready_tx_err.send(Err(anyhow!(err.to_string())));
    }

    result
}

fn register_nbc_trace_handlers(tab: &Arc<Tab>, trace: Arc<StdMutex<NbcTraceState>>) -> Result<()> {
    tab.register_response_handling(
        "nbc-trace",
        Box::new(move |event, fetch_body| {
            let url = event.response.url.clone();
            let status = event.response.status as u16;
            if !(200..400).contains(&status) {
                return;
            }

            let mut trace_state = match trace.lock() {
                Ok(guard) => guard,
                Err(_) => return,
            };

            if update_nbc_trace_from_url(&mut trace_state, &url) {
                return;
            }
            if url.contains(
                "tokenverifier.digitalsvc.apps.nbcuni.com/tokenverifier/entitlement/verifier",
            ) {
                if let Ok(body) = fetch_body() {
                    if !body.base_64_encoded && body.body.contains("\"valid\":true") {
                        trace_state.token_verified = true;
                    }
                }
            }
        }),
    )?;
    tab.register_loading_failed_handling(
        "nbc-trace-loading-failed",
        Box::new(move |response, failure| {
            tracing::warn!(
                url = %response.response.url,
                status = response.response.status,
                error_text = %failure.error_text,
                canceled = failure.canceled,
                "NBC request failed while waiting for playback"
            );
        }),
    )?;
    Ok(())
}

fn update_nbc_trace_from_url(trace_state: &mut NbcTraceState, url: &str) -> bool {
    let mut matched = false;
    if url.contains("friendship.nbc.com/v3/graphql") {
        trace_state.friendship_ready = true;
        matched = true;
    }
    if url.contains("sp.auth.adobe.com/adobe-services/session") {
        trace_state.adobe_session_ready = true;
        matched = true;
    }
    if url.contains("sp.auth.adobe.com/adobe-services/authorize") {
        trace_state.adobe_authorized = true;
        matched = true;
    }
    if url.contains("sp.auth.adobe.com/adobe-services/shortAuthorize") {
        trace_state.short_authorized = true;
        matched = true;
    }
    if url.contains("mt.ssai-oneapp.nbcuni.com/") {
        trace_state.mt_session_ready = true;
        matched = true;
    }
    if url.contains("entitlement.auth.adobe.com/entitlement/v4/completeBackgroundLogin.html") {
        trace_state.background_login_complete = true;
        matched = true;
    }
    if url.contains("cssott.com/")
        || url.contains("/Content/CMAF_")
        || url.contains(".m4s")
        || url.contains(".mpd")
        || url.contains("item_Segment")
    {
        trace_state.media_activity_seen = true;
        trace_state.last_media_activity_at = Some(Instant::now());
        matched = true;
    }
    matched
}

fn nbc_trace_is_authorized(trace_state: &NbcTraceState) -> bool {
    trace_state.token_verified
        || trace_state.short_authorized
        || trace_state.mt_session_ready
        || trace_state.adobe_authorized
        || trace_state.background_login_complete
}

fn nbc_trace_has_recent_media_activity(trace_state: &NbcTraceState) -> bool {
    trace_state
        .last_media_activity_at
        .map(|instant| instant.elapsed() <= Duration::from_secs(15))
        .unwrap_or(false)
}

fn nbc_url_is_background_login_complete(url: &str) -> bool {
    url.contains("completeBackgroundLogin.html")
}

fn nbc_url_is_provider_linked(url: &str) -> bool {
    let Ok(url) = Url::parse(url) else {
        return false;
    };
    let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
    let path = url.path().to_ascii_lowercase();
    (host.ends_with("nbc.com") || host.ends_with(".nbc.com")) && path.contains("provider-linked")
}

fn nbc_url_is_mvpd_complete(url: &str) -> bool {
    let Ok(url) = Url::parse(url) else {
        return false;
    };
    let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
    let path = url.path().to_ascii_lowercase();
    (host.ends_with("nbc.com") || host.ends_with(".nbc.com")) && path.contains("mvpd-complete")
}

fn nbc_url_is_optional_profile_signin(url: &str) -> bool {
    let Ok(url) = Url::parse(url) else {
        return false;
    };
    let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
    let path = url.path().to_ascii_lowercase();
    (host.ends_with("nbc.com") || host.ends_with(".nbc.com"))
        && (path.contains("sign-in") || path.contains("login"))
}

fn nbc_url_requires_interactive_auth(url: &str) -> bool {
    let Ok(url) = Url::parse(url) else {
        return false;
    };
    let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
    let path = url.path().to_ascii_lowercase();
    host.contains("auth.adobe.com") || host.contains("verizon") || path.contains("mvpd")
}

fn nbc_page_is_watch_surface(url: &str) -> bool {
    let Ok(url) = Url::parse(url) else {
        return false;
    };
    let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
    (host.ends_with("nbc.com") || host.ends_with(".nbc.com"))
        && !nbc_url_is_optional_profile_signin(url.as_str())
        && !nbc_url_is_provider_linked(url.as_str())
        && !nbc_url_is_mvpd_complete(url.as_str())
}

fn nbc_title_looks_like_verizon_popup(title: &str) -> bool {
    let title = title.to_ascii_lowercase();
    (title.contains("verizon") || title.contains("fios"))
        && (title.contains("sign in") || title.contains("signin") || title.contains("log in"))
}

fn nbc_title_looks_like_provider_linked(title: &str) -> bool {
    title.to_ascii_lowercase().contains("tv provider linked")
}

fn nbc_title_looks_like_optional_profile_signin(title: &str) -> bool {
    let title = title.to_ascii_lowercase();
    title.contains("nbc account sign in")
        || title.contains("nbcuniversal profile")
        || title.contains("nbc profile")
}

fn nbc_state_is_optional_profile_signin(state: &NbcVideoState) -> bool {
    nbc_url_is_optional_profile_signin(&state.page_url)
        || nbc_title_looks_like_optional_profile_signin(&state.title)
}

fn nbc_clues_look_geo_blocked(clues: &NbcPageClues) -> bool {
    let body_text = clues.body_text.to_ascii_lowercase();
    body_text.contains("not authorized to access this content from outside of the us")
        || body_text.contains("not authorized to access this content from outside of the u.s.")
        || body_text.contains("outside of the us and its territories")
        || body_text.contains("outside of the u.s. and its territories")
}

fn browser_tabs(browser: &Browser) -> Vec<Arc<Tab>> {
    browser.register_missing_tabs();
    browser.get_tabs().lock().unwrap().iter().cloned().collect()
}

fn register_nbc_trace_handlers_for_browser(
    browser: &Browser,
    trace: &Arc<StdMutex<NbcTraceState>>,
    tracked_tabs: &mut HashSet<String>,
) {
    for tab in browser_tabs(browser) {
        let target_id = tab.get_target_id().clone();
        if !tracked_tabs.insert(target_id.to_string()) {
            continue;
        }
        let _ = tab.enable_stealth_mode();
        if let Err(err) = register_nbc_trace_handlers(&tab, trace.clone()) {
            tracing::debug!(target_id = %target_id, "failed to register NBC trace handlers on tab: {err:#}");
        }
    }
}

fn collect_browser_tab_states(browser: &Browser) -> Vec<BrowserTabState> {
    browser_tabs(browser)
        .into_iter()
        .map(|tab| {
            let state = probe_nbc_video(&tab).unwrap_or_default();
            BrowserTabState { tab, state }
        })
        .collect()
}

fn find_primary_tab_state<'a>(
    tabs: &'a [BrowserTabState],
    primary_tab: &Arc<Tab>,
) -> Option<&'a BrowserTabState> {
    let target_id = primary_tab.get_target_id();
    tabs.iter()
        .find(|candidate| candidate.tab.get_target_id() == target_id)
}

fn nbc_video_state_has_decoded_frame(state: &NbcVideoState) -> bool {
    state.has_video
        && state.width > 0
        && state.height > 0
        && !state.paused
        && state.current_time > 0.0
        && state.ready_state >= 2
}

fn find_provider_linked_tab_state(tabs: &[BrowserTabState]) -> Option<&BrowserTabState> {
    tabs.iter().find(|candidate| {
        nbc_title_looks_like_provider_linked(&candidate.state.title)
            || nbc_url_is_provider_linked(&candidate.state.page_url)
            || nbc_url_is_mvpd_complete(&candidate.state.page_url)
    })
}

fn find_interaction_tab_state<'a>(
    tabs: &'a [BrowserTabState],
    primary_tab: &'a Arc<Tab>,
) -> Option<&'a BrowserTabState> {
    find_provider_linked_tab_state(tabs)
        .or_else(|| {
            tabs.iter().find(|candidate| {
                candidate.tab.get_target_id() != primary_tab.get_target_id()
                    && (nbc_title_looks_like_verizon_popup(&candidate.state.title)
                        || nbc_url_requires_interactive_auth(&candidate.state.page_url))
            })
        })
        .or_else(|| find_primary_tab_state(tabs, primary_tab))
}

fn close_auxiliary_browser_tabs(browser: &Browser, primary_tab: &Arc<Tab>) {
    for tab in browser_tabs(browser) {
        if tab.get_target_id() == primary_tab.get_target_id() {
            continue;
        }
        let _ = tab.close(false);
    }
}

fn kick_nbc_player(tab: &Arc<Tab>) -> Result<()> {
    let script = r#"
        (() => {
          const labels = [
            "watch",
            "watch live",
            "watch now",
            "watch live tv",
            "play",
            "resume",
            "continue",
            "start watching",
            "start watching now",
            "sign in",
            "link tv provider",
            "unlock",
          ];
          const lower = (value) => (value || "").trim().toLowerCase();
          const nodes = Array.from(document.querySelectorAll("button,a,[role='button']"));
          const candidate = nodes.find((node) => {
            const text = lower(node.innerText || node.textContent || "");
            return labels.some((label) => text === label || text.startsWith(label + " "));
          });
          if (candidate) candidate.click();
          const video = document.querySelector("video");
          if (!video) return false;
          video.muted = true;
          video.controls = false;
          if (typeof video.play === "function") {
            Promise.resolve(video.play()).catch(() => {});
          }
          return true;
        })();
    "#;
    let _ = tab.evaluate(script, true)?;
    Ok(())
}

fn advance_nbc_auth_flow(tab: &Arc<Tab>) -> Result<Option<NbcAuthAdvanceResult>> {
    let provider = nbc_mvpd_provider_name();
    let provider_json = serde_json::to_string(&provider)?;
    let provider_needles = serde_json::to_string(&{
        let mut needles = vec![provider.to_ascii_lowercase()];
        for token in provider
            .split(|ch: char| !ch.is_ascii_alphanumeric())
            .filter(|token| !token.is_empty())
        {
            let token = token.to_ascii_lowercase();
            if !needles.contains(&token) {
                needles.push(token);
            }
        }
        needles
    })?;
    let script = format!(
        r#"
        JSON.stringify((() => {{
          const provider = {provider_json};
          const providerNeedles = {provider_needles};
          const normalize = (value) => (value || "").replace(/\s+/g, " ").trim().toLowerCase();
          const visible = (node) => {{
            if (!node || typeof node.getBoundingClientRect !== "function") return false;
            const rect = node.getBoundingClientRect();
            const style = window.getComputedStyle(node);
            return rect.width > 0 &&
              rect.height > 0 &&
              style.display !== "none" &&
              style.visibility !== "hidden" &&
              style.opacity !== "0";
          }};
          const textOf = (node) => normalize([
            node.innerText || node.textContent || "",
            node.getAttribute?.("aria-label") || "",
            node.getAttribute?.("title") || "",
            node.getAttribute?.("name") || "",
            node.id || "",
            node.getAttribute?.("data-provider-name") || "",
          ].join(" "));
          const clickNode = (node, action) => {{
            if (!node || !visible(node)) return false;
            node.scrollIntoView?.({{ block: "center", inline: "center" }});
            node.dispatchEvent?.(new MouseEvent("mouseover", {{ bubbles: true, cancelable: true, view: window }}));
            node.dispatchEvent?.(new MouseEvent("mousedown", {{ bubbles: true, cancelable: true, view: window }}));
            node.dispatchEvent?.(new MouseEvent("mouseup", {{ bubbles: true, cancelable: true, view: window }}));
            node.click?.();
            actions.push(action);
            return true;
          }};
          const pressEnter = (node) => {{
            if (!node) return false;
            for (const type of ["keydown", "keypress", "keyup"]) {{
              node.dispatchEvent?.(new KeyboardEvent(type, {{
                key: "Enter",
                code: "Enter",
                which: 13,
                keyCode: 13,
                bubbles: true,
                cancelable: true,
              }}));
            }}
            actions.push("press:enter");
            return true;
          }};
          const setValue = (node, value) => {{
            if (!node) return false;
            const prototype = Object.getPrototypeOf(node);
            const descriptor = prototype && Object.getOwnPropertyDescriptor(prototype, "value");
            if (descriptor && descriptor.set) {{
              descriptor.set.call(node, value);
            }} else {{
              node.value = value;
            }}
            node.focus?.();
            node.dispatchEvent(new Event("input", {{ bubbles: true }}));
            node.dispatchEvent(new Event("change", {{ bubbles: true }}));
            actions.push(`typed:${{value}}`);
            return true;
          }};
          const actions = [];
          const url = window.location.href || "";
          let host = "";
          try {{
            host = new URL(url).hostname.toLowerCase();
          }} catch (_err) {{}}
          const title = document.title || "";
          const titleText = `${{title}} ${{url}}`.toLowerCase();
          const looksLikeOptionalNbcProfile =
            (host.endsWith("nbc.com") || host.endsWith(".nbc.com")) &&
            (url.includes("/sign-in") ||
              url.includes("/login") ||
              titleText.includes("nbc account sign in") ||
              titleText.includes("nbcuniversal profile") ||
              titleText.includes("nbc profile"));
          if (looksLikeOptionalNbcProfile) {{
            return {{ pageUrl: url, title, actions }};
          }}
          const candidates = Array.from(
            document.querySelectorAll(
              "button,a,[role='button'],[role='option'],label,li,[data-provider-name],[data-provider-id],[data-provider]"
            )
          );
          const providerCta = candidates.find((node) => {{
            const text = textOf(node);
            return visible(node) &&
              (
                text === "link tv provider" ||
                text === "link provider" ||
                text.startsWith("link tv provider ") ||
                text.startsWith("link provider ")
              );
          }});
          clickNode(providerCta, "click:link-provider");

          if (url.includes("mvpd")) {{
            const fullListNode = candidates.find((node) => {{
              const text = textOf(node);
              return visible(node) && (text === "full list" || text.startsWith("full list "));
            }});
            clickNode(fullListNode, "click:full-list");

            const input = Array.from(document.querySelectorAll("input,textarea,select")).find((node) => {{
              if (node.disabled || node.type === "hidden") return false;
              const hint = normalize([
                node.getAttribute("placeholder") || "",
                node.getAttribute("aria-label") || "",
                node.getAttribute("name") || "",
                node.id || "",
                node.type || "",
                node.getAttribute("data-provider-name") || "",
                node.getAttribute("data-provider-id") || "",
              ].join(" "));
              return hint.includes("search") || hint.includes("provider") || node.type === "search";
            }});
            if (input && normalize(input.value || "") !== normalize(provider)) {{
              setValue(input, provider);
              pressEnter(input);
            }}

            const inputRect = input?.getBoundingClientRect?.() || null;
            const searchNode = inputRect && candidates.find((node) => {{
              const text = textOf(node);
              const rect = node.getBoundingClientRect?.();
              if (!visible(node) || !rect) return false;
              const sameRow = Math.abs((rect.top + rect.height / 2) - (inputRect.top + inputRect.height / 2)) <= Math.max(48, inputRect.height);
              const nearRightEdge = rect.left >= inputRect.right - 24 && rect.left <= inputRect.right + 160;
              const looksLikeSearch = text.includes("search") || text.includes("find");
              const compactButton = rect.width <= 96 && rect.height <= Math.max(96, inputRect.height * 2);
              return (looksLikeSearch || compactButton) && sameRow && nearRightEdge;
            }});
            clickNode(searchNode, "click:provider-search");

            const providerNode = candidates
              .filter((node) => {{
              const text = textOf(node);
              const providerMeta = normalize([
                node.getAttribute?.("data-provider-name") || "",
                node.getAttribute?.("data-provider-id") || "",
                node.getAttribute?.("data-provider") || "",
              ].join(" "));
              return visible(node) && providerNeedles.some((needle) =>
                text.includes(needle) || providerMeta.includes(needle)
              );
              }})
              .sort((a, b) => textOf(a).length - textOf(b).length)[0];
            clickNode(providerNode, `click:provider:${{textOf(providerNode).slice(0, 120)}}`);
          }}

          return {{
            pageUrl: url,
            title,
            actions,
          }};
        }})())
        "#
    );
    let value = tab
        .evaluate(&script, false)?
        .value
        .and_then(|value| value.as_str().map(|value| value.to_string()))
        .and_then(|value| serde_json::from_str::<NbcAuthAdvanceResult>(&value).ok());
    Ok(value.filter(|value| !value.actions.is_empty()))
}

fn advance_mvpd_login_flow(tab: &Arc<Tab>) -> Result<Option<NbcAuthAdvanceResult>> {
    let username = match nbc_mvpd_username() {
        Some(value) => value,
        None => return Ok(None),
    };
    let password = match nbc_mvpd_password() {
        Some(value) => value,
        None => return Ok(None),
    };
    let username_json = serde_json::to_string(&username)?;
    let password_json = serde_json::to_string(&password)?;
    let script = format!(
        r#"
        JSON.stringify((() => {{
          const username = {username_json};
          const password = {password_json};
          const normalize = (value) => (value || "").replace(/\s+/g, " ").trim().toLowerCase();
          const visible = (node) => {{
            if (!node || typeof node.getBoundingClientRect !== "function") return false;
            const rect = node.getBoundingClientRect();
            const style = window.getComputedStyle(node);
            return rect.width > 0 &&
              rect.height > 0 &&
              style.display !== "none" &&
              style.visibility !== "hidden" &&
              style.opacity !== "0";
          }};
          const textOf = (node) => normalize([
            node.innerText || node.textContent || "",
            node.getAttribute?.("aria-label") || "",
            node.getAttribute?.("title") || "",
            node.getAttribute?.("name") || "",
            node.id || "",
            node.value || "",
          ].join(" "));
          const setValue = (node, value, label) => {{
            if (!node || !visible(node)) return false;
            const prototype = Object.getPrototypeOf(node);
            const descriptor = prototype && Object.getOwnPropertyDescriptor(prototype, "value");
            if (descriptor && descriptor.set) {{
              descriptor.set.call(node, value);
            }} else {{
              node.value = value;
            }}
            node.focus?.();
            node.dispatchEvent(new Event("input", {{ bubbles: true }}));
            node.dispatchEvent(new Event("change", {{ bubbles: true }}));
            actions.push(`typed:${{label}}`);
            return true;
          }};
          const pressEnter = (node) => {{
            if (!node) return false;
            for (const type of ["keydown", "keypress", "keyup"]) {{
              node.dispatchEvent?.(new KeyboardEvent(type, {{
                key: "Enter",
                code: "Enter",
                which: 13,
                keyCode: 13,
                bubbles: true,
                cancelable: true,
              }}));
            }}
            actions.push("press:enter");
            return true;
          }};
          const clickNode = (node, action) => {{
            if (!node || !visible(node)) return false;
            node.scrollIntoView?.({{ block: "center", inline: "center" }});
            node.click?.();
            actions.push(action);
            return true;
          }};
          const actions = [];
          const url = window.location.href || "";
          const title = document.title || "";
          const titleText = `${{title}} ${{url}}`.toLowerCase();
          let host = "";
          try {{
            host = new URL(url).hostname.toLowerCase();
          }} catch (_err) {{}}
          const looksLikeVerizon = titleText.includes("verizon") || titleText.includes("fios");
          const onNbcMvpdPicker =
            (host.endsWith("nbc.com") || host.endsWith(".nbc.com")) &&
            url.includes("/mvpd");
          const looksLikeProviderLogin =
            !onNbcMvpdPicker &&
            (
              looksLikeVerizon ||
              host.includes("auth.adobe.com") ||
              host.includes("verizon")
            );
          const looksLikeOptionalNbcProfile =
            (host.endsWith("nbc.com") || host.endsWith(".nbc.com")) &&
            (url.includes("/sign-in") ||
              url.includes("/login") ||
              titleText.includes("nbc account sign in") ||
              titleText.includes("nbcuniversal profile") ||
              titleText.includes("nbc profile"));
          if (looksLikeOptionalNbcProfile) {{
            const profileButtons = Array.from(document.querySelectorAll("button,a,[role='button'],input[type='submit'],input[type='button']"));
            const dismissButton = profileButtons.find((node) => {{
              const text = textOf(node);
              return visible(node) && (
                text === "skip" ||
                text.startsWith("skip ") ||
                text === "skip for now" ||
                text === "maybe later" ||
                text === "not now" ||
                text === "no thanks" ||
                text === "close" ||
                text === "continue watching" ||
                text.startsWith("continue watching ") ||
                text === "continue without signing in" ||
                text === "continue without profile" ||
                text === "continue as guest" ||
                text === "watch live" ||
                text === "watch now" ||
                text.startsWith("watch live ") ||
                text.startsWith("watch now ")
              );
            }});
            if (dismissButton) {{
              clickNode(dismissButton, `click:profile-dismiss:${{textOf(dismissButton).slice(0, 120)}}`);
            }}
            return {{ pageUrl: url, title, actions }};
          }}
          if (!looksLikeProviderLogin) {{
            return {{ pageUrl: url, title, actions }};
          }}

          const fields = Array.from(document.querySelectorAll("input"));
          const usernameField = fields.find((node) => {{
            if (!visible(node) || node.disabled) return false;
            const type = normalize(node.type || "");
            if (type === "hidden" || type === "password" || type === "submit" || type === "checkbox") return false;
            const hint = textOf(node);
            return hint.includes("user")
              || hint.includes("email")
              || hint.includes("mobile")
              || hint.includes("phone")
              || hint.includes("name")
              || type === "email"
              || type === "tel"
              || type === "text";
          }});
          const passwordField = fields.find((node) => {{
            if (!visible(node) || node.disabled) return false;
            return normalize(node.type || "") === "password" || textOf(node).includes("password");
          }});
          if (usernameField && normalize(usernameField.value || "") !== normalize(username)) {{
            setValue(usernameField, username, "username");
          }}
          if (passwordField && normalize(passwordField.value || "") !== normalize(password)) {{
            setValue(passwordField, password, "password");
          }}

          const buttons = Array.from(document.querySelectorAll("button,input[type='submit'],input[type='button'],a,[role='button']"));
          const button = buttons.find((node) => {{
            const text = textOf(node);
            return visible(node) && (
              text === "sign in" ||
              text === "log in" ||
              text === "continue" ||
              text === "next" ||
              text === "submit" ||
              text.startsWith("sign in ") ||
              text.startsWith("continue ")
            );
          }});
          if (button) {{
            clickNode(button, `click:${{textOf(button).slice(0, 120)}}`);
          }} else if (passwordField || usernameField) {{
            pressEnter(passwordField || usernameField);
          }}

          return {{ pageUrl: url, title, actions }};
        }})())
        "#
    );
    let value = tab
        .evaluate(&script, false)?
        .value
        .and_then(|value| value.as_str().map(|value| value.to_string()))
        .and_then(|value| serde_json::from_str::<NbcAuthAdvanceResult>(&value).ok());
    Ok(value.filter(|value| !value.actions.is_empty()))
}

fn advance_nbc_post_auth_flow(tab: &Arc<Tab>) -> Result<Option<NbcAuthAdvanceResult>> {
    let script = r#"
        JSON.stringify((() => {
          const normalize = (value) => (value || "").replace(/\s+/g, " ").trim().toLowerCase();
          const visible = (node) => {
            if (!node || typeof node.getBoundingClientRect !== "function") return false;
            const rect = node.getBoundingClientRect();
            const style = window.getComputedStyle(node);
            return rect.width > 0 &&
              rect.height > 0 &&
              style.display !== "none" &&
              style.visibility !== "hidden" &&
              style.opacity !== "0";
          };
          const textOf = (node) => normalize([
            node.innerText || node.textContent || "",
            node.getAttribute?.("aria-label") || "",
            node.getAttribute?.("title") || "",
            node.getAttribute?.("name") || "",
            node.id || "",
          ].join(" "));
          const clickNode = (node, action) => {
            if (!node || !visible(node)) return false;
            node.scrollIntoView?.({ block: "center", inline: "center" });
            node.click?.();
            actions.push(action);
            return true;
          };
          const actions = [];
          const url = window.location.href || "";
          const title = document.title || "";
          const bodyText = normalize(document.body?.innerText || "");
          const looksLinked = title.toLowerCase().includes("tv provider linked")
            || url.includes("provider-linked")
            || url.includes("mvpd-complete")
            || bodyText.includes("tv provider linked");
          const looksOptionalProfile =
            (url.includes("/sign-in") ||
              url.includes("/login") ||
              normalize(title).includes("nbc account sign in") ||
              normalize(title).includes("nbcuniversal profile") ||
              normalize(title).includes("nbc profile"));
          if (!looksLinked && !looksOptionalProfile) {
            return { pageUrl: url, title, actions };
          }

          const buttons = Array.from(document.querySelectorAll("button,a,[role='button'],input[type='submit'],input[type='button']"));
          const skipButton = buttons.find((node) => {
            const text = textOf(node);
            return visible(node) && (
              text === "skip" ||
              text.startsWith("skip ") ||
              text === "skip for now" ||
              (looksLinked && text === "continue") ||
              (looksLinked && text.startsWith("continue ")) ||
              text === "maybe later" ||
              text === "not now" ||
              text === "no thanks" ||
              text === "close" ||
              text === "continue watching" ||
              text.startsWith("continue watching") ||
              text === "continue without signing in" ||
              text === "continue without profile" ||
              text === "continue as guest" ||
              text === "watch live" ||
              text === "watch now" ||
              text.startsWith("watch live ") ||
              text.startsWith("watch now ")
            );
          });
          if (skipButton) {
            clickNode(skipButton, `click:${textOf(skipButton).slice(0, 120)}`);
          }

          return { pageUrl: url, title, actions };
        })())
    "#;
    let value = tab
        .evaluate(script, false)?
        .value
        .and_then(|value| value.as_str().map(|value| value.to_string()))
        .and_then(|value| serde_json::from_str::<NbcAuthAdvanceResult>(&value).ok());
    Ok(value.filter(|value| !value.actions.is_empty()))
}

fn probe_nbc_page_clues(tab: &Arc<Tab>) -> Option<NbcPageClues> {
    let value = tab
        .evaluate(
            r#"
            JSON.stringify((() => {
              const visible = (node) => {
                if (!node || typeof node.getBoundingClientRect !== "function") return false;
                const rect = node.getBoundingClientRect();
                const style = window.getComputedStyle(node);
                return rect.width > 0 &&
                  rect.height > 0 &&
                  style.display !== "none" &&
                  style.visibility !== "hidden" &&
                  style.opacity !== "0";
              };
              const summarize = (node) => {
                const pieces = [
                  node.innerText || node.textContent || "",
                  node.getAttribute?.("aria-label") || "",
                  node.getAttribute?.("title") || "",
                ]
                  .map((value) => (value || "").replace(/\s+/g, " ").trim())
                  .filter(Boolean);
                return pieces.join(" | ").slice(0, 160);
              };
              const ctas = Array.from(document.querySelectorAll("button,a,[role='button']"))
                .filter(visible)
                .map(summarize)
                .filter(Boolean)
                .slice(0, 20);
              const iframes = Array.from(document.querySelectorAll("iframe"))
                .map((frame) => frame.src || "")
                .filter(Boolean)
                .slice(0, 12);
              const inputs = Array.from(document.querySelectorAll("input,textarea,select"))
                .map((node) => [
                  node.tagName || "",
                  node.getAttribute?.("type") || "",
                  node.getAttribute?.("name") || "",
                  node.id || "",
                  node.getAttribute?.("placeholder") || "",
                  node.getAttribute?.("aria-label") || "",
                  node.getAttribute?.("data-provider-name") || "",
                  node.getAttribute?.("data-provider-id") || "",
                ]
                  .map((value) => (value || "").replace(/\s+/g, " ").trim())
                  .filter(Boolean)
                  .join(" | "))
                .filter(Boolean)
                .slice(0, 20);
              const providerCandidates = Array.from(document.querySelectorAll(
                "[data-provider-name],[data-provider-id],[data-provider]"
              ))
                .map(summarize)
                .filter(Boolean)
                .slice(0, 20);
              const bodyText = (document.body?.innerText || "")
                .replace(/\s+/g, " ")
                .trim()
                .slice(0, 1200);
              return {
                bodyText,
                ctas,
                iframes,
                inputs,
                providerCandidates,
                videoCount: document.querySelectorAll("video").length,
              };
            })())
            "#,
            false,
        )
        .ok()?
        .value
        .and_then(|value| value.as_str().map(|value| value.to_string()))
        .and_then(|value| serde_json::from_str::<NbcPageClues>(&value).ok())?;
    Some(value)
}

fn probe_nbc_video(tab: &Arc<Tab>) -> Result<NbcVideoState> {
    let fallback_page_url = tab.get_url();
    let fallback_title = tab.get_title().unwrap_or_default();
    let value = match tab.evaluate(
        r#"
        JSON.stringify((() => {
          const video = document.querySelector("video");
          return {
            title: document.title || "",
            pageUrl: window.location.href || "",
            documentReadyState: document.readyState || "",
            hasVideo: Boolean(video),
            currentTime: video?.currentTime || 0,
            readyState: video?.readyState || 0,
            paused: video ? Boolean(video.paused) : true,
            width: video?.videoWidth || 0,
            height: video?.videoHeight || 0,
          };
        })())
        "#,
        false,
    ) {
        Ok(result) => result
            .value
            .and_then(|value| value.as_str().map(|value| value.to_string()))
            .and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok())
            .unwrap_or_else(|| serde_json::json!({})),
        Err(err) => {
            tracing::debug!(
                page_url = %fallback_page_url,
                title = %fallback_title,
                "failed to evaluate NBC video probe: {err:#}"
            );
            serde_json::json!({})
        }
    };

    Ok(NbcVideoState {
        title: value
            .get("title")
            .and_then(|value| value.as_str())
            .filter(|value| !value.is_empty())
            .unwrap_or(&fallback_title)
            .to_string(),
        page_url: value
            .get("pageUrl")
            .and_then(|value| value.as_str())
            .filter(|value| !value.is_empty())
            .unwrap_or(&fallback_page_url)
            .to_string(),
        document_ready_state: value
            .get("documentReadyState")
            .and_then(|value| value.as_str())
            .unwrap_or_default()
            .to_string(),
        has_video: value
            .get("hasVideo")
            .and_then(|value| value.as_bool())
            .unwrap_or(false),
        current_time: value
            .get("currentTime")
            .and_then(|value| value.as_f64())
            .unwrap_or(0.0),
        ready_state: value
            .get("readyState")
            .and_then(|value| value.as_i64())
            .unwrap_or(0),
        paused: value
            .get("paused")
            .and_then(|value| value.as_bool())
            .unwrap_or(true),
        width: value
            .get("width")
            .and_then(|value| value.as_u64())
            .unwrap_or(0),
        height: value
            .get("height")
            .and_then(|value| value.as_u64())
            .unwrap_or(0),
    })
}

fn wait_for_nbc_playback(
    browser: &Browser,
    tab: &Arc<Tab>,
    source_url: &str,
    trace: &Arc<StdMutex<NbcTraceState>>,
    auth_mode: AuthMode,
    screenshot_out: Option<PathBuf>,
) -> Result<WaitOutcome> {
    let deadline = Instant::now() + nbc_capture_timeout();
    let auth_forbidden = matches!(&auth_mode, AuthMode::Forbidden);
    let mut interactive_deadline = None::<Instant>;
    let mut interactive_auth_required = false;
    let mut screenshot_path = None::<PathBuf>;
    let mut last_state = None::<NbcVideoState>;
    let mut last_trace_state = None::<NbcTraceState>;
    let mut last_log = Instant::now() - Duration::from_secs(10);
    let mut last_clue_log = Instant::now() - Duration::from_secs(30);
    let mut playback_samples = HashMap::<String, (f64, Instant)>::new();
    let mut resumed_after_background_login = false;
    let mut resumed_after_authenticated_surface = false;
    let mut optional_profile_signin_recoveries = 0_u8;
    let mut last_optional_profile_signin_retry = None::<Instant>;
    let mut watch_surface_seen_at = None::<Instant>;
    let mut optional_profile_signin_seen_at = None::<Instant>;
    let mut tracked_tabs = HashSet::new();
    let mut provider_linked_completed = false;

    loop {
        register_nbc_trace_handlers_for_browser(browser, trace, &mut tracked_tabs);
        let tab_states = collect_browser_tab_states(browser);
        let primary_state = find_primary_tab_state(&tab_states, tab)
            .map(|value| value.state.clone())
            .unwrap_or_else(|| probe_nbc_video(tab).unwrap_or_default());
        let now = Instant::now();
        for playing_tab in tab_states
            .iter()
            .filter(|candidate| nbc_video_state_has_decoded_frame(&candidate.state))
        {
            let target_id = playing_tab.tab.get_target_id().to_string();
            if let Some((previous_time, first_seen)) = playback_samples.get(&target_id) {
                if playing_tab.state.current_time >= *previous_time + 0.25
                    && first_seen.elapsed() >= Duration::from_millis(500)
                {
                    return Ok(WaitOutcome {
                        tab: playing_tab.tab.clone(),
                        state: playing_tab.state.clone(),
                        trace: trace.lock().map(|state| state.clone()).unwrap_or_default(),
                        interactive_auth_required,
                        screenshot_path,
                    });
                }
            }
            playback_samples
                .entry(target_id)
                .and_modify(|(previous_time, _)| {
                    if playing_tab.state.current_time < *previous_time {
                        *previous_time = playing_tab.state.current_time;
                    }
                })
                .or_insert((playing_tab.state.current_time, now));
        }

        let interaction_tab = find_interaction_tab_state(&tab_states, tab)
            .map(|value| value.tab.clone())
            .unwrap_or_else(|| tab.clone());
        let _ = interaction_tab.activate();
        kick_nbc_player(&interaction_tab).ok();
        let pre_state = probe_nbc_video(&interaction_tab).unwrap_or_default();
        if nbc_title_looks_like_provider_linked(&pre_state.title)
            || nbc_url_is_provider_linked(&pre_state.page_url)
            || nbc_url_is_mvpd_complete(&pre_state.page_url)
        {
            provider_linked_completed = true;
        }
        let pre_trace_state = trace.lock().map(|state| state.clone()).unwrap_or_default();
        let pre_authorized = nbc_trace_is_authorized(&pre_trace_state) || provider_linked_completed;
        if let Some(progress) = advance_nbc_post_auth_flow(&interaction_tab).ok().flatten() {
            if nbc_title_looks_like_provider_linked(&progress.title)
                || nbc_url_is_provider_linked(&progress.page_url)
                || nbc_url_is_mvpd_complete(&progress.page_url)
                || progress
                    .actions
                    .iter()
                    .any(|action| action.starts_with("click:skip"))
            {
                provider_linked_completed = true;
            }
            tracing::info!(
                title = %progress.title,
                page_url = %progress.page_url,
                actions = ?progress.actions,
                "advanced NBC post-auth flow"
            );
        }
        if !pre_authorized {
            if let Some(progress) = advance_mvpd_login_flow(&interaction_tab).ok().flatten() {
                tracing::info!(
                    title = %progress.title,
                    page_url = %progress.page_url,
                    actions = ?progress.actions,
                    "advanced NBC MVPD login flow"
                );
            }
            if let Some(progress) = advance_nbc_auth_flow(&interaction_tab).ok().flatten() {
                tracing::info!(
                    title = %progress.title,
                    page_url = %progress.page_url,
                    actions = ?progress.actions,
                    "advanced NBC interactive auth flow"
                );
            }
        }
        let state = probe_nbc_video(&interaction_tab).unwrap_or_default();
        if nbc_title_looks_like_provider_linked(&state.title)
            || nbc_url_is_provider_linked(&state.page_url)
            || nbc_url_is_mvpd_complete(&state.page_url)
        {
            provider_linked_completed = true;
        }
        let trace_state = trace.lock().map(|state| state.clone()).unwrap_or_default();
        let authorized = nbc_trace_is_authorized(&trace_state) || provider_linked_completed;
        let recent_media_activity = nbc_trace_has_recent_media_activity(&trace_state);

        if !authorized && nbc_state_is_optional_profile_signin(&state) && !state.has_video {
            let first_seen = *optional_profile_signin_seen_at.get_or_insert_with(Instant::now);
            if auth_forbidden && first_seen.elapsed() >= nbc_profile_signin_gate_timeout() {
                return Err(anyhow!(
                    "NBC account sign-in gate reached before TV-provider auth; refusing non-interactive retry loop without decoded video (title='{}', page_url='{}')",
                    state.title,
                    state.page_url,
                ));
            }
        } else {
            optional_profile_signin_seen_at = None;
        }

        if last_log.elapsed() >= Duration::from_secs(5) {
            last_log = Instant::now();
            tracing::info!(
                title = %state.title,
                page_url = %state.page_url,
                document_ready_state = %state.document_ready_state,
                has_video = state.has_video,
                current_time = state.current_time,
                ready_state = state.ready_state,
                paused = state.paused,
                authorized,
                recent_media_activity,
                interactive_auth_required,
                "waiting for NBC playback"
            );
        }

        if nbc_url_requires_interactive_auth(&state.page_url) && !authorized {
            if !interactive_auth_required {
                interactive_auth_required = true;
                if let Some(path) = screenshot_out.as_ref() {
                    save_tab_screenshot(&interaction_tab, path).ok();
                    screenshot_path = Some(path.clone());
                }
                tracing::info!(
                    page_url = %state.page_url,
                    "NBC interactive auth required"
                );
                interactive_deadline = match auth_mode {
                    AuthMode::Forbidden => {
                        return Err(anyhow!(
                            "interactive NBC auth required at {}; run `ec-node nbc-bootstrap` against this URL first",
                            state.page_url
                        ));
                    }
                    AuthMode::AllowInteractive { timeout } => Some(Instant::now() + timeout),
                };
            }
        }

        let auth_completion_page = nbc_url_is_background_login_complete(&state.page_url)
            || nbc_url_is_mvpd_complete(&state.page_url);
        if (trace_state.background_login_complete || auth_completion_page)
            && !resumed_after_background_login
        {
            resumed_after_background_login = true;
            tracing::info!(
                title = %state.title,
                page_url = %state.page_url,
                "NBC background login completed; closing auth tabs and reloading watch page"
            );
            close_auxiliary_browser_tabs(browser, tab);
            let _ = tab.activate();
            if nbc_url_is_mvpd_complete(&state.page_url) {
                tab.navigate_to(source_url)?;
                tab.wait_until_navigated()?;
            } else {
                let _ = tab.evaluate("window.location.reload()", true);
            }
            std::thread::sleep(Duration::from_secs(2));
            continue;
        }

        if authorized && nbc_state_is_optional_profile_signin(&state) && !state.has_video {
            if optional_profile_signin_recoveries == 0
                && last_optional_profile_signin_retry
                    .map(|instant| instant.elapsed() >= Duration::from_secs(3))
                    .unwrap_or(true)
            {
                optional_profile_signin_recoveries += 1;
                last_optional_profile_signin_retry = Some(Instant::now());
                tracing::info!(
                    title = %state.title,
                    page_url = %state.page_url,
                    authorized,
                    source_url,
                    "NBC account sign-in gate detected after provider authorization; trying one live-url recovery"
                );
                close_auxiliary_browser_tabs(browser, tab);
                let _ = tab.activate();
                tab.navigate_to(source_url)?;
                tab.wait_until_navigated()?;
                std::thread::sleep(Duration::from_secs(2));
                continue;
            }
            return Err(anyhow!(
                "NBC account sign-in gate reached after TV-provider auth; refusing retry loop without decoded video (title='{}', page_url='{}')",
                state.title,
                state.page_url,
            ));
        }
        if authorized && nbc_state_is_optional_profile_signin(&state) && state.has_video {
            tracing::debug!(
                title = %state.title,
                page_url = %state.page_url,
                "NBC optional profile sign-in is visible but a video element is already present; staying on the page"
            );
        }

        let interaction_is_primary = interaction_tab.get_target_id() == tab.get_target_id();
        let fully_loaded_watch_surface = (!interactive_auth_required || authorized)
            && interaction_is_primary
            && nbc_page_is_watch_surface(&primary_state.page_url)
            && primary_state
                .document_ready_state
                .eq_ignore_ascii_case("complete");
        if fully_loaded_watch_surface
            && !primary_state.has_video
            && last_clue_log.elapsed() >= Duration::from_secs(15)
        {
            last_clue_log = Instant::now();
            if let Some(clues) = probe_nbc_page_clues(tab) {
                tracing::info!(
                    ctas = ?clues.ctas,
                    iframes = ?clues.iframes,
                    inputs = ?clues.inputs,
                    provider_candidates = ?clues.provider_candidates,
                    video_count = clues.video_count,
                    body_text = %clues.body_text,
                    "NBC watch surface clues"
                );
                if nbc_clues_look_geo_blocked(&clues) {
                    return Err(anyhow!(
                        "NBC geo-blocked current egress; page says this content is not authorized outside the US/territories (title='{}', page_url='{}')",
                        primary_state.title,
                        primary_state.page_url,
                    ));
                }
            }
        }
        if fully_loaded_watch_surface && !primary_state.has_video {
            watch_surface_seen_at.get_or_insert_with(Instant::now);
        } else {
            watch_surface_seen_at = None;
        }
        if fully_loaded_watch_surface
            && !primary_state.has_video
            && !resumed_after_authenticated_surface
            && watch_surface_seen_at
                .map(|seen_at| seen_at.elapsed() >= Duration::from_secs(3))
                .unwrap_or(false)
        {
            resumed_after_authenticated_surface = true;
            tracing::info!(
                title = %primary_state.title,
                page_url = %primary_state.page_url,
                "NBC browser is on a fully loaded watch surface without video; reloading once"
            );
            let _ = tab.evaluate("window.location.reload()", true);
            std::thread::sleep(Duration::from_secs(2));
            continue;
        }

        last_state = Some(state);
        last_trace_state = Some(trace_state);

        let now = Instant::now();
        if now >= deadline {
            break;
        }
        if let Some(interactive_deadline) = interactive_deadline {
            if now >= interactive_deadline {
                break;
            }
        }
        std::thread::sleep(Duration::from_secs(1));
    }

    let last_state = last_state.unwrap_or_default();
    let last_trace_state = last_trace_state.unwrap_or_default();
    if screenshot_path.is_none() {
        if let Some(path) = screenshot_out.as_ref() {
            if save_tab_screenshot(tab, path).is_ok() {
                screenshot_path = Some(path.clone());
            }
        }
    }

    Err(anyhow!(
        "timed out waiting for NBC playback (title='{}', page_url='{}', current_time={}, authorized={}, interactive_auth_required={})",
        last_state.title,
        last_state.page_url,
        last_state.current_time,
        nbc_trace_is_authorized(&last_trace_state),
        interactive_auth_required,
    ))
}

fn save_tab_screenshot(tab: &Arc<Tab>, path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("failed to create {}", parent.display()))?;
    }
    let screenshot =
        tab.capture_screenshot(Page::CaptureScreenshotFormatOption::Png, None, None, true)?;
    fs::write(path, screenshot).with_context(|| format!("failed to write {}", path.display()))?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn interactive_auth_url_detection_catches_mvpd_hosts() {
        assert!(nbc_url_requires_interactive_auth(
            "https://sp.auth.adobe.com/adobe-services/authenticate/saml"
        ));
        assert!(nbc_url_requires_interactive_auth(
            "https://secure.verizon.com/signin"
        ));
        assert!(!nbc_url_requires_interactive_auth(
            "https://www.nbc.com/sign-in"
        ));
        assert!(!nbc_url_requires_interactive_auth(
            "https://www.nbc.com/watch/some-show"
        ));
    }

    #[test]
    fn trace_updates_mark_authorization_edges() {
        let mut trace = NbcTraceState::default();
        assert!(update_nbc_trace_from_url(
            &mut trace,
            "https://sp.auth.adobe.com/adobe-services/authorize"
        ));
        assert!(nbc_trace_is_authorized(&trace));
        assert!(update_nbc_trace_from_url(
            &mut trace,
            "https://entitlement.auth.adobe.com/entitlement/v4/completeBackgroundLogin.html"
        ));
        assert!(trace.background_login_complete);
    }

    #[test]
    fn verizon_popup_title_detection_matches_expected_surface() {
        assert!(nbc_title_looks_like_verizon_popup(
            "Verizon FiOS - sign in - Google Chrome"
        ));
        assert!(!nbc_title_looks_like_verizon_popup("MVPD Picker"));
    }

    #[test]
    fn background_login_url_detection_matches_adobe_completion() {
        assert!(nbc_url_is_background_login_complete(
            "https://entitlement.auth.adobe.com/entitlement/v4/completeBackgroundLogin.html"
        ));
        assert!(!nbc_url_is_background_login_complete(
            "https://www.nbc.com/mvpd-picker"
        ));
    }

    #[test]
    fn provider_linked_surface_detection_matches_expected_url() {
        assert!(nbc_url_is_provider_linked(
            "https://www.nbc.com/provider-linked"
        ));
        assert!(nbc_url_is_mvpd_complete(
            "https://www.nbc.com/mvpd-complete"
        ));
        assert!(nbc_title_looks_like_provider_linked("TV Provider Linked"));
        assert!(!nbc_url_is_provider_linked(
            "https://www.nbc.com/live?brand=nbc-sports-philadelphia"
        ));
    }

    #[test]
    fn optional_profile_signin_detection_matches_expected_surface() {
        assert!(nbc_url_is_optional_profile_signin(
            "https://www.nbc.com/sign-in"
        ));
        assert!(nbc_title_looks_like_optional_profile_signin(
            "NBC Account Sign In - NBC.com"
        ));
        assert!(!nbc_url_is_optional_profile_signin(
            "https://secure.verizon.com/signin"
        ));
    }

    #[test]
    fn optional_profile_signin_is_not_treated_as_watch_surface() {
        assert!(!nbc_page_is_watch_surface("https://www.nbc.com/sign-in"));
        assert!(!nbc_page_is_watch_surface(
            "https://www.nbc.com/mvpd-complete"
        ));
        assert!(nbc_page_is_watch_surface(
            "https://www.nbc.com/live?brand=nbc-sports-philadelphia"
        ));
    }

    #[test]
    fn geo_block_clues_fail_closed() {
        let clues = NbcPageClues {
            body_text:
                "We're sorry. You are not authorized to access this content from outside of the US and its territories."
                    .to_string(),
            ..NbcPageClues::default()
        };
        assert!(nbc_clues_look_geo_blocked(&clues));

        let allowed = NbcPageClues {
            body_text: "NBC News NOW ON NOW until 7:00 AM".to_string(),
            ..NbcPageClues::default()
        };
        assert!(!nbc_clues_look_geo_blocked(&allowed));
    }

    #[test]
    fn cssott_media_requests_mark_recent_media_activity() {
        let mut trace = NbcTraceState::default();
        assert!(update_nbc_trace_from_url(
            &mut trace,
            "https://g001-live-us-cmaf-prd-ak.pcdn03.cssott.com/Content/CMAF_OL2-CTR-4s/Live/channel(123)/1752757734249item-04item_Segment-5754562.mp4"
        ));
        assert!(trace.media_activity_seen);
        assert!(nbc_trace_has_recent_media_activity(&trace));
    }

    #[test]
    fn decoded_frame_detection_requires_advancing_video_surface() {
        let mut state = NbcVideoState {
            has_video: true,
            width: 1920,
            height: 1080,
            paused: false,
            ready_state: 2,
            current_time: 1.0,
            ..NbcVideoState::default()
        };
        assert!(nbc_video_state_has_decoded_frame(&state));

        state.current_time = 0.0;
        assert!(!nbc_video_state_has_decoded_frame(&state));

        state.current_time = 1.0;
        state.width = 0;
        assert!(!nbc_video_state_has_decoded_frame(&state));
    }
}
