//! Full-screen Ratatui TUI for quicproquo.
//!
//! Layout:
//!   ┌──────────────┬──────────────────────────────────────────┐
//!   │  Channels    │           Messages                       │
//!   │  (20%)       │           (80%)                          │
//!   │              │                                          │
//!   │              ├──────────────────────────────────────────┤
//!   │              │  Input bar                               │
//!   └──────────────┴──────────────────────────────────────────┘
//!
//! Keyboard:
//!   Enter        — send message
//!   Up / Down    — scroll message history
//!   Tab          — next channel
//!   Shift+Tab    — prev channel
//!   Ctrl+C / q   — quit

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

use anyhow::Context;
use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
    Frame, Terminal,
};
use tokio::sync::mpsc;
use tokio::time::interval;

use crate::{ClientAuth, init_auth};
use super::commands::{opaque_login, opaque_register};
use super::conversation::{now_ms, ConversationId, StoredMessage};
use super::rpc::{
    connect_node, enqueue, fetch_hybrid_key, fetch_wait, try_hybrid_decrypt, upload_hybrid_key,
    upload_key_package,
};
use super::session::SessionState;
use super::state::load_or_init_state;
use super::token_cache::{load_cached_session, save_cached_session};

use quicproquo_core::{
    AppMessage, DiskKeyStore, GroupMember, IdentityKeypair, ReceivedMessage,
    hybrid_encrypt, parse as parse_app_msg, serialize_chat,
};
use quicproquo_proto::node_capnp::node_service;

// ── App events ───────────────────────────────────────────────────────────────

/// Events sent from background tasks into the main TUI loop.
enum TuiEvent {
    /// A key event from the terminal.
    Key(event::KeyEvent),
    /// New messages received from the server (conv_id, sender_short, body).
    NewMessages(Vec<(ConversationId, String, String)>),
    /// Tick — redraw periodically even if nothing happened.
    Tick,
}

// ── Display message ───────────────────────────────────────────────────────────

#[derive(Clone)]
struct DisplayMessage {
    sender: String,
    body: String,
    timestamp_ms: u64,
    is_outgoing: bool,
}

// ── App state ─────────────────────────────────────────────────────────────────

struct App {
    /// Channel (conversation) names shown in the sidebar.
    channel_names: Vec<String>,
    /// Conversation IDs, parallel to `channel_names`.
    channel_ids: Vec<ConversationId>,
    /// Index of the selected channel in the sidebar.
    selected_channel: usize,
    /// Messages for the currently active channel.
    messages: Vec<DisplayMessage>,
    /// Current input buffer.
    input: String,
    /// Scroll offset (0 = bottom).
    scroll_offset: usize,
    /// Whether the user has requested quit.
    should_quit: bool,
    /// Short identity string for display.
    identity_short: String,
}

impl App {
    fn new(session: &SessionState) -> anyhow::Result<Self> {
        let convs = session.conv_store.list_conversations()?;
        let channel_names: Vec<String> = convs.iter().map(|c| c.display_name.clone()).collect();
        let channel_ids: Vec<ConversationId> = convs.iter().map(|c| c.id.clone()).collect();

        Ok(Self {
            channel_names,
            channel_ids,
            selected_channel: 0,
            messages: Vec::new(),
            input: String::new(),
            scroll_offset: 0,
            should_quit: false,
            identity_short: session.identity_short(),
        })
    }

    fn active_conv_id(&self) -> Option<&ConversationId> {
        self.channel_ids.get(self.selected_channel)
    }

    /// Reload messages for the currently selected channel from the session store.
    fn reload_messages(&mut self, session: &SessionState) -> anyhow::Result<()> {
        let conv_id = match self.active_conv_id() {
            Some(id) => id.clone(),
            None => {
                self.messages.clear();
                return Ok(());
            }
        };
        let stored = session.conv_store.load_recent_messages(&conv_id, 200)?;
        self.messages = stored
            .into_iter()
            .map(|m| {
                let sender = if m.is_outgoing {
                    format!("me({})", &self.identity_short)
                } else if let Some(name) = &m.sender_name {
                    name.clone()
                } else {
                    // Shorten sender key to 8 hex chars.
                    let hex_short = hex::encode(&m.sender_key[..m.sender_key.len().min(4)]);
                    format!("{hex_short}")
                };
                DisplayMessage {
                    sender,
                    body: m.body,
                    timestamp_ms: m.timestamp_ms,
                    is_outgoing: m.is_outgoing,
                }
            })
            .collect();
        // Reset scroll to bottom on channel switch.
        self.scroll_offset = 0;
        Ok(())
    }

    fn select_next_channel(&mut self, session: &SessionState) {
        if self.channel_names.is_empty() {
            return;
        }
        self.selected_channel = (self.selected_channel + 1) % self.channel_names.len();
        let _ = self.reload_messages(session);
    }

    fn select_prev_channel(&mut self, session: &SessionState) {
        if self.channel_names.is_empty() {
            return;
        }
        if self.selected_channel == 0 {
            self.selected_channel = self.channel_names.len() - 1;
        } else {
            self.selected_channel -= 1;
        }
        let _ = self.reload_messages(session);
    }

    fn scroll_up(&mut self) {
        self.scroll_offset = self.scroll_offset.saturating_add(1);
    }

    fn scroll_down(&mut self) {
        self.scroll_offset = self.scroll_offset.saturating_sub(1);
    }

    /// Append newly received messages to the in-memory list (no DB reload needed
    /// since we already have them from the poll task, but we also save them via
    /// the session so they appear on reload).
    fn append_messages(&mut self, msgs: Vec<(ConversationId, String, String)>) {
        let active = self.active_conv_id().cloned();
        for (conv_id, sender, body) in msgs {
            if active.as_ref() == Some(&conv_id) {
                self.messages.push(DisplayMessage {
                    sender,
                    body,
                    timestamp_ms: now_ms(),
                    is_outgoing: false,
                });
                // Snap to bottom if user wasn't scrolled.
                if self.scroll_offset == 0 {
                    // Already at bottom — nothing to do.
                }
            }
        }
    }
}

// ── Drawing ───────────────────────────────────────────────────────────────────

fn ui(frame: &mut Frame, app: &App) {
    let size = frame.area();

    // Top-level split: sidebar | main area.
    let h_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
        .split(size);

    // Main area split: messages | input bar.
    let v_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(3), Constraint::Length(3)])
        .split(h_chunks[1]);

    draw_sidebar(frame, app, h_chunks[0]);
    draw_messages(frame, app, v_chunks[0]);
    draw_input(frame, app, v_chunks[1]);
}

fn draw_sidebar(frame: &mut Frame, app: &App, area: Rect) {
    let items: Vec<ListItem> = app
        .channel_names
        .iter()
        .enumerate()
        .map(|(i, name)| {
            let style = if i == app.selected_channel {
                Style::default()
                    .fg(Color::Cyan)
                    .add_modifier(Modifier::BOLD | Modifier::REVERSED)
            } else {
                Style::default().fg(Color::Cyan)
            };
            ListItem::new(Line::from(Span::styled(name.clone(), style)))
        })
        .collect();

    let block = Block::default()
        .title(" Channels ")
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let mut list_state = ListState::default();
    if !app.channel_names.is_empty() {
        list_state.select(Some(app.selected_channel));
    }

    frame.render_stateful_widget(
        List::new(items).block(block),
        area,
        &mut list_state,
    );
}

fn draw_messages(frame: &mut Frame, app: &App, area: Rect) {
    let channel_title = app
        .channel_names
        .get(app.selected_channel)
        .map(|n| format!(" {n} "))
        .unwrap_or_else(|| " Messages ".to_string());

    let block = Block::default()
        .title(channel_title)
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let inner_height = area.height.saturating_sub(2) as usize;

    // Build lines from messages (newest at bottom).
    let mut lines: Vec<Line> = app
        .messages
        .iter()
        .map(|m| {
            let ts = format_timestamp(m.timestamp_ms);
            let ts_span = Span::styled(ts, Style::default().fg(Color::DarkGray));

            let sender_style = if m.is_outgoing {
                Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
            };
            let sender_span = Span::styled(format!(" {} ", m.sender), sender_style);
            let body_span = Span::raw(m.body.clone());

            Line::from(vec![ts_span, sender_span, body_span])
        })
        .collect();

    // Apply scroll: scroll_offset=0 means newest at bottom.
    let total = lines.len();
    let visible_start = if total > inner_height {
        let bottom = total - app.scroll_offset.min(total);
        bottom.saturating_sub(inner_height)
    } else {
        0
    };
    let visible_end = if total > inner_height {
        total - app.scroll_offset.min(total)
    } else {
        total
    };
    let visible_lines: Vec<Line> = lines
        .drain(visible_start..visible_end.min(lines.len()))
        .collect();

    let paragraph = Paragraph::new(visible_lines)
        .block(block)
        .wrap(Wrap { trim: false });

    frame.render_widget(paragraph, area);
}

fn draw_input(frame: &mut Frame, app: &App, area: Rect) {
    let block = Block::default()
        .title(" Input (Enter=send, Tab=switch channel, q/Ctrl+C=quit) ")
        .borders(Borders::ALL)
        .style(Style::default().fg(Color::DarkGray));

    let input_text = Paragraph::new(app.input.as_str())
        .block(block)
        .style(Style::default().fg(Color::White));

    frame.render_widget(input_text, area);

    // Position cursor at end of input.
    let cursor_x = area.x + 1 + app.input.len() as u16;
    let cursor_y = area.y + 1;
    if cursor_x < area.x + area.width - 1 {
        frame.set_cursor_position((cursor_x, cursor_y));
    }
}

fn format_timestamp(ms: u64) -> String {
    // Simple HH:MM format from epoch ms.
    let secs = ms / 1000;
    let hours = (secs / 3600) % 24;
    let minutes = (secs / 60) % 60;
    format!("[{:02}:{:02}] ", hours, minutes)
}

// ── Message polling task ──────────────────────────────────────────────────────

/// Background task that polls the server for new messages and sends them via `tx`.
async fn poll_task(
    mut session: SessionState,
    client: node_service::Client,
    tx: mpsc::Sender<TuiEvent>,
) {
    let mut poll_interval = interval(Duration::from_millis(1000));
    poll_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

    loop {
        poll_interval.tick().await;

        let identity_bytes = session.identity_bytes();
        let payloads = match fetch_wait(&client, &identity_bytes, 0).await {
            Ok(p) => p,
            Err(_) => continue,
        };

        if payloads.is_empty() {
            continue;
        }

        let mut new_msgs: Vec<(ConversationId, String, String)> = Vec::new();
        let my_key = session.identity_bytes();

        let mut sorted = payloads;
        sorted.sort_by_key(|(seq, _)| *seq);

        for (_seq, payload) in &sorted {
            let mls_payload = match try_hybrid_decrypt(session.hybrid_kp.as_ref(), payload) {
                Ok(b) => b,
                Err(_) => payload.clone(),
            };

            let conv_ids: Vec<ConversationId> = session.members.keys().cloned().collect();

            for conv_id in &conv_ids {
                let member = match session.members.get_mut(conv_id) {
                    Some(m) => m,
                    None => continue,
                };

                match member.receive_message(&mls_payload) {
                    Ok(ReceivedMessage::Application(plaintext)) => {
                        let (sender_key, app_bytes) = {
                            let after_unpad = quicproquo_core::padding::unpad(&plaintext)
                                .unwrap_or_else(|_| plaintext.clone());

                            if quicproquo_core::sealed_sender::is_sealed(&after_unpad) {
                                match quicproquo_core::sealed_sender::unseal(&after_unpad) {
                                    Ok((sk, inner)) => (sk.to_vec(), inner),
                                    Err(_) => (my_key.clone(), after_unpad),
                                }
                            } else {
                                (my_key.clone(), after_unpad)
                            }
                        };

                        let (body, msg_id, msg_type, ref_msg_id) =
                            match parse_app_msg(&app_bytes) {
                                Ok((_, AppMessage::Chat { message_id, body })) => (
                                    String::from_utf8_lossy(&body).to_string(),
                                    Some(message_id),
                                    "chat",
                                    None,
                                ),
                                Ok((_, AppMessage::Reply { ref_msg_id, body })) => (
                                    String::from_utf8_lossy(&body).to_string(),
                                    None,
                                    "reply",
                                    Some(ref_msg_id),
                                ),
                                Ok((_, AppMessage::Reaction { ref_msg_id, emoji })) => (
                                    String::from_utf8_lossy(&emoji).to_string(),
                                    None,
                                    "reaction",
                                    Some(ref_msg_id),
                                ),
                                _ => (
                                    String::from_utf8_lossy(&app_bytes).to_string(),
                                    None,
                                    "chat",
                                    None,
                                ),
                            };

                        let stored = StoredMessage {
                            conversation_id: conv_id.clone(),
                            message_id: msg_id,
                            sender_key: sender_key.clone(),
                            sender_name: None,
                            body: body.clone(),
                            msg_type: msg_type.into(),
                            ref_msg_id,
                            timestamp_ms: now_ms(),
                            is_outgoing: false,
                        };

                        if session.conv_store.save_message(&stored).is_ok() {
                            let sender_short = hex::encode(&sender_key[..sender_key.len().min(4)]);
                            new_msgs.push((conv_id.clone(), sender_short, body));
                        }

                        let _ = session.conv_store.update_activity(conv_id, now_ms());
                        let _ = session.save_member(conv_id);
                        break;
                    }
                    Ok(ReceivedMessage::StateChanged) | Ok(ReceivedMessage::SelfRemoved) => {
                        let _ = session.save_member(conv_id);
                        break;
                    }
                    _ => {}
                }
            }
        }

        if !new_msgs.is_empty() {
            if tx.send(TuiEvent::NewMessages(new_msgs)).await.is_err() {
                break;
            }
        }
    }
}

// ── Send message ──────────────────────────────────────────────────────────────

async fn send_message(
    session: &mut SessionState,
    client: &node_service::Client,
    conv_id: &ConversationId,
    text: &str,
) -> anyhow::Result<()> {
    let my_key = session.identity_bytes();
    let identity = Arc::clone(&session.identity);

    let member = session
        .members
        .get_mut(conv_id)
        .context("no GroupMember for this conversation")?;

    // Wrap in structured AppMessage format.
    let app_payload = serialize_chat(text.as_bytes(), None)
        .context("serialize app message")?;

    // Metadata protection: seal + pad.
    let sealed = quicproquo_core::sealed_sender::seal(&identity, &app_payload);
    let padded = quicproquo_core::padding::pad(&sealed);

    let ct = member.send_message(&padded).context("MLS encrypt")?;

    let recipients: Vec<Vec<u8>> = member
        .member_identities()
        .into_iter()
        .filter(|id| id.as_slice() != my_key.as_slice())
        .collect();

    for recipient_key in &recipients {
        let peer_hybrid_pk = fetch_hybrid_key(client, recipient_key).await?;
        let payload = if let Some(ref pk) = peer_hybrid_pk {
            hybrid_encrypt(pk, &ct, b"", b"").context("hybrid encrypt")?
        } else {
            ct.clone()
        };
        enqueue(client, recipient_key, &payload).await?;
    }

    // Extract message_id from what we just serialized.
    let msg_id = parse_app_msg(&app_payload)
        .ok()
        .and_then(|(_, m)| match m {
            AppMessage::Chat { message_id, .. } => Some(message_id),
            _ => None,
        });

    // Save outgoing message.
    let stored = StoredMessage {
        conversation_id: conv_id.clone(),
        message_id: msg_id,
        sender_key: my_key,
        sender_name: Some("you".into()),
        body: text.to_string(),
        msg_type: "chat".into(),
        ref_msg_id: None,
        timestamp_ms: now_ms(),
        is_outgoing: true,
    };
    session.conv_store.save_message(&stored)?;
    session.conv_store.update_activity(conv_id, now_ms())?;
    session.save_member(conv_id)?;

    Ok(())
}

// ── TUI entry point ───────────────────────────────────────────────────────────

/// Entry point for `qpq tui`. Sets up the terminal, runs the event loop, and
/// restores the terminal on exit.
pub async fn run_tui(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    password: Option<&str>,
    username: Option<&str>,
    opaque_password: Option<&str>,
    access_token: &str,
    device_id: Option<&str>,
) -> anyhow::Result<()> {
    // ── Auth ──────────────────────────────────────────────────────────────────
    let resolved_token = resolve_tui_access_token(
        state_path,
        server,
        ca_cert,
        server_name,
        password,
        username,
        opaque_password,
        access_token,
    )
    .await?;

    let token_bytes = hex::decode(&resolved_token)
        .unwrap_or_else(|_| resolved_token.into_bytes());
    let auth_ctx = ClientAuth::from_raw(token_bytes, device_id.map(String::from));
    init_auth(auth_ctx);

    // ── Session + RPC ─────────────────────────────────────────────────────────
    let mut session = SessionState::load(state_path, password)?;
    let client = connect_node(server, ca_cert, server_name).await?;

    // Auto-upload KeyPackage.
    let _ = auto_upload_keys_tui(&session, &client).await;

    // ── Terminal setup ────────────────────────────────────────────────────────
    enable_raw_mode().context("enable raw mode")?;
    let mut stdout = std::io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
        .context("enter alternate screen")?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend).context("create terminal")?;

    let result = tui_loop(&mut terminal, &mut session, client).await;

    // ── Terminal cleanup (always restore, even on error) ───────────────────
    disable_raw_mode().ok();
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )
    .ok();
    terminal.show_cursor().ok();

    session.save_all()?;

    result
}

async fn tui_loop(
    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
    session: &mut SessionState,
    client: node_service::Client,
) -> anyhow::Result<()> {
    let mut app = App::new(session)?;
    app.reload_messages(session)?;

    let (event_tx, mut event_rx) = mpsc::channel::<TuiEvent>(256);

    // ── Keyboard event task ───────────────────────────────────────────────────
    let key_tx = event_tx.clone();
    tokio::task::spawn_local(async move {
        loop {
            // crossterm event polling — 50ms timeout so we can tick.
            match event::poll(Duration::from_millis(50)) {
                Ok(true) => {
                    if let Ok(Event::Key(key)) = event::read() {
                        if key_tx.send(TuiEvent::Key(key)).await.is_err() {
                            break;
                        }
                    }
                }
                Ok(false) => {
                    // No event — send a tick so the UI redraws.
                    if key_tx.send(TuiEvent::Tick).await.is_err() {
                        break;
                    }
                }
                Err(_) => break,
            }
        }
    });

    // ── Message poll task ─────────────────────────────────────────────────────
    // Clone session state for the poll task (it needs its own SessionState).
    let poll_session = SessionState::load(
        &session.state_path.clone(),
        session.password.as_ref().map(|p| p.as_str()),
    )?;
    let poll_tx = event_tx.clone();
    tokio::task::spawn_local(poll_task(poll_session, client.clone(), poll_tx));

    // ── Main loop ─────────────────────────────────────────────────────────────
    loop {
        terminal.draw(|f| ui(f, &app)).context("draw")?;

        match event_rx.recv().await {
            None => break,
            Some(TuiEvent::Tick) => {
                // Just redraw.
            }
            Some(TuiEvent::NewMessages(msgs)) => {
                app.append_messages(msgs);
            }
            Some(TuiEvent::Key(key)) => {
                match key.code {
                    KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                        app.should_quit = true;
                    }
                    KeyCode::Char('q') if app.input.is_empty() => {
                        app.should_quit = true;
                    }
                    KeyCode::Enter => {
                        let text = app.input.trim().to_string();
                        if !text.is_empty() {
                            app.input.clear();
                            if let Some(conv_id) = app.active_conv_id().cloned() {
                                match send_message(session, &client, &conv_id, &text).await {
                                    Ok(()) => {
                                        // Add to in-memory list immediately.
                                        app.messages.push(DisplayMessage {
                                            sender: format!("me({})", app.identity_short),
                                            body: text,
                                            timestamp_ms: now_ms(),
                                            is_outgoing: true,
                                        });
                                    }
                                    Err(_e) => {
                                        // Silently drop — user will see nothing happened.
                                    }
                                }
                            }
                        }
                    }
                    KeyCode::Char(c) => {
                        app.input.push(c);
                    }
                    KeyCode::Backspace => {
                        app.input.pop();
                    }
                    KeyCode::Up => {
                        app.scroll_up();
                    }
                    KeyCode::Down => {
                        app.scroll_down();
                    }
                    KeyCode::Tab => {
                        if key.modifiers.contains(KeyModifiers::SHIFT) {
                            app.select_prev_channel(session);
                        } else {
                            app.select_next_channel(session);
                        }
                        app.reload_messages(session)?;
                    }
                    _ => {}
                }
            }
        }

        if app.should_quit {
            break;
        }
    }

    Ok(())
}

// ── Startup helpers ───────────────────────────────────────────────────────────

async fn auto_upload_keys_tui(
    session: &SessionState,
    client: &node_service::Client,
) -> anyhow::Result<()> {
    let ks_path = session.state_path.with_extension("pending.ks");
    let ks = DiskKeyStore::persistent(&ks_path).unwrap_or_else(|_| DiskKeyStore::ephemeral());
    let mut member = GroupMember::new_with_state(
        Arc::clone(&session.identity),
        ks,
        None,
        false,
    );
    let kp_bytes = member.generate_key_package().context("generate KeyPackage")?;
    let id_key = session.identity.public_key_bytes();
    upload_key_package(client, &id_key, &kp_bytes).await?;
    if let Some(ref hkp) = session.hybrid_kp {
        upload_hybrid_key(client, &id_key, &hkp.public_key()).await?;
    }
    Ok(())
}

async fn resolve_tui_access_token(
    state_path: &Path,
    server: &str,
    ca_cert: &Path,
    server_name: &str,
    state_password: Option<&str>,
    username: Option<&str>,
    opaque_password: Option<&str>,
    cli_access_token: &str,
) -> anyhow::Result<String> {
    if !cli_access_token.is_empty() {
        return Ok(cli_access_token.to_string());
    }

    if let Some(cached) = load_cached_session(state_path, state_password) {
        return Ok(cached.token_hex);
    }

    let username = match username {
        Some(u) => u.to_string(),
        None => {
            use std::io::Write;
            eprint!("Username: ");
            std::io::stderr().flush().ok();
            let mut input = String::new();
            std::io::stdin()
                .read_line(&mut input)
                .context("failed to read username")?;
            let trimmed = input.trim().to_string();
            anyhow::ensure!(!trimmed.is_empty(), "username is required");
            trimmed
        }
    };

    let opaque_password = match opaque_password {
        Some(p) => p.to_string(),
        None => rpassword::read_password().context("failed to read password")?,
    };

    let state = load_or_init_state(state_path, state_password)?;
    let identity = IdentityKeypair::from_seed(state.identity_seed);
    let identity_key = identity.public_key_bytes().to_vec();

    let node_client = connect_node(server, ca_cert, server_name).await?;

    match opaque_register(&node_client, &username, &opaque_password, Some(&identity_key)).await {
        Ok(()) | Err(_) => {}
    }

    let token_bytes = opaque_login(&node_client, &username, &opaque_password, &identity_key)
        .await
        .context("OPAQUE login failed")?;
    let token_hex = hex::encode(&token_bytes);

    save_cached_session(state_path, &username, &token_hex, state_password)?;

    Ok(token_hex)
}
