//! Frontend /eval hot-patch channel — the AI's escape hatch for
//! driving any webapp-backed subcommand's UI without a rebuild.
//!
//! The pattern is specified in `gallia/skills/app-creator/SKILL.md` §7b.
//! AI (or any CLI caller) queues a JS snippet via `POST /eval`, the UI
//! long-polls `GET /eval/pending`, runs the snippet via
//! `new AsyncFunction('ctx', code)(window.__appCtx || {})`, and POSTs
//! the return value (or error) to `POST /eval/:id/result`. The queuer
//! blocks on a Condvar waiting for that result slot to fill.
//!
//! Because this is a full remote-code-execution primitive against
//! the app's own frontend it MUST NOT be exposed in any context
//! other than a dev-mode local webview. Our binding is `127.0.0.1`
//! only so that property holds.

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

/// A pending snippet waiting for the UI to pick it up.
pub struct EvalJob {
    pub id: String,
    pub code: String,
}

/// Shared state: a FIFO of pending snippets + a map of returned
/// results keyed by id. A single Condvar wakes both long-poll
/// `GET /eval/pending` waiters (when new jobs arrive) AND
/// `block_on_result` waiters (when the UI posts a result). We
/// demultiplex by checking what we were waiting for on wakeup.
pub struct EvalQueue {
    pub state: Mutex<EvalState>,
    pub cond: Condvar,
}

#[derive(Default)]
pub struct EvalState {
    pub pending: VecDeque<EvalJob>,
    pub results: HashMap<String, serde_json::Value>,
}

pub type SharedEval = Arc<EvalQueue>;

pub fn new() -> SharedEval {
    Arc::new(EvalQueue {
        state: Mutex::new(EvalState::default()),
        cond: Condvar::new(),
    })
}

/// `POST /eval` — queue a new snippet, return its id. Body shape:
/// `{"code":"return 1+1"}`. Response: `{"id":"<uuid>"}`.
pub fn handle_post(mut request: Request, eval: &SharedEval) -> Result<(), String> {
    let mut body = String::new();
    request
        .as_reader()
        .read_to_string(&mut body)
        .map_err(|e| e.to_string())?;
    let parsed: 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 code = match parsed.get("code").and_then(|v| v.as_str()) {
        Some(c) => c.to_string(),
        None => {
            let _ = request.respond(json_response(
                400,
                r#"{"ok":false,"error":"missing 'code' field"}"#,
            ));
            return Ok(());
        }
    };
    let id = Uuid::new_v4().to_string();
    {
        let mut guard = eval.state.lock().unwrap();
        guard.pending.push_back(EvalJob {
            id: id.clone(),
            code,
        });
    }
    eval.cond.notify_all();
    let body = format!(r#"{{"ok":true,"id":"{}"}}"#, id);
    request
        .respond(json_response(200, &body))
        .map_err(|e| e.to_string())
}

/// `GET /eval/pending` — long-poll for the next pending snippet.
/// Blocks up to 25 seconds waiting for the queue to have something,
/// returns the oldest pending job as `{"id":"...","code":"..."}`,
/// or 204 No Content on timeout. The UI is expected to loop.
pub fn handle_get_pending(request: Request, eval: &SharedEval) -> Result<(), String> {
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut guard = eval.state.lock().unwrap();
    loop {
        if let Some(job) = guard.pending.pop_front() {
            let body = format!(
                r#"{{"id":"{}","code":{}}}"#,
                job.id,
                serde_json::to_string(&job.code).unwrap()
            );
            return request
                .respond(json_response(200, &body))
                .map_err(|e| e.to_string());
        }
        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() {
            return request
                .respond(empty_response(204))
                .map_err(|e| e.to_string());
        }
        let (g, wait_result) = eval.cond.wait_timeout(guard, remaining).unwrap();
        guard = g;
        if wait_result.timed_out() && guard.pending.is_empty() {
            return request
                .respond(empty_response(204))
                .map_err(|e| e.to_string());
        }
    }
}

/// `GET /eval/:id/result` — the CLI caller (usually
/// `adom-video-post eval`) polls this to retrieve the snippet's return
/// value. Long-polls up to 25 seconds on a Condvar. Returns 200 +
/// the stored result object on success, 204 No Content on timeout.
pub fn handle_get_result(request: Request, eval: &SharedEval, id: &str) -> Result<(), String> {
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut guard = eval.state.lock().unwrap();
    loop {
        if let Some(result) = guard.results.remove(id) {
            let body = result.to_string();
            return request
                .respond(json_response(200, &body))
                .map_err(|e| e.to_string());
        }
        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() {
            return request
                .respond(empty_response(204))
                .map_err(|e| e.to_string());
        }
        let (g, wait_result) = eval.cond.wait_timeout(guard, remaining).unwrap();
        guard = g;
        if wait_result.timed_out() && !guard.results.contains_key(id) {
            return request
                .respond(empty_response(204))
                .map_err(|e| e.to_string());
        }
    }
}

/// `POST /eval/:id/result` — the UI posts the snippet's return value
/// (or error) back. Body shape: `{"ok":true,"value":...}` or
/// `{"ok":false,"error":"...","stack":"..."}`. We store the whole
/// object in the result map and wake any callers blocked on this id.
pub fn handle_post_result(
    mut request: Request,
    eval: &SharedEval,
    id: &str,
) -> Result<(), String> {
    let mut body = String::new();
    request
        .as_reader()
        .read_to_string(&mut body)
        .map_err(|e| e.to_string())?;
    let parsed: 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 mut guard = eval.state.lock().unwrap();
        guard.results.insert(id.to_string(), parsed);
    }
    eval.cond.notify_all();
    request
        .respond(json_response(200, r#"{"ok":true}"#))
        .map_err(|e| e.to_string())
}

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())
}

fn empty_response(status: u16) -> Response<std::io::Empty> {
    Response::empty(status)
}