//! Shared in-app log buffer.
//!
//! Every webapp piping the UI's `console.log` output back to the
//! server via `POST /console` so the CLI operator (and the AI driving
//! the tool) can read what the UI is saying without having to open
//! devtools in the Hydrogen webview. The same buffer is the target
//! of `adom-video-post ctrl <target> console --tail` / `--follow` so the
//! AI can stream UI logs into the Claude Code Monitor tool.

use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use tiny_http::{Header, Request, Response};

pub const CONSOLE_MAX: usize = 500;

/// Shared handle to the in-app log buffer. Append to the back,
/// evict from the front on overflow. Each entry is a pre-serialized
/// JSON string so the GET handler can splat them directly.
pub struct Console {
    pub messages: Mutex<VecDeque<ConsoleEntry>>,
    /// Incremented on every append, used by long-poll clients to
    /// detect "anything new since last poll".
    pub seq: Mutex<u64>,
    pub cond: Condvar,
}

pub struct ConsoleEntry {
    pub seq: u64,
    pub json: String,
}

pub type SharedConsole = Arc<Console>;

pub fn new() -> SharedConsole {
    Arc::new(Console {
        messages: Mutex::new(VecDeque::with_capacity(CONSOLE_MAX)),
        seq: Mutex::new(0),
        cond: Condvar::new(),
    })
}

/// `GET /console[?since=N&wait=MS]` — tail the buffer.
///
/// Without query params, returns every message currently in the
/// ring. With `since=N`, returns only messages whose seq is > N.
/// With `wait=MS` AND nothing new to return, blocks up to MS on a
/// Condvar waiting for a new append. This is the long-poll variant
/// used by `adom-video-post ctrl ... console --follow` to feed realtime
/// UI logs into the Claude Code Monitor tool.
pub fn handle_get(request: Request, console: &SharedConsole, url: &str) -> Result<(), String> {
    let query = url.split_once('?').map(|(_, q)| q).unwrap_or("");
    let mut since: u64 = 0;
    let mut wait_ms: u64 = 0;
    for pair in query.split('&') {
        if let Some(n) = pair.strip_prefix("since=") {
            since = n.parse().unwrap_or(0);
        } else if let Some(n) = pair.strip_prefix("wait=") {
            wait_ms = n.parse().unwrap_or(0);
        }
    }

    let entries = {
        let mut guard = console.messages.lock().unwrap();
        let mut matching: Vec<String> = guard
            .iter()
            .filter(|e| e.seq > since)
            .map(|e| e.json.clone())
            .collect();
        if matching.is_empty() && wait_ms > 0 {
            let deadline = Instant::now() + Duration::from_millis(wait_ms);
            while matching.is_empty() {
                let remaining = deadline.saturating_duration_since(Instant::now());
                if remaining.is_zero() {
                    break;
                }
                let (g, wait_result) = console.cond.wait_timeout(guard, remaining).unwrap();
                guard = g;
                matching = guard
                    .iter()
                    .filter(|e| e.seq > since)
                    .map(|e| e.json.clone())
                    .collect();
                if wait_result.timed_out() {
                    break;
                }
            }
        }
        matching
    };

    let latest_seq = *console.seq.lock().unwrap();
    let body = format!(
        r#"{{"ok":true,"seq":{},"messages":[{}]}}"#,
        latest_seq,
        entries.join(",")
    );
    request
        .respond(json_response(200, &body))
        .map_err(|e| e.to_string())
}

/// `POST /console` — append a JSON-encoded log message. The request
/// body is validated as JSON, re-serialized compactly, and echoed
/// to the server's stdout so the CLI operator sees it too.
pub fn handle_post(mut request: Request, console: &SharedConsole) -> Result<(), String> {
    let mut body = String::new();
    request
        .as_reader()
        .read_to_string(&mut body)
        .map_err(|e| e.to_string())?;
    let msg: serde_json::Value = match serde_json::from_str(&body) {
        Ok(v) => v,
        Err(e) => {
            let _ = request.respond(json_response(
                400,
                &format!(r#"{{"ok":false,"error":"invalid json: {}"}}"#, e),
            ));
            return Ok(());
        }
    };
    let compact = msg.to_string();
    println!("[ui-console] {}", compact);
    append(console, compact);
    request
        .respond(json_response(200, r#"{"ok":true}"#))
        .map_err(|e| e.to_string())
}

/// `DELETE /console` — drop all buffered messages.
pub fn handle_delete(request: Request, console: &SharedConsole) -> Result<(), String> {
    console.messages.lock().unwrap().clear();
    request
        .respond(json_response(200, r#"{"ok":true}"#))
        .map_err(|e| e.to_string())
}

/// Append a pre-serialized JSON string to the buffer, evicting the
/// oldest entry on overflow, bumping the seq counter, and waking
/// any long-poll waiters.
pub fn append(console: &SharedConsole, json: String) {
    let mut seq_guard = console.seq.lock().unwrap();
    *seq_guard += 1;
    let seq = *seq_guard;
    drop(seq_guard);
    let mut guard = console.messages.lock().unwrap();
    if guard.len() >= CONSOLE_MAX {
        guard.pop_front();
    }
    guard.push_back(ConsoleEntry { seq, json });
    console.cond.notify_all();
}

fn json_response(status: u16, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {
    Response::from_string(body)
        .with_status_code(status)
        .with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
}