mod api;
mod app;
mod auth;
mod config;
mod errors;
mod logger;
mod logo;
mod models;
mod syntax;
mod tunnel;
mod ui;
mod updater;

use anyhow::{Result, anyhow};
use chrono::{Duration as ChronoDuration, Utc};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL_CONDENSED};
use crossterm::{
    cursor::{MoveToColumn, Show},
    event::{self, Event, KeyEventKind},
    execute,
    style::Stylize,
    terminal::{
        Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
        enable_raw_mode,
    },
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io::{self, Write};
use std::path::PathBuf;
use std::time::Duration;
use tokio::{
    sync::{mpsc, watch},
    time::sleep,
};
use tracing::error;

use api::ApiClient;
use app::{App, AppState};
use logger::{LogConfig, Logger};
use tunnel::TunnelEvent;

#[derive(Parser)]
#[command(name = "hooklistener")]
#[command(about = "A CLI tool for debugging webhooks")]
#[command(version)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Output non-interactive command responses as JSON
    #[arg(long, global = true)]
    json: bool,

    /// Log level (trace, debug, info, warn, error)
    #[arg(long, default_value = "info", value_parser = validate_log_level)]
    log_level: String,

    /// Custom directory for log files
    #[arg(long)]
    log_dir: Option<PathBuf>,

    /// Output logs to stdout in addition to files (for debugging)
    #[arg(long)]
    log_stdout: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Authenticate with Hooklistener via the device flow
    Login {
        /// Start a new authentication even if a valid token already exists
        #[arg(long)]
        force: bool,
    },
    /// Start WebSocket tunnel to forward webhooks to local server
    Listen {
        /// Debug endpoint slug to listen to
        endpoint: String,

        /// Local URL to forward requests to
        #[arg(short, long, default_value = "http://localhost:3000")]
        target: String,

        /// WebSocket server URL (defaults to production)
        #[arg(long)]
        ws_url: Option<String>,
    },
    /// Generate a diagnostic bundle for support
    Diagnostics {
        /// Output directory for the diagnostic bundle
        #[arg(short, long, default_value = ".")]
        output: PathBuf,
    },
    /// Clean up old log files
    CleanLogs {
        /// Maximum number of log files to keep
        #[arg(short, long, default_value = "10")]
        keep: usize,
    },
    /// Manage CLI configuration
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },
    /// Sign out and clear locally stored token
    Logout,
    /// Organization helpers
    Org {
        #[command(subcommand)]
        action: OrgAction,
    },
    /// Debug endpoint helpers
    Endpoint {
        #[command(subcommand)]
        action: EndpointAction,
    },
    /// Static tunnel slug management
    StaticTunnel {
        #[command(subcommand)]
        action: StaticTunnelAction,
    },
    /// Anonymous (temporary) debug endpoints — no login required
    Anon {
        #[command(subcommand)]
        action: AnonAction,
    },
    /// Share captured requests via public links
    Share {
        #[command(subcommand)]
        action: ShareAction,
    },
    /// Uptime monitoring for your endpoints
    Monitor {
        #[command(subcommand)]
        action: MonitorAction,
    },
    /// Generate shell completion scripts
    Completions {
        /// Target shell
        #[arg(value_enum)]
        shell: CompletionShell,
    },
    /// Update hooklistener to the latest version
    Update,
    /// Start HTTP tunnel to forward requests to local server
    Tunnel {
        /// Local port to forward requests to
        #[arg(short, long, default_value = "3000")]
        port: u16,

        /// Local host to forward to
        #[arg(long, default_value = "localhost")]
        host: String,

        /// Organization ID (optional, uses default)
        #[arg(short, long)]
        org: Option<String>,

        /// Static tunnel slug (paid plans only, creates persistent subdomain)
        #[arg(short, long)]
        slug: Option<String>,
    },
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum CompletionShell {
    Bash,
    Zsh,
    Fish,
    PowerShell,
    Elvish,
}

#[derive(Subcommand)]
enum ConfigAction {
    /// Display current configuration
    Show,
    /// Set a configuration value
    Set {
        /// Configuration key (selected_organization_id)
        key: String,
        /// New value
        value: String,
    },
}

#[derive(Subcommand)]
enum OrgAction {
    /// List organizations available to your account
    List,
    /// Set the default organization used by CLI commands
    Use {
        /// Organization ID
        id: String,
    },
    /// Clear the default organization
    Clear,
}

#[derive(Subcommand)]
enum EndpointAction {
    /// Create a debug endpoint
    Create {
        /// Endpoint display name
        name: String,
        /// Optional custom slug
        #[arg(long)]
        slug: Option<String>,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// List debug endpoints for an organization
    List {
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Show a single debug endpoint by ID
    Show {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Delete a debug endpoint by ID
    Delete {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// List captured requests for an endpoint
    Requests {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Page number
        #[arg(long, default_value = "1")]
        page: u32,
        /// Page size
        #[arg(long, default_value = "50")]
        page_size: u32,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Show a single captured request
    Request {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Debug request ID
        request_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Delete a captured request
    DeleteRequest {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Debug request ID
        request_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Replay a captured request to a target URL
    ForwardRequest {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Debug request ID
        request_id: String,
        /// Target URL to replay to
        target_url: String,
        /// Optional HTTP method override (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
        #[arg(long)]
        method: Option<String>,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// List forwards created from a captured request
    Forwards {
        /// Debug endpoint ID
        endpoint_id: String,
        /// Debug request ID
        request_id: String,
        /// Page number
        #[arg(long, default_value = "1")]
        page: u32,
        /// Page size
        #[arg(long, default_value = "50")]
        page_size: u32,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Show a single forward attempt by ID
    Forward {
        /// Forward ID
        forward_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
}

#[derive(Subcommand)]
enum StaticTunnelAction {
    /// List reserved static tunnel slugs
    List {
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Create a new static tunnel slug
    Create {
        /// Slug to reserve
        slug: String,
        /// Optional display name
        #[arg(long)]
        name: Option<String>,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Delete a static tunnel slug by ID
    Delete {
        /// Static tunnel ID
        slug_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
}

#[derive(Subcommand)]
enum AnonAction {
    /// Create a temporary anonymous endpoint (no login required)
    Create {
        /// Time-to-live in seconds (default: 86400 = 24 hours)
        #[arg(long)]
        ttl: Option<u64>,
    },
    /// Show status of an anonymous endpoint
    Show {
        /// Anonymous endpoint ID
        id: String,
    },
    /// List captured events for an anonymous endpoint
    Events {
        /// Anonymous endpoint ID
        endpoint_id: String,
        /// Viewer token (returned when the endpoint was created)
        #[arg(long)]
        token: String,
        /// Page number
        #[arg(long, default_value = "1")]
        page: u32,
        /// Page size
        #[arg(long, default_value = "50")]
        page_size: u32,
    },
    /// Show a single captured event
    Event {
        /// Anonymous endpoint ID
        endpoint_id: String,
        /// Event ID
        event_id: String,
        /// Viewer token (returned when the endpoint was created)
        #[arg(long)]
        token: String,
    },
}

#[derive(Subcommand)]
enum ShareAction {
    /// Create a shareable link for a captured request
    Create {
        /// Debug request ID to share
        debug_request_id: String,
        /// Expiration in hours (e.g. 24)
        #[arg(long)]
        expires_in_hours: Option<u64>,
        /// Optional password to protect the share
        #[arg(long)]
        password: Option<String>,
        /// Include forwards in the shared view
        #[arg(long)]
        include_forwards: bool,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// List all shares for a captured request
    List {
        /// Debug request ID
        debug_request_id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// View a shared request by its share token (public, no login required)
    Show {
        /// Share token
        token: String,
    },
    /// Revoke a shared request link
    Revoke {
        /// Share token to revoke
        token: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
}

#[derive(Subcommand)]
enum MonitorAction {
    /// Create a new uptime monitor
    Create {
        /// Monitor display name
        name: String,
        /// URL to monitor (must be http:// or https://)
        url: String,
        /// HTTP method (get, post, put, patch, delete, head)
        #[arg(long, default_value = "get")]
        method: String,
        /// Expected HTTP status code
        #[arg(long, default_value = "200")]
        expected_status: u16,
        /// Check interval in minutes (1, 5, 10, 30, 60)
        #[arg(long, default_value = "5")]
        interval: u32,
        /// String the response body must contain
        #[arg(long)]
        body_contains: Option<String>,
        /// Request body to send (for POST/PUT/PATCH)
        #[arg(long)]
        body: Option<String>,
        /// Number of consecutive failures before alerting
        #[arg(long, default_value = "2")]
        failure_threshold: u32,
        /// Enable email notifications
        #[arg(long, default_value = "true")]
        email: bool,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// List all uptime monitors
    List {
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Show details of an uptime monitor
    Show {
        /// Monitor ID
        id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Update an uptime monitor
    Update {
        /// Monitor ID
        id: String,
        /// New name
        #[arg(long)]
        name: Option<String>,
        /// New URL
        #[arg(long)]
        url: Option<String>,
        /// HTTP method
        #[arg(long)]
        method: Option<String>,
        /// Expected HTTP status code
        #[arg(long)]
        expected_status: Option<u16>,
        /// Check interval in minutes (1, 5, 10, 30, 60)
        #[arg(long)]
        interval: Option<u32>,
        /// Enable or disable the monitor
        #[arg(long)]
        enabled: Option<bool>,
        /// Number of consecutive failures before alerting
        #[arg(long)]
        failure_threshold: Option<u32>,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Delete an uptime monitor
    Delete {
        /// Monitor ID
        id: String,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
    /// Show recent check history for a monitor
    Checks {
        /// Monitor ID
        id: String,
        /// Page number
        #[arg(long, default_value = "1")]
        page: u32,
        /// Page size
        #[arg(long, default_value = "50")]
        page_size: u32,
        /// Organization ID override (falls back to configured default)
        #[arg(long)]
        org: Option<String>,
    },
}

fn validate_log_level(s: &str) -> Result<String, String> {
    match s.to_lowercase().as_str() {
        "trace" | "debug" | "info" | "warn" | "error" => Ok(s.to_string()),
        _ => Err(format!(
            "Invalid log level: {}. Valid levels are: trace, debug, info, warn, error",
            s
        )),
    }
}

fn normalize_http_method(method: Option<String>) -> Result<Option<String>> {
    let Some(method) = method else {
        return Ok(None);
    };

    let normalized = method.trim().to_ascii_uppercase();
    let valid = matches!(
        normalized.as_str(),
        "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
    );

    if !valid {
        return Err(anyhow!(
            "Invalid HTTP method '{}'. Valid values: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS",
            method
        ));
    }

    Ok(Some(normalized))
}

pub(crate) fn print_json<T: serde::Serialize>(value: &T) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(value)?);
    Ok(())
}

const SESSION_TOKEN_VALIDITY_DAYS: i64 = 60;

#[tokio::main]
async fn main() -> Result<()> {
    let Cli {
        command,
        json,
        log_level,
        log_dir,
        log_stdout,
    } = Cli::parse();

    let Some(command) = command else {
        Cli::command().print_help()?;
        println!();
        return Ok(());
    };

    // Spawn background version check for non-interactive, non-update commands
    let update_handle =
        if !json && !matches!(command, Commands::Update | Commands::Completions { .. }) {
            config::Config::load()
                .ok()
                .and_then(|cfg| updater::spawn_version_check(&cfg))
        } else {
            None
        };

    match command {
        Commands::Login { force } => {
            let log_config = LogConfig {
                level: log_level.clone(),
                output_to_stdout: log_stdout,
                directory: log_dir
                    .clone()
                    .unwrap_or_else(|| LogConfig::default().directory),
                ..Default::default()
            };
            let _logger = Logger::new(log_config)?;
            run_login_flow(force).await?;
        }
        Commands::Listen {
            endpoint,
            target,
            ws_url,
        } => {
            // Initialize logging for tunnel
            let log_config = LogConfig {
                level: log_level.clone(),
                output_to_stdout: false, // Disable stdout logging for TUI
                directory: log_dir
                    .clone()
                    .unwrap_or_else(|| LogConfig::default().directory),
                ..Default::default()
            };
            let _logger = Logger::new(log_config)?;

            // Load config for auth token
            let config = config::Config::load()?;

            // Check if authenticated
            if !config.is_token_valid() {
                eprintln!(
                    "❌ Not authenticated. Please run 'hooklistener login' to authenticate first."
                );
                std::process::exit(1);
            }

            let access_token = config
                .access_token
                .ok_or_else(|| anyhow::anyhow!("No access token found"))?;

            // Setup TUI for listen command
            let mut terminal = setup_terminal()?;
            let mut app = App::new()?;

            // Set app state to listening
            app.state = AppState::Listening;
            app.listening_endpoint = endpoint.clone();
            app.listening_target = target.clone();

            // Create channel for tunnel events
            let (event_tx, event_rx) = mpsc::channel(100);

            // Create and spawn tunnel client
            let tunnel_client = tunnel::TunnelClient::new(
                access_token,
                endpoint.clone(),
                target.clone(),
                ws_url,
                event_tx,
            );

            tokio::spawn(async move {
                if let Err(e) = tunnel_client
                    .connect_with_reconnect(tunnel::ReconnectConfig::default())
                    .await
                {
                    error!("Tunnel client error: {}", e);
                }
            });

            let logo_rx = logo::spawn_logo_animation();
            let res = run_app(&mut terminal, &mut app, event_rx, None, None, Some(logo_rx)).await;

            restore_terminal(&mut terminal)?;

            if let Err(err) = res {
                error!(error = %err, "Application terminated with error");
                display_error(&err);
            }
        }
        Commands::Diagnostics { output } => {
            // Initialize minimal logging for diagnostics
            let log_config = LogConfig {
                level: "info".to_string(),
                // Keep machine-readable output clean when --json is enabled.
                output_to_stdout: !json,
                ..Default::default()
            };
            let logger = Logger::new(log_config)?;
            logger.create_diagnostic_bundle(&output)?;
            if json {
                print_json(&serde_json::json!({
                    "status": "ok",
                    "output": output.display().to_string()
                }))?;
            } else {
                println!(
                    "✅ Diagnostic bundle created in: {}",
                    output.display().to_string().bold()
                );
            }
        }
        Commands::CleanLogs { keep } => {
            let directory = log_dir
                .clone()
                .unwrap_or_else(|| LogConfig::default().directory);
            std::fs::create_dir_all(&directory)?;
            let removed = Logger::cleanup_old_logs(&directory, keep)?;
            if json {
                print_json(&serde_json::json!({
                    "removed": removed,
                    "directory": directory.display().to_string(),
                    "keep": keep
                }))?;
            } else {
                println!(
                    "Removed {} old log file(s) from {} (keeping {} most recent)",
                    removed.to_string().bold(),
                    directory.display().to_string().dim(),
                    keep.to_string().bold()
                );
            }
        }
        Commands::Config { action } => match action {
            ConfigAction::Show => {
                let config = config::Config::load()?;
                let config_path = config::Config::config_path()?;
                if json {
                    let token_status = if config.access_token.is_none() {
                        "none"
                    } else if config.is_token_valid() {
                        "valid"
                    } else {
                        "expired"
                    };

                    print_json(&serde_json::json!({
                        "config_path": config_path.display().to_string(),
                        "token": {
                            "present": config.access_token.is_some(),
                            "status": token_status
                        },
                        "organization_id": config.selected_organization_id
                    }))?;
                } else {
                    println!("{} {}", "Config file:".dim(), config_path.display());
                    println!();
                    match &config.access_token {
                        Some(token) => {
                            let truncated = if token.len() > 8 {
                                format!("{}...", &token[..8])
                            } else {
                                token.clone()
                            };
                            if config.is_token_valid() {
                                println!(
                                    "  {} {} {}",
                                    "Token:".bold(),
                                    truncated,
                                    "(valid)".green()
                                );
                            } else {
                                println!(
                                    "  {} {} {}",
                                    "Token:".bold(),
                                    truncated,
                                    "(expired)".red()
                                );
                            }
                        }
                        None => println!("  {} {}", "Token:".bold(), "(none)".dim()),
                    }
                    match &config.selected_organization_id {
                        Some(org_id) => {
                            println!("  {} {}", "Organization:".bold(), org_id)
                        }
                        None => {
                            println!("  {} {}", "Organization:".bold(), "(none)".dim())
                        }
                    }
                }
            }
            ConfigAction::Set { key, value } => match key.as_str() {
                "selected_organization_id" => {
                    let mut config = config::Config::load()?;
                    if value == "none" {
                        config.selected_organization_id = None;
                        config.save()?;
                        if json {
                            print_json(&serde_json::json!({
                                "status": "ok",
                                "key": "selected_organization_id",
                                "value": null
                            }))?;
                        } else {
                            println!("✅ Cleared {}", "selected_organization_id".bold());
                        }
                    } else {
                        config.selected_organization_id = Some(value);
                        config.save()?;
                        if json {
                            print_json(&serde_json::json!({
                                "status": "ok",
                                "key": "selected_organization_id",
                                "value": config.selected_organization_id
                            }))?;
                        } else {
                            println!(
                                "✅ Set {} to {}",
                                "selected_organization_id".bold(),
                                config.selected_organization_id.as_deref().unwrap().bold()
                            );
                        }
                    }
                }
                _ => {
                    eprintln!(
                        "Unknown config key: {}. Available keys: selected_organization_id",
                        key
                    );
                    std::process::exit(1);
                }
            },
        },
        Commands::Logout => {
            let mut config = config::Config::load()?;
            if config.access_token.is_none() && config.token_expires_at.is_none() {
                if json {
                    print_json(&serde_json::json!({
                        "status": "already_logged_out"
                    }))?;
                } else {
                    println!("{}", "Already logged out.".dim());
                }
            } else {
                // Best-effort revoke refresh token server-side
                if let Some(ref refresh_token) = config.refresh_token {
                    let _ = api::revoke_refresh_token(refresh_token).await;
                }
                config.clear_token();
                config.save()?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "logged_out"
                    }))?;
                } else {
                    println!("✅ Logged out successfully.");
                }
            }
        }
        Commands::Org { action } => match action {
            OrgAction::List => {
                let mut config = config::Config::load()?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, None)?;
                let organizations = client.list_organizations().await?;
                if json {
                    print_json(&serde_json::json!({
                        "selected_organization_id": config.selected_organization_id,
                        "organizations": organizations
                    }))?;
                } else {
                    print_organizations(&organizations, config.selected_organization_id.as_deref());
                }
            }
            OrgAction::Use { id } => {
                let mut config = config::Config::load()?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, None)?;
                let organizations = client.list_organizations().await?;
                let organization_name = organizations
                    .iter()
                    .find(|org| org.id == id)
                    .map(|org| org.name.clone())
                    .ok_or_else(|| {
                        anyhow!("Organization not found or not accessible with id: {}", id)
                    })?;

                config.selected_organization_id = Some(id.clone());
                config.save()?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "ok",
                        "selected_organization_id": id,
                        "organization_name": organization_name
                    }))?;
                } else {
                    println!(
                        "✅ Selected organization: {} ({})",
                        organization_name.bold(),
                        id.dim()
                    );
                }
            }
            OrgAction::Clear => {
                let mut config = config::Config::load()?;
                config.selected_organization_id = None;
                config.save()?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "ok",
                        "selected_organization_id": null
                    }))?;
                } else {
                    println!("✅ Cleared selected organization.");
                }
            }
        },
        Commands::Endpoint { action } => match action {
            EndpointAction::Create { name, slug, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let endpoint = client.create_endpoint(&name, slug.as_deref()).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint": endpoint
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    println!("✅ Created endpoint:");
                    print_endpoints(std::slice::from_ref(&endpoint));
                }
            }
            EndpointAction::List { org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let endpoints = client.list_endpoints().await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoints": endpoints
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_endpoints(&endpoints);
                }
            }
            EndpointAction::Show { endpoint_id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let endpoint = client.get_endpoint(&endpoint_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint": endpoint
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_endpoints(std::slice::from_ref(&endpoint));
                }
            }
            EndpointAction::Delete { endpoint_id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                client.delete_endpoint(&endpoint_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "deleted",
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id
                    }))?;
                } else {
                    println!(
                        "🗑  Deleted endpoint: {} {}",
                        endpoint_id.bold(),
                        format!("(organization {})", organization_id).dim()
                    );
                }
            }
            EndpointAction::Requests {
                endpoint_id,
                page,
                page_size,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let requests = client
                    .list_endpoint_requests(&endpoint_id, page, page_size)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id,
                        "requests": requests
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Endpoint:", &endpoint_id);
                    print_endpoint_requests(&requests);
                }
            }
            EndpointAction::Request {
                endpoint_id,
                request_id,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let request = client
                    .get_endpoint_request(&endpoint_id, &request_id)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id,
                        "request": request
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Endpoint:", &endpoint_id);
                    print_endpoint_request_detail(&request);
                }
            }
            EndpointAction::DeleteRequest {
                endpoint_id,
                request_id,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                client
                    .delete_endpoint_request(&endpoint_id, &request_id)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "deleted",
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id,
                        "request_id": request_id
                    }))?;
                } else {
                    println!(
                        "🗑  Deleted request: {} {}",
                        request_id.bold(),
                        format!(
                            "(endpoint {}, organization {})",
                            endpoint_id, organization_id
                        )
                        .dim()
                    );
                }
            }
            EndpointAction::ForwardRequest {
                endpoint_id,
                request_id,
                target_url,
                method,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let normalized_method = normalize_http_method(method)?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let response = client
                    .forward_endpoint_request(
                        &endpoint_id,
                        &request_id,
                        &target_url,
                        normalized_method.as_deref(),
                    )
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id,
                        "request_id": request_id,
                        "forward": response
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Endpoint:", &endpoint_id);
                    print_context("Request:", &request_id);
                    println!(
                        "✅ Forward accepted: {} (status: {}, target: {})",
                        response.forward_id.bold(),
                        response.status.bold(),
                        response.target_url.underlined()
                    );
                }
            }
            EndpointAction::Forwards {
                endpoint_id,
                request_id,
                page,
                page_size,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let forwards = client
                    .list_endpoint_request_forwards(&endpoint_id, &request_id, page, page_size)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "endpoint_id": endpoint_id,
                        "request_id": request_id,
                        "forwards": forwards
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Endpoint:", &endpoint_id);
                    print_context("Request:", &request_id);
                    print_endpoint_request_forwards(&forwards);
                }
            }
            EndpointAction::Forward { forward_id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let forward = client.get_forward(&forward_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "forward": forward
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_forward_detail(&forward);
                }
            }
        },
        Commands::StaticTunnel { action } => match action {
            StaticTunnelAction::List { org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let tunnels = client.list_static_tunnels(&organization_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "tunnels": tunnels
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_static_tunnels(&tunnels);
                }
            }
            StaticTunnelAction::Create { slug, name, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let created = client
                    .create_static_tunnel(&organization_id, &slug, name.as_deref())
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "result": created
                    }))?;
                } else {
                    println!(
                        "✅ Created static tunnel: {} {}",
                        created.static_tunnel.slug.bold(),
                        format!("({})", created.static_tunnel.id).dim()
                    );
                    if let Some(message) = created.message {
                        println!("   {}", message.dim());
                    }
                }
            }
            StaticTunnelAction::Delete { slug_id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let response = client
                    .delete_static_tunnel(&organization_id, &slug_id)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "slug_id": slug_id,
                        "status": "deleted",
                        "message": response.message
                    }))?;
                } else {
                    println!(
                        "🗑  {}",
                        response
                            .message
                            .unwrap_or_else(|| "Static tunnel deleted.".to_string())
                    );
                }
            }
        },
        Commands::Anon { action } => match action {
            AnonAction::Create { ttl } => {
                let client = ApiClient::unauthenticated()?;
                let endpoint = client.create_anon_endpoint(ttl).await?;
                if json {
                    print_json(&endpoint)?;
                } else {
                    println!("✅ Created anonymous endpoint:");
                    println!("  {} {}", "ID:".bold(), endpoint.id);
                    println!(
                        "  {} {}",
                        "Webhook URL:".bold(),
                        endpoint.webhook_url.as_str().underlined()
                    );
                    println!("  {} {}", "Expires At:".bold(), endpoint.expires_at.dim());
                    println!();
                    println!(
                        "  {} {}",
                        "Viewer Token:".bold(),
                        endpoint.viewer_token.as_str().yellow()
                    );
                    println!(
                        "  {}",
                        "Save this token! You need it to list captured events.".dim()
                    );
                }
            }
            AnonAction::Show { id } => {
                let client = ApiClient::unauthenticated()?;
                let status = client.get_anon_endpoint(&id).await?;
                if json {
                    print_json(&status)?;
                } else {
                    println!("  {} {}", "ID:".bold(), status.id);
                    let active_str = if status.active {
                        "active".green().to_string()
                    } else {
                        "expired".red().to_string()
                    };
                    println!("  {} {}", "Status:".bold(), active_str);
                    if let Some(expires) = status.expires_at.as_deref() {
                        println!("  {} {}", "Expires At:".bold(), expires.dim());
                    }
                    if let Some(url) = status.webhook_url.as_deref() {
                        println!("  {} {}", "Webhook URL:".bold(), url.underlined());
                    }
                    if let Some(error) = status.error.as_deref() {
                        println!("  {} {}", "Error:".red().bold(), error);
                    }
                }
            }
            AnonAction::Events {
                endpoint_id,
                token,
                page,
                page_size,
            } => {
                let client = ApiClient::with_organization(token, None)?;
                let response = client
                    .list_anon_events(&endpoint_id, page, page_size)
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "endpoint_id": endpoint_id,
                        "events": response
                    }))?;
                } else {
                    print_context("Endpoint:", &endpoint_id);
                    print_anon_events(&response);
                }
            }
            AnonAction::Event {
                endpoint_id,
                event_id,
                token,
            } => {
                let client = ApiClient::with_organization(token, None)?;
                let event = client.get_anon_event(&endpoint_id, &event_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "endpoint_id": endpoint_id,
                        "event": event
                    }))?;
                } else {
                    print_context("Endpoint:", &endpoint_id);
                    print_anon_event_detail(&event);
                }
            }
        },
        Commands::Share { action } => match action {
            ShareAction::Create {
                debug_request_id,
                expires_in_hours,
                password,
                include_forwards,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let shared = client
                    .create_shared_request(
                        &debug_request_id,
                        expires_in_hours,
                        password.as_deref(),
                        include_forwards,
                    )
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "shared_request": shared
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    println!("✅ Created shared link:");
                    println!("  {} {}", "Share Token:".bold(), shared.share_token);
                    if let Some(url) = shared.share_url.as_deref() {
                        println!("  {} {}", "Share URL:".bold(), url.underlined());
                    }
                    println!("  {} {}", "Request ID:".bold(), shared.debug_request_id);
                    if shared.password_protected {
                        println!("  {} {}", "Password:".bold(), "protected".yellow());
                    }
                    if let Some(expires) = shared.expires_at.as_deref() {
                        println!("  {} {}", "Expires At:".bold(), expires.dim());
                    }
                    println!(
                        "  {} {}",
                        "Include Forwards:".bold(),
                        shared.include_forwards
                    );
                }
            }
            ShareAction::List {
                debug_request_id,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let shares = client.list_shared_requests(&debug_request_id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "debug_request_id": debug_request_id,
                        "shares": shares
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Request:", &debug_request_id);
                    print_shared_requests(&shares);
                }
            }
            ShareAction::Show { token } => {
                let client = ApiClient::unauthenticated()?;
                let data = client.get_shared_request(&token).await?;
                if json {
                    print_json(&data)?;
                } else {
                    // Check if it's a protected share
                    if data.get("protected").and_then(|v| v.as_bool()) == Some(true) {
                        println!("{} This share is password-protected.", "🔒".bold());
                        if let Some(expires) = data.get("expires_at").and_then(|v| v.as_str()) {
                            println!("  {} {}", "Expires At:".bold(), expires.dim());
                        }
                        println!(
                            "\n  {}",
                            "Use the web UI or API to authenticate with the password.".dim()
                        );
                    } else {
                        print_shared_request_full(&data);
                    }
                }
            }
            ShareAction::Revoke { token, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let access_token = ensure_valid_token(&mut config).await?;
                let client =
                    ApiClient::with_organization(access_token, Some(organization_id.clone()))?;
                client.revoke_shared_request(&token).await?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "revoked",
                        "organization_id": organization_id,
                        "share_token": token
                    }))?;
                } else {
                    println!(
                        "🗑  Revoked share: {} {}",
                        token.bold(),
                        format!("(organization {})", organization_id).dim()
                    );
                }
            }
        },
        Commands::Monitor { action } => match action {
            MonitorAction::Create {
                name,
                url,
                method,
                expected_status,
                interval,
                body_contains,
                body,
                failure_threshold,
                email,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;

                let mut params = serde_json::json!({
                    "name": name,
                    "url": url,
                    "method": method.to_lowercase(),
                    "expected_status_code": expected_status,
                    "check_interval": interval,
                    "failure_threshold": failure_threshold,
                    "email_enabled": email,
                });
                if let Some(bc) = body_contains {
                    params["body_contains"] = serde_json::Value::String(bc);
                }
                if let Some(b) = body {
                    params["body"] = serde_json::Value::String(b);
                }

                let monitor = client.create_uptime_monitor(&params).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "monitor": monitor
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    println!("✅ Created uptime monitor:");
                    print_monitor_detail(&monitor);
                }
            }
            MonitorAction::List { org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let monitors = client.list_uptime_monitors().await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "monitors": monitors
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_monitors(&monitors);
                }
            }
            MonitorAction::Show { id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let monitor = client.get_uptime_monitor(&id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "monitor": monitor
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_monitor_detail(&monitor);
                }
            }
            MonitorAction::Update {
                id,
                name,
                url,
                method,
                expected_status,
                interval,
                enabled,
                failure_threshold,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;

                let mut params = serde_json::Map::new();
                if let Some(v) = name {
                    params.insert("name".into(), serde_json::Value::String(v));
                }
                if let Some(v) = url {
                    params.insert("url".into(), serde_json::Value::String(v));
                }
                if let Some(v) = method {
                    params.insert("method".into(), serde_json::Value::String(v.to_lowercase()));
                }
                if let Some(v) = expected_status {
                    params.insert("expected_status_code".into(), v.into());
                }
                if let Some(v) = interval {
                    params.insert("check_interval".into(), v.into());
                }
                if let Some(v) = enabled {
                    params.insert("enabled".into(), serde_json::Value::Bool(v));
                }
                if let Some(v) = failure_threshold {
                    params.insert("failure_threshold".into(), v.into());
                }

                if params.is_empty() {
                    return Err(anyhow!(
                        "No fields to update. Use --name, --url, --method, --expected-status, --interval, --enabled, or --failure-threshold."
                    ));
                }

                let monitor = client
                    .update_uptime_monitor(&id, &serde_json::Value::Object(params))
                    .await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "monitor": monitor
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    println!("✅ Updated monitor:");
                    print_monitor_detail(&monitor);
                }
            }
            MonitorAction::Delete { id, org } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                client.delete_uptime_monitor(&id).await?;
                if json {
                    print_json(&serde_json::json!({
                        "status": "deleted",
                        "organization_id": organization_id,
                        "monitor_id": id
                    }))?;
                } else {
                    println!(
                        "🗑  Deleted monitor: {} {}",
                        id.bold(),
                        format!("(organization {})", organization_id).dim()
                    );
                }
            }
            MonitorAction::Checks {
                id,
                page,
                page_size,
                org,
            } => {
                let mut config = config::Config::load()?;
                let organization_id = require_organization(org, &config)?;
                let token = ensure_valid_token(&mut config).await?;
                let client = ApiClient::with_organization(token, Some(organization_id.clone()))?;
                let response = client.list_uptime_checks(&id, page, page_size).await?;
                if json {
                    print_json(&serde_json::json!({
                        "organization_id": organization_id,
                        "monitor_id": id,
                        "checks": response
                    }))?;
                } else {
                    print_context("Organization:", &organization_id);
                    print_context("Monitor:", &id);
                    print_uptime_checks(&response);
                }
            }
        },
        Commands::Completions { shell } => {
            use clap_complete::generate;
            use clap_complete::shells::{Bash, Elvish, Fish, PowerShell, Zsh};

            let mut command = Cli::command();
            let bin_name = command.get_name().to_string();
            let mut stdout = io::stdout();

            match shell {
                CompletionShell::Bash => generate(Bash, &mut command, bin_name, &mut stdout),
                CompletionShell::Zsh => generate(Zsh, &mut command, bin_name, &mut stdout),
                CompletionShell::Fish => generate(Fish, &mut command, bin_name, &mut stdout),
                CompletionShell::PowerShell => {
                    generate(PowerShell, &mut command, bin_name, &mut stdout)
                }
                CompletionShell::Elvish => generate(Elvish, &mut command, bin_name, &mut stdout),
            }
        }
        Commands::Update => {
            updater::run_self_update(json).await?;
        }
        Commands::Tunnel {
            port,
            host,
            org,
            slug,
        } => {
            // Initialize logging for tunnel
            let log_config = LogConfig {
                level: log_level.clone(),
                output_to_stdout: false, // Disable stdout logging for TUI
                directory: log_dir
                    .clone()
                    .unwrap_or_else(|| LogConfig::default().directory),
                ..Default::default()
            };
            let _logger = Logger::new(log_config)?;

            // Load config for auth token
            let config = config::Config::load()?;

            // Check if authenticated
            if !config.is_token_valid() {
                eprintln!(
                    "❌ Not authenticated. Please run 'hooklistener login' to authenticate first."
                );
                std::process::exit(1);
            }

            let selected_org = resolve_tunnel_org(org, &config);

            let access_token = config
                .access_token
                .ok_or_else(|| anyhow::anyhow!("No access token found"))?;

            // Setup TUI for tunnel command
            let mut terminal = setup_terminal()?;
            let mut app = App::new()?;

            // Set app state to tunneling
            app.state = AppState::Tunneling;
            app.tunnel_local_host = host.clone();
            app.tunnel_local_port = port;
            // Prefer explicit CLI org, then fall back to configured organization.
            app.tunnel_org_id = selected_org.clone();
            app.tunnel_requested_slug = slug.clone();

            // Create channel for tunnel events
            let (event_tx, event_rx) = mpsc::channel(100);

            // Create and spawn tunnel forwarder manager
            let reconnect_tx = spawn_tunnel_forwarder_manager(
                access_token,
                host,
                port,
                selected_org,
                slug,
                event_tx.clone(),
            );

            let res = run_app(
                &mut terminal,
                &mut app,
                event_rx,
                Some(reconnect_tx),
                Some(event_tx),
                Some(logo::spawn_logo_animation()),
            )
            .await;

            restore_terminal(&mut terminal)?;

            if let Err(err) = res {
                error!(error = %err, "Application terminated with error");
                display_error(&err);
            }
        }
    }

    // Await background version check and show notification if update is available
    if let Some(handle) = update_handle
        && let Ok(Ok(Some(new_version))) =
            tokio::time::timeout(Duration::from_millis(500), handle).await
    {
        updater::persist_check_result(Some(&new_version));
        updater::print_update_notification(&new_version);
    }

    Ok(())
}

async fn run_login_flow(force_reauth: bool) -> Result<()> {
    let mut config = config::Config::load()?;

    if config.is_token_valid() && !force_reauth {
        println!("\n  ✅ You're already authenticated.\n");
        println!(
            "  Run {} to start forwarding webhooks.",
            "hooklistener listen <endpoint>".bold()
        );
        println!(
            "  Use {} if you need to re-authenticate.\n",
            "hooklistener login --force".bold()
        );
        return Ok(());
    }

    if force_reauth {
        config.clear_token();
        config.save()?;
    }

    let mut device_flow = auth::DeviceCodeFlow::new(api::default_base_url());

    let user_code = device_flow.initiate_device_flow().await?;
    let display_code = device_flow
        .format_user_code()
        .unwrap_or_else(|| user_code.clone());
    let portal_url = device_portal_url();

    let clipboard_ok = arboard::Clipboard::new()
        .and_then(|mut cb| cb.set_text(&display_code))
        .is_ok();

    println!("\n  🔐 {}\n", "Hooklistener Login".bold());
    println!("  Open:  {}", portal_url.as_str().underlined());
    print!("  Code:  {}", display_code.bold());
    if clipboard_ok {
        print!("  {}", "(copied to clipboard)".dim());
    }
    println!("\n");

    let spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"];
    let mut spinner_idx: usize = 0;
    let mut stdout = io::stdout();

    let mut poll_interval = tokio::time::interval(Duration::from_secs(5));
    poll_interval.tick().await; // consume the immediate first tick

    loop {
        // Poll the API
        match device_flow.poll_for_authorization().await {
            Ok(Some(token_response)) => {
                execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
                let access_expires_at = match token_response.expires_in {
                    Some(secs) => Utc::now() + ChronoDuration::seconds(secs as i64),
                    None => Utc::now() + ChronoDuration::days(SESSION_TOKEN_VALIDITY_DAYS),
                };
                let refresh_expires_at = token_response
                    .refresh_expires_in
                    .map(|secs| Utc::now() + ChronoDuration::seconds(secs as i64));
                config.set_tokens(
                    token_response.access_token,
                    access_expires_at,
                    token_response.refresh_token,
                    refresh_expires_at,
                );
                config.save()?;
                println!("  ✅ Authentication successful!\n");
                println!(
                    "  Run {} to forward webhooks.\n",
                    "hooklistener listen <endpoint>".bold()
                );
                return Ok(());
            }
            Ok(None) => {
                if let Some(remaining) = device_flow.time_remaining()
                    && remaining == ChronoDuration::zero()
                {
                    execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
                    return Err(anyhow!(
                        "Device code expired before authorization completed. Please run `hooklistener login` again."
                    ));
                }
            }
            Err(err) => {
                execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
                return Err(anyhow!("Authentication failed: {}", err));
            }
        }

        // Animate spinner until next poll
        loop {
            let spinner = spinner_chars[spinner_idx % spinner_chars.len()];
            spinner_idx = (spinner_idx + 1) % spinner_chars.len();

            let status = if let Some(remaining) = device_flow.time_remaining() {
                let minutes = remaining.num_minutes();
                let seconds = remaining.num_seconds() % 60;
                let timer = if minutes > 0 {
                    format!("{minutes}m {seconds:02}s")
                } else {
                    format!("{seconds}s")
                };
                format!("  {spinner} Waiting for approval... {}", timer.dim())
            } else {
                format!("  {spinner} Waiting for approval...")
            };

            execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
            print!("{status}");
            stdout.flush()?;

            tokio::select! {
                _ = sleep(Duration::from_millis(80)) => continue,
                _ = poll_interval.tick() => break,
            }
        }
    }
}

fn device_portal_url() -> String {
    std::env::var("HOOKLISTENER_DEVICE_PORTAL_URL")
        .unwrap_or_else(|_| "https://app.hooklistener.com/device-codes".to_string())
}

fn resolve_tunnel_org(cli_org: Option<String>, config: &config::Config) -> Option<String> {
    cli_org.or_else(|| config.selected_organization_id.clone())
}

async fn ensure_valid_token(config: &mut config::Config) -> Result<String> {
    // 1. If access token is still valid, return it
    if config.is_token_valid() {
        return config
            .access_token
            .clone()
            .ok_or_else(|| anyhow!("No access token found. Please run `hooklistener login`."));
    }

    // 2. If refresh token is valid, try refreshing
    if let Some(ref refresh_token) = config.refresh_token.clone()
        && config.is_refresh_token_valid()
        && let Ok(response) = api::refresh_access_token(refresh_token).await
    {
        let expires_at = Utc::now() + ChronoDuration::seconds(response.expires_in as i64);
        config.set_tokens(
            response.access_token.clone(),
            expires_at,
            config.refresh_token.clone(),
            config.refresh_token_expires_at,
        );
        config.save()?;
        return Ok(response.access_token);
    }

    // 3. No valid tokens
    Err(anyhow!(
        "Session expired. Please run `hooklistener login` to re-authenticate."
    ))
}

fn require_organization(cli_org: Option<String>, config: &config::Config) -> Result<String> {
    resolve_tunnel_org(cli_org, config).ok_or_else(|| {
        anyhow!(
            "No organization selected. Use `hooklistener org use <organization-id>` or pass --org."
        )
    })
}

/// Print a dim context line like "Organization: abc123".
fn print_context(label: &str, value: &str) {
    println!("{} {}", label.dim(), value.dim());
}

/// Print a pagination footer.
fn print_pagination(p: &api::Pagination) {
    println!(
        "{}",
        format!(
            "Page {}/{} (page_size={}, total={})",
            p.page, p.total_pages, p.page_size, p.total_count
        )
        .dim()
    );
}

/// Print a key-value map (headers, query params) with a bold section label.
fn print_key_value_map(
    label: &str,
    map: &std::collections::HashMap<String, serde_json::Value>,
    separator: &str,
) {
    if map.is_empty() {
        println!("{} {}", label.bold(), "(none)".dim());
    } else {
        println!("{}", label.bold());
        for (key, value) in map {
            println!("  {}{}{}", key.as_str().dim(), separator, value);
        }
    }
}

/// Print a body section, showing "(empty)" when the body is absent or blank.
fn print_body_section(label: &str, body: Option<&str>) {
    match body {
        Some(body) if !body.is_empty() => {
            println!("{}", label.bold());
            println!("{}", body);
        }
        _ => println!("{} {}", label.bold(), "(empty)".dim()),
    }
}

/// Create a pre-configured table with the standard preset and dynamic content arrangement.
fn new_table(headers: &[&str]) -> Table {
    let mut table = Table::new();
    table
        .load_preset(UTF8_FULL_CONDENSED)
        .set_content_arrangement(ContentArrangement::Dynamic)
        .set_header(headers.to_vec());
    table
}

fn print_organizations(organizations: &[api::Organization], selected_org: Option<&str>) {
    if organizations.is_empty() {
        println!("{}", "No organizations found.".dim());
        return;
    }

    let mut table = new_table(&["", "ID", "Name"]);
    for org in organizations {
        let marker = if selected_org.is_some_and(|id| id == org.id) {
            "*"
        } else {
            ""
        };
        table.add_row(vec![marker, &org.id, &org.name]);
    }
    println!("{table}");
}

fn print_endpoints(endpoints: &[api::DebugEndpointSummary]) {
    if endpoints.is_empty() {
        println!("{}", "No debug endpoints found.".dim());
        return;
    }

    let mut table = new_table(&["ID", "Slug", "Status", "Webhook URL", "Name"]);
    for endpoint in endpoints {
        table.add_row(vec![
            &endpoint.id,
            &endpoint.slug,
            &endpoint.status,
            &endpoint.webhook_url,
            &endpoint.name,
        ]);
    }
    println!("{table}");
}

fn print_endpoint_requests(response: &api::EndpointRequestsResponse) {
    if response.data.is_empty() {
        println!("{}", "No requests found.".dim());
        return;
    }

    let mut table = new_table(&["ID", "Method", "URL", "Remote"]);
    for request in &response.data {
        table.add_row(vec![
            &request.id,
            &request.method,
            &request.url,
            &request.remote_addr,
        ]);
    }
    println!("{table}");
    print_pagination(&response.pagination);
}

fn print_endpoint_request_detail(request: &api::DebugRequestDetail) {
    println!("{} {}", "Request ID:".bold(), request.id);
    println!("{} {}", "Method:".bold(), request.method.as_str().bold());
    if let Some(path) = request.path.as_deref() {
        println!("{} {}", "Path:".bold(), path);
    }
    println!("{} {}", "URL:".bold(), request.url);

    if let Some(status_remote) = request.remote_addr.as_deref() {
        println!("{} {}", "Remote Address:".bold(), status_remote);
    }
    if let Some(content_length) = request.content_length {
        println!("{} {}", "Content Length:".bold(), content_length);
    }
    if let Some(created_at) = request.created_at.as_deref() {
        println!("{} {}", "Created At:".bold(), created_at.dim());
    }

    println!();
    print_key_value_map("Headers:", &request.headers, ": ");

    println!();
    print_key_value_map("Query Params:", &request.query_params, "=");

    println!();
    print_body_section(
        "Body:",
        request.body.as_deref().or(request.body_preview.as_deref()),
    );
}

fn print_endpoint_request_forwards(response: &api::EndpointRequestForwardsResponse) {
    if response.data.is_empty() {
        println!("{}", "No forwards found.".dim());
        return;
    }

    let mut table = new_table(&["ID", "Method", "Status", "Duration", "Target"]);
    for forward in &response.data {
        let status = forward
            .status_code
            .map(|c| c.to_string())
            .unwrap_or_else(|| "-".into());
        let duration = forward
            .duration_ms
            .map(|ms| format!("{ms}ms"))
            .unwrap_or_else(|| "-".into());
        let target = match forward.error_message.as_deref() {
            Some(err) => format!("{}\n  error: {err}", forward.target_url),
            None => forward.target_url.clone(),
        };
        table.add_row(vec![
            forward.id.as_str(),
            &forward.method,
            &status,
            &duration,
            &target,
        ]);
    }
    println!("{table}");
    print_pagination(&response.pagination);
}

fn print_forward_detail(forward: &api::DebugRequestForwardDetail) {
    println!("{} {}", "Forward ID:".bold(), forward.id);
    println!("{} {}", "Request ID:".bold(), forward.debug_request_id);
    println!("{} {}", "Target URL:".bold(), forward.target_url);
    println!("{} {}", "Method:".bold(), forward.method.as_str().bold());
    if let Some(status_code) = forward.status_code {
        println!("{} {}", "Status:".bold(), style_status_code(status_code));
    } else {
        println!("{} {}", "Status:".bold(), "(pending)".yellow());
    }
    if let Some(duration_ms) = forward.duration_ms {
        println!("{} {}ms", "Duration:".bold(), duration_ms);
    }
    if let Some(attempted_at) = forward.attempted_at.as_deref() {
        println!("{} {}", "Attempted At:".bold(), attempted_at.dim());
    }
    if let Some(error_message) = forward.error_message.as_deref() {
        println!("{} {}", "Error:".red().bold(), error_message);
    }

    println!();
    print_key_value_map("Request Headers:", &forward.request_headers, ": ");

    if forward
        .request_body
        .as_deref()
        .is_some_and(|b| !b.is_empty())
    {
        println!();
        print_body_section("Request Body:", forward.request_body.as_deref());
    }

    println!();
    print_key_value_map("Response Headers:", &forward.response_headers, ": ");

    if forward
        .response_body
        .as_deref()
        .is_some_and(|b| !b.is_empty())
    {
        println!();
        print_body_section("Response Body:", forward.response_body.as_deref());
    }
}

fn print_static_tunnels(response: &api::StaticTunnelsResponse) {
    if !response.static_tunnels.is_empty() {
        let mut table = new_table(&["ID", "Slug", "Name"]);
        for tunnel in &response.static_tunnels {
            let name = tunnel.name.as_deref().unwrap_or("");
            table.add_row(vec![tunnel.id.as_str(), &tunnel.slug, name]);
        }
        println!("{table}");
    }

    println!(
        "{}",
        format!("Used {}/{} static tunnels", response.used, response.limit).dim()
    );
}

fn print_anon_events(response: &api::AnonEventsResponse) {
    if response.data.is_empty() {
        println!("{}", "No events captured yet.".dim());
    } else {
        println!(
            "{}",
            format!("{:<36}  {:<7}  Received At", "ID", "Method").dim()
        );
        for event in &response.data {
            println!(
                "{:<36}  {:<7}  {}",
                event.id,
                event.method.as_str().bold(),
                event.inserted_at.as_deref().unwrap_or("-").dim()
            );
        }
    }
    print_pagination(&response.pagination);
}

fn print_anon_event_detail(event: &api::AnonEvent) {
    println!("{} {}", "Event ID:".bold(), event.id);
    println!("{} {}", "Endpoint ID:".bold(), event.endpoint_id);
    println!("{} {}", "Method:".bold(), event.method.as_str().bold());
    if let Some(status) = event.status.as_deref() {
        println!("{} {}", "Status:".bold(), status);
    }
    if let Some(inserted_at) = event.inserted_at.as_deref() {
        println!("{} {}", "Received At:".bold(), inserted_at.dim());
    }

    println!();
    print_key_value_map("Headers:", &event.headers, ": ");

    println!();
    print_body_section("Body:", event.body.as_deref());
}

fn print_shared_requests(shares: &[api::SharedRequestSummary]) {
    if shares.is_empty() {
        println!("{}", "No shares found.".dim());
        return;
    }

    println!(
        "{}",
        format!(
            "{:<36}  {:<32}  {:<5}  {:<6}  {:<10}  Expires At",
            "ID", "Token", "Fwds", "Views", "Protected"
        )
        .dim()
    );
    for share in shares {
        let protected = if share.password_protected {
            "yes".yellow().to_string()
        } else {
            "no".to_string()
        };
        let expires = share.expires_at.as_deref().unwrap_or("-");
        println!(
            "{:<36}  {:<32}  {:<5}  {:<6}  {:<10}  {}",
            share.id,
            share.share_token.as_str().bold(),
            if share.include_forwards { "yes" } else { "no" },
            share.view_count,
            protected,
            expires.dim()
        );
    }
}

fn print_shared_request_full(data: &serde_json::Value) {
    if let Some(token) = data.get("share_token").and_then(|v| v.as_str()) {
        println!("{} {}", "Share Token:".bold(), token);
    }
    if let Some(expires) = data.get("expires_at").and_then(|v| v.as_str()) {
        println!("{} {}", "Expires At:".bold(), expires.dim());
    }
    if let Some(views) = data.get("view_count").and_then(|v| v.as_u64()) {
        println!("{} {}", "Views:".bold(), views);
    }

    if let Some(request) = data.get("debug_request") {
        println!();
        println!("{}", "── Debug Request ──".bold());
        if let Some(id) = request.get("id").and_then(|v| v.as_str()) {
            println!("  {} {}", "ID:".bold(), id);
        }
        if let Some(method) = request.get("method").and_then(|v| v.as_str()) {
            println!("  {} {}", "Method:".bold(), method.bold());
        }
        if let Some(url) = request.get("url").and_then(|v| v.as_str()) {
            println!("  {} {}", "URL:".bold(), url);
        }
        if let Some(remote) = request.get("remote_addr").and_then(|v| v.as_str()) {
            println!("  {} {}", "Remote:".bold(), remote.dim());
        }
        if let Some(created) = request.get("created_at").and_then(|v| v.as_str()) {
            println!("  {} {}", "Created At:".bold(), created.dim());
        }

        // Headers
        if let Some(headers) = request.get("headers").and_then(|v| v.as_object())
            && !headers.is_empty()
        {
            println!();
            println!("  {}", "Headers:".bold());
            for (key, value) in headers {
                println!("    {}: {}", key.as_str().dim(), value);
            }
        }

        // Body
        let body = request
            .get("body")
            .and_then(|v| v.as_str())
            .or_else(|| request.get("body_preview").and_then(|v| v.as_str()));
        if let Some(body) = body
            && !body.is_empty()
        {
            println!();
            println!("  {}", "Body:".bold());
            println!("  {}", body);
        }
    }

    if let Some(forwards) = data.get("forwards").and_then(|v| v.as_array())
        && !forwards.is_empty()
    {
        println!();
        println!("{}", "── Forwards ──".bold());
        for fwd in forwards {
            let target = fwd
                .get("target_url")
                .and_then(|v| v.as_str())
                .unwrap_or("-");
            let method = fwd.get("method").and_then(|v| v.as_str()).unwrap_or("-");
            let status = fwd
                .get("status_code")
                .and_then(|v| v.as_u64())
                .map(|c| style_status_code(c as u16))
                .unwrap_or_else(|| "-".to_string());
            let duration = fwd
                .get("duration_ms")
                .and_then(|v| v.as_u64())
                .map(|ms| format!("{}ms", ms))
                .unwrap_or_else(|| "-".to_string());
            println!("  {} {} → {} ({})", method.bold(), status, target, duration);
        }
    }
}

fn style_monitor_status(status: Option<&str>) -> String {
    match status {
        Some("up") => "up".green().to_string(),
        Some("down") => "down".red().to_string(),
        Some(s) => s.yellow().to_string(),
        None => "pending".yellow().to_string(),
    }
}

fn print_monitors(monitors: &[api::UptimeMonitor]) {
    if monitors.is_empty() {
        println!("{}", "No uptime monitors found.".dim());
        return;
    }

    println!(
        "{}",
        format!(
            "{:<36}  {:<8}  {:<7}  {:<5}  {:<24}  Name",
            "ID", "Status", "Method", "Int", "URL"
        )
        .dim()
    );
    for m in monitors {
        let status = style_monitor_status(m.current_status.as_deref());
        let enabled_marker = if m.enabled { "" } else { " (disabled)" };
        let interval = m
            .check_interval
            .map(|i| format!("{}m", i))
            .unwrap_or_default();
        // Truncate URL for table display
        let url_display = if m.url.len() > 24 {
            format!("{}…", &m.url[..23])
        } else {
            m.url.clone()
        };
        println!(
            "{:<36}  {:<8}  {:<7}  {:<5}  {:<24}  {}{}",
            m.id,
            status,
            m.method.to_uppercase().bold(),
            interval,
            url_display,
            m.name,
            enabled_marker.dim()
        );
    }
}

fn print_monitor_detail(m: &api::UptimeMonitor) {
    println!("{} {}", "ID:".bold(), m.id);
    println!("{} {}", "Name:".bold(), m.name);
    println!("{} {}", "URL:".bold(), m.url.as_str().underlined());
    println!("{} {}", "Method:".bold(), m.method.to_uppercase().bold());

    let status = style_monitor_status(m.current_status.as_deref());
    let enabled = if m.enabled {
        "yes".green().to_string()
    } else {
        "no (paused)".yellow().to_string()
    };
    println!("{} {}", "Status:".bold(), status);
    println!("{} {}", "Enabled:".bold(), enabled);

    if let Some(code) = m.expected_status_code {
        println!("{} {}", "Expected Status:".bold(), code);
    }
    if let Some(ref bc) = m.body_contains {
        println!("{} {}", "Body Contains:".bold(), bc);
    }
    if let Some(interval) = m.check_interval {
        println!("{} {}m", "Check Interval:".bold(), interval);
    }
    if let Some(threshold) = m.failure_threshold {
        println!("{} {}", "Failure Threshold:".bold(), threshold);
    }
    if let Some(failures) = m.consecutive_failures {
        println!("{} {}", "Consecutive Failures:".bold(), failures);
    }

    println!(
        "{} email={}, slack={}",
        "Notifications:".bold(),
        if m.email_enabled { "on" } else { "off" },
        if m.slack_enabled { "on" } else { "off" }
    );

    if let Some(ref checked) = m.last_checked_at {
        println!("{} {}", "Last Checked:".bold(), checked.as_str().dim());
    }
    if let Some(ref changed) = m.last_status_change_at {
        println!(
            "{} {}",
            "Last Status Change:".bold(),
            changed.as_str().dim()
        );
    }
    if let Some(ref created) = m.created_at {
        println!("{} {}", "Created At:".bold(), created.as_str().dim());
    }
}

fn print_uptime_checks(response: &api::UptimeChecksResponse) {
    // Stats summary
    if let Some(ref stats) = response.stats {
        let uptime = stats
            .uptime_percentage
            .map(|p| {
                let s = format!("{:.2}%", p);
                if p >= 99.9 {
                    s.green().to_string()
                } else if p >= 95.0 {
                    s.yellow().to_string()
                } else {
                    s.red().to_string()
                }
            })
            .unwrap_or_else(|| "-".to_string());
        let avg_rt = stats
            .avg_response_time_ms
            .map(|ms| format!("{:.0}ms", ms))
            .unwrap_or_else(|| "-".to_string());
        println!(
            "{} {}  {} {}",
            "Uptime:".bold(),
            uptime,
            "Avg Response:".bold(),
            avg_rt
        );
        println!();
    }

    if response.data.is_empty() {
        println!("{}", "No checks recorded yet.".dim());
    } else {
        println!(
            "{}",
            format!(
                "{:<36}  {:<6}  {:<8}  {:<10}  Checked At",
                "ID", "Status", "Code", "Response"
            )
            .dim()
        );
        for check in &response.data {
            let status = match check.status.as_str() {
                "up" => "up".green().to_string(),
                "down" => "down".red().to_string(),
                s => s.to_string(),
            };
            let code = check
                .status_code
                .map(style_status_code)
                .unwrap_or_else(|| "-".to_string());
            let rt = check
                .response_time_ms
                .map(|ms| format!("{}ms", ms))
                .unwrap_or_else(|| "-".to_string());
            let checked = check.checked_at.as_deref().unwrap_or("-");

            println!(
                "{:<36}  {:<6}  {:<8}  {:<10}  {}",
                check.id,
                status,
                code,
                rt,
                checked.dim()
            );
            if let Some(ref err) = check.error_message {
                println!("  {} {}", "error:".red(), err);
            }
        }
    }

    print_pagination(&response.pagination);
}

fn spawn_tunnel_forwarder_manager(
    access_token: String,
    host: String,
    port: u16,
    org: Option<String>,
    slug: Option<String>,
    event_tx: mpsc::Sender<TunnelEvent>,
) -> mpsc::UnboundedSender<()> {
    let (reconnect_tx, mut reconnect_rx) = mpsc::unbounded_channel::<()>();

    tokio::spawn(async move {
        let mut worker = tokio::spawn(run_tunnel_forwarder_connection(
            access_token.clone(),
            host.clone(),
            port,
            org.clone(),
            slug.clone(),
            event_tx.clone(),
        ));

        while reconnect_rx.recv().await.is_some() {
            worker.abort();
            let _ = worker.await;

            // Collapse bursty manual reconnect presses into a single restart.
            while reconnect_rx.try_recv().is_ok() {}

            worker = tokio::spawn(run_tunnel_forwarder_connection(
                access_token.clone(),
                host.clone(),
                port,
                org.clone(),
                slug.clone(),
                event_tx.clone(),
            ));
        }

        worker.abort();
        let _ = worker.await;
    });

    reconnect_tx
}

async fn run_tunnel_forwarder_connection(
    access_token: String,
    host: String,
    port: u16,
    org: Option<String>,
    slug: Option<String>,
    event_tx: mpsc::Sender<TunnelEvent>,
) {
    let tunnel_forwarder =
        tunnel::TunnelForwarder::new(access_token, host, port, org, slug, event_tx);

    if let Err(e) = tunnel_forwarder
        .connect_with_reconnect(tunnel::ReconnectConfig::default())
        .await
    {
        error!("Tunnel forwarder error: {}", e);
    }
}

fn spawn_tunnel_replay(
    replay_request: app::TunnelReplayRequest,
    event_tx: mpsc::Sender<TunnelEvent>,
) {
    tokio::spawn(async move {
        let request_id = replay_request.request_id.clone();
        let replay_result = replay_request.send().await;
        let event = match replay_result {
            Ok(outcome) => TunnelEvent::ReplayCompleted {
                request_id,
                status: outcome.status,
                duration_ms: outcome.duration_ms,
            },
            Err(error) => TunnelEvent::ReplayFailed {
                request_id,
                error: error.to_string(),
            },
        };
        let _ = event_tx.send(event).await;
    });
}

async fn run_app<B: ratatui::backend::Backend + Send>(
    terminal: &mut Terminal<B>,
    app: &mut App,
    mut tunnel_rx: mpsc::Receiver<TunnelEvent>,
    tunnel_reconnect_tx: Option<mpsc::UnboundedSender<()>>,
    tunnel_event_tx: Option<mpsc::Sender<TunnelEvent>>,
    mut logo_rx: Option<watch::Receiver<String>>,
) -> Result<()>
where
    <B as ratatui::backend::Backend>::Error: std::error::Error + Send + Sync + 'static,
{
    // Ensure proper terminal cleanup on any exit
    let _cleanup = TerminalCleanup;

    loop {
        if let Some(logo_rx) = logo_rx.as_mut() {
            let should_update_logo =
                app.logo_frame.is_none() || logo_rx.has_changed().unwrap_or(false);
            if should_update_logo {
                app.logo_frame = Some(logo_rx.borrow_and_update().clone());
            }
        }

        terminal.draw(|frame| ui::draw(frame, app))?;

        // Update animations
        app.tick();

        if app.should_quit {
            break;
        }

        // Handle tunnel events
        while let Ok(event) = tunnel_rx.try_recv() {
            match event {
                TunnelEvent::Connecting => {
                    // Update UI to show connecting state
                }
                TunnelEvent::Connected => {
                    app.listening_connected = true;
                    app.listening_error = None;
                    app.tunnel_connected = true;
                    app.tunnel_connected_at = Some(std::time::Instant::now());
                }
                TunnelEvent::TunnelEstablished {
                    subdomain,
                    tunnel_id,
                    is_static,
                } => {
                    app.tunnel_subdomain = Some(subdomain);
                    app.tunnel_id = Some(tunnel_id);
                    app.tunnel_is_static = is_static;
                    app.tunnel_connected = true;
                    app.tunnel_connected_at = Some(std::time::Instant::now());
                }
                TunnelEvent::ConnectionError(err) => {
                    app.listening_connected = false;
                    app.listening_error = Some(err.clone());
                    app.tunnel_connected = false;
                    app.tunnel_error = Some(err);
                }
                TunnelEvent::Disconnected => {
                    app.listening_connected = false;
                    app.tunnel_connected = false;
                }
                TunnelEvent::WebhookReceived(request) => {
                    app.listening_requests.push(*request);
                    app.listening_stats.total_requests += 1;
                }
                TunnelEvent::RequestReceived {
                    request_id,
                    method,
                    path,
                    headers,
                    body,
                    query_string,
                } => {
                    use std::time::Instant;
                    let tunnel_request = app::TunnelRequest {
                        request_id,
                        method,
                        path,
                        received_at: Instant::now(),
                        status: None,
                        completed_at: None,
                        error: None,
                        headers,
                        body: app::truncate_body(body),
                        query_string,
                        response_headers: None,
                        response_body: None,
                        pinned: false,
                    };
                    app.push_tunnel_request(tunnel_request);
                    app.tunnel_stats.total += 1;
                }
                TunnelEvent::RequestForwarded {
                    request_id,
                    status,
                    duration_ms,
                    response_headers,
                    response_body,
                } => {
                    // Update the request in the list
                    if let Some(req) = app
                        .tunnel_requests
                        .iter_mut()
                        .find(|r| r.request_id == request_id)
                    {
                        req.status = Some(status);
                        req.completed_at = Some(std::time::Instant::now());
                        req.response_headers = Some(response_headers);
                        req.response_body = app::truncate_body(response_body);
                    }
                    app.tunnel_stats.record_response(status, duration_ms);
                }
                TunnelEvent::RequestFailed { request_id, error } => {
                    // Update the request in the list
                    if let Some(req) = app
                        .tunnel_requests
                        .iter_mut()
                        .find(|r| r.request_id == request_id)
                    {
                        req.error = Some(error);
                        req.completed_at = Some(std::time::Instant::now());
                    }
                    app.tunnel_stats.failed += 1;
                }
                TunnelEvent::ReplayCompleted {
                    request_id,
                    status,
                    duration_ms,
                } => {
                    app.tunnel_status_message = Some((
                        format!("Replayed {} {} {}ms", request_id, status, duration_ms),
                        std::time::Instant::now(),
                    ));
                }
                TunnelEvent::ReplayFailed { request_id, error } => {
                    app.tunnel_status_message = Some((
                        format!("Replay {} failed: {}", request_id, error),
                        std::time::Instant::now(),
                    ));
                }
                TunnelEvent::ForwardSuccess => {
                    app.listening_stats.successful_forwards += 1;
                }
                TunnelEvent::ForwardError => {
                    app.listening_stats.failed_forwards += 1;
                }
                TunnelEvent::Reconnecting {
                    attempt,
                    max_attempts,
                    next_retry_in_secs,
                } => {
                    let msg = format!(
                        "Reconnecting (attempt {}/{})... next retry in {}s",
                        attempt, max_attempts, next_retry_in_secs
                    );
                    app.listening_connected = false;
                    app.listening_error = Some(msg.clone());
                    app.tunnel_connected = false;
                    app.tunnel_error = Some(msg);
                }
                TunnelEvent::ReconnectFailed { reason } => {
                    let msg = format!("Connection lost: {}", reason);
                    app.listening_connected = false;
                    app.listening_error = Some(msg.clone());
                    app.tunnel_connected = false;
                    app.tunnel_error = Some(msg);
                }
            }
        }

        // Handle async states that don't require user input
        if matches!(app.state, AppState::ForwardingRequest) {
            app.forward_request().await?;
            continue;
        }

        if event::poll(Duration::from_millis(100))?
            && let Event::Key(key) = event::read()?
            && key.kind == KeyEventKind::Press
        {
            app.handle_key_event(key)?;

            if app.take_tunnel_reconnect_request()
                && let Some(tx) = tunnel_reconnect_tx.as_ref()
                && tx.send(()).is_err()
            {
                app.tunnel_error = Some("Failed to request tunnel reconnect".to_string());
            }

            if let Some(replay_request) = app.take_tunnel_replay_request() {
                if let Some(tx) = tunnel_event_tx.as_ref() {
                    spawn_tunnel_replay(replay_request, tx.clone());
                } else {
                    app.tunnel_status_message =
                        Some(("Replay unavailable".to_string(), std::time::Instant::now()));
                }
            }

            if matches!(app.state, AppState::ForwardingRequest) {
                app.forward_request().await?;
            }
        }
    }

    Ok(())
}

fn style_status_code(code: u16) -> String {
    let s = code.to_string();
    match code {
        200..=299 => s.green().to_string(),
        300..=399 => s.yellow().to_string(),
        400..=599 => s.red().to_string(),
        _ => s,
    }
}

fn error_hint(err: &anyhow::Error) -> Option<&str> {
    if let Some(e) = err.downcast_ref::<errors::ApiError>() {
        return e.hint();
    }
    if let Some(e) = err.downcast_ref::<errors::TunnelError>() {
        return e.hint();
    }
    if let Some(e) = err.downcast_ref::<errors::ConfigError>() {
        return e.hint();
    }
    if let Some(e) = err.downcast_ref::<errors::UpdateError>() {
        return e.hint();
    }
    None
}

fn display_error(err: &anyhow::Error) {
    eprintln!("{} {}", "Error:".red().bold(), err);
    if let Some(hint) = error_hint(err) {
        eprintln!("{} {}", "Hint:".yellow().bold(), hint);
    }
    for cause in err.chain().skip(1) {
        eprintln!("{} {}", "Caused by:".dim(), cause);
    }
}

fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let terminal = Terminal::new(backend)?;
    Ok(terminal)
}

struct TerminalCleanup;

impl Drop for TerminalCleanup {
    fn drop(&mut self) {
        // Ensure terminal is always restored, even on panic
        let _ = disable_raw_mode();
        let _ = execute!(io::stdout(), LeaveAlternateScreen);
        let _ = execute!(io::stdout(), Show);
    }
}

fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;
    Ok(())
}

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

    fn make_config(selected_org: Option<&str>) -> config::Config {
        config::Config {
            selected_organization_id: selected_org.map(String::from),
            ..config::Config::default()
        }
    }

    #[test]
    fn resolve_tunnel_org_prefers_cli_arg() {
        let config = make_config(Some("org-config"));
        let resolved = resolve_tunnel_org(Some("org-cli".to_string()), &config);
        assert_eq!(resolved.as_deref(), Some("org-cli"));
    }

    #[test]
    fn resolve_tunnel_org_falls_back_to_config() {
        let config = make_config(Some("org-config"));
        let resolved = resolve_tunnel_org(None, &config);
        assert_eq!(resolved.as_deref(), Some("org-config"));
    }

    #[test]
    fn resolve_tunnel_org_none_when_not_set() {
        let config = make_config(None);
        let resolved = resolve_tunnel_org(None, &config);
        assert!(resolved.is_none());
    }

    #[test]
    fn require_organization_uses_cli_value() {
        let config = make_config(Some("org-config"));
        let org = require_organization(Some("org-cli".to_string()), &config).unwrap();
        assert_eq!(org, "org-cli");
    }

    #[test]
    fn require_organization_errors_when_missing() {
        let config = make_config(None);
        let err = require_organization(None, &config).unwrap_err();
        assert!(
            err.to_string().contains("No organization selected"),
            "unexpected error: {}",
            err
        );
    }

    #[tokio::test]
    async fn ensure_valid_token_returns_error_when_expired() {
        let mut config = make_config(Some("org-config"));
        let err = ensure_valid_token(&mut config).await.unwrap_err();
        assert!(
            err.to_string().contains("Session expired"),
            "unexpected error: {}",
            err
        );
    }

    #[test]
    fn normalize_http_method_accepts_lowercase() {
        let method = normalize_http_method(Some("post".to_string())).unwrap();
        assert_eq!(method.as_deref(), Some("POST"));
    }

    #[test]
    fn normalize_http_method_rejects_invalid_values() {
        let err = normalize_http_method(Some("TRACE".to_string())).unwrap_err();
        assert!(
            err.to_string().contains("Invalid HTTP method"),
            "unexpected error: {}",
            err
        );
    }
}
