//! Sandboxed tile renderer.
//!
//! Provides high-level APIs that combine image preparation (Fork 1) with
//! sandboxed rendering (Fork 2). Callers supply [`BuildParams`] and get back
//! a [`TileRenderer`] or dump result — all fork/sandbox/IPC details are hidden.

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::compile::LoadedImages;
use crate::fork_sandbox::{SandboxConfig, TypedReader, TypedWriter, fork_sandboxed};
use crate::frame::{DocumentMeta, HighlightRect, HighlightSpec, TilePngs};
use crate::log::{LogBuffer, LogEntry};
use crate::pipeline::{BuildParams, compile_and_dump, compile_and_tile};

pub use crate::fork_sandbox::ChildProcess;

// ---------------------------------------------------------------------------
// IPC protocol types (private)
// ---------------------------------------------------------------------------

/// Request from parent to child.
#[derive(Serialize, Deserialize)]
enum Request {
    RenderTile(usize),
    FindHighlightRects { idx: usize, spec: HighlightSpec },
    Shutdown,
}

/// Response from child to parent.
///
/// The first message is always `Meta`. Subsequent messages are `Tile`, `Rects`,
/// or `Error`.
#[derive(Serialize, Deserialize)]
enum Response {
    Meta(DocumentMeta),
    Tile {
        idx: usize,
        pngs: TilePngs,
    },
    Rects {
        idx: usize,
        rects: Vec<HighlightRect>,
    },
    Error(String),
}

/// Wire-level wrapper: every child→parent message carries piggyback log entries.
#[derive(Serialize, Deserialize)]
struct ChildMessage {
    response: Response,
    logs: Vec<LogEntry>,
}

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// A response from the child process, tagged with the tile index.
#[derive(Debug)]
pub enum TileResponse {
    Tile {
        idx: usize,
        pngs: TilePngs,
    },
    Rects {
        idx: usize,
        rects: Vec<HighlightRect>,
    },
}

/// Tile renderer communicating with a forked child process via typed IPC.
pub struct TileRenderer {
    tx: TypedWriter<Request>,
    rx: TypedReader<ChildMessage>,
    log_buffer: LogBuffer,
}

impl TileRenderer {
    /// Receive the initial metadata response from the child.
    ///
    /// Must be called exactly once as the first operation after [`build_renderer`].
    pub fn wait_for_meta(&mut self) -> Result<DocumentMeta> {
        match self
            .recv_and_ingest()
            .context("failed to receive metadata from child")?
        {
            Response::Meta(m) => Ok(m),
            Response::Error(e) => anyhow::bail!("child build error: {e}"),
            _ => anyhow::bail!("unexpected response, expected Meta"),
        }
    }

    /// Send a tile render request without waiting for the response.
    pub fn send_render_tile(&mut self, idx: usize) -> Result<()> {
        self.tx.send(&Request::RenderTile(idx))
    }

    /// Send a highlight rects request without waiting for the response.
    pub fn send_find_rects(&mut self, idx: usize, spec: &HighlightSpec) -> Result<()> {
        self.tx.send(&Request::FindHighlightRects {
            idx,
            spec: spec.clone(),
        })
    }

    /// Non-blocking receive. Returns `Ok(None)` if no data is ready.
    pub fn try_recv(&mut self) -> Result<Option<TileResponse>> {
        if !self.has_pending_data() {
            return Ok(None);
        }
        self.recv().map(Some)
    }

    /// Blocking receive. Waits for the next response from the child.
    pub fn recv(&mut self) -> Result<TileResponse> {
        match self.recv_and_ingest()? {
            Response::Tile { idx, pngs } => Ok(TileResponse::Tile { idx, pngs }),
            Response::Rects { idx, rects } => Ok(TileResponse::Rects { idx, rects }),
            Response::Error(e) => anyhow::bail!("{e}"),
            Response::Meta(_) => anyhow::bail!("unexpected Meta response"),
        }
    }

    /// Receive a `ChildMessage`, ingest piggybacked logs, return the `Response`.
    fn recv_and_ingest(&mut self) -> Result<Response> {
        let msg = self.rx.recv()?;
        for entry in msg.logs {
            self.log_buffer.push(entry);
        }
        Ok(msg.response)
    }

    /// Request a tile pair (content + sidebar) from the child.
    pub fn render_tile_pair(&mut self, idx: usize) -> Result<TilePngs> {
        self.send_render_tile(idx)?;
        match self.recv()? {
            TileResponse::Tile { pngs, .. } => Ok(pngs),
            _ => anyhow::bail!("unexpected response, expected Tile"),
        }
    }

    /// Request highlight rectangles for a tile's content (no rendering).
    pub fn find_highlight_rects(
        &mut self,
        idx: usize,
        spec: &HighlightSpec,
    ) -> Result<Vec<HighlightRect>> {
        self.send_find_rects(idx, spec)?;
        match self.recv()? {
            TileResponse::Rects { rects, .. } => Ok(rects),
            _ => anyhow::bail!("unexpected response, expected Rects"),
        }
    }

    /// Check if the child has sent data (non-blocking).
    pub fn has_pending_data(&self) -> bool {
        use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
        use std::os::fd::AsFd;
        let mut fds = [PollFd::new(self.rx.as_fd(), PollFlags::POLLIN)];
        match poll(&mut fds, PollTimeout::ZERO) {
            Ok(n) => {
                n > 0
                    && fds[0]
                        .revents()
                        .is_some_and(|r| r.contains(PollFlags::POLLIN))
            }
            Err(_) => false,
        }
    }

    /// Send shutdown request to the child.
    pub fn shutdown(mut self) {
        let _ = self.tx.send(&Request::Shutdown);
    }
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

/// Drain child-local log buffer and send a response wrapped in `ChildMessage`.
fn send_with_logs(
    tx: &mut TypedWriter<ChildMessage>,
    response: Response,
    log_buf: &LogBuffer,
) -> Result<()> {
    let logs = log_buf.drain();
    tx.send(&ChildMessage { response, logs })
}

/// Extract image paths (Fork 1, sandboxed) and fetch remote images (parent).
///
/// Fork 1 runs pulldown-cmark under a sandbox with no FS access and TCP denied.
/// The parent then fetches any remote images on the trusted side.
fn prepare_remote_images(
    params: &BuildParams,
    no_sandbox: bool,
    log_buffer: &LogBuffer,
) -> Result<(crate::compile::Prescan, LoadedImages)> {
    use crate::fork_sandbox::fork_compute;

    // Fork 1: prescan under maximum-restriction sandbox (no FS, no network)
    let prescan_sandbox = if no_sandbox {
        SandboxConfig::Disabled
    } else {
        SandboxConfig::deny_all()
    };
    let prescan_result = fork_compute(prescan_sandbox, log_buffer, {
        let md = params.markdown.clone();
        move || crate::compile::prescan(&md)
    })?;
    let paths = &prescan_result.image_paths;

    // Parent: fetch remote images (trusted side)
    let remote_images = if params.allow_remote_images {
        let remote_urls: Vec<String> = paths
            .iter()
            .filter(|p| p.starts_with("http://") || p.starts_with("https://"))
            .cloned()
            .collect();
        if remote_urls.is_empty() {
            LoadedImages::default()
        } else {
            let (images, errors) = crate::compile::load_images(&remote_urls, None, true);
            for err in &errors {
                log::warn!("{err}");
            }
            images
        }
    } else {
        LoadedImages::default()
    };

    Ok((prescan_result, remote_images))
}

/// Build sandbox config from BuildParams.
fn build_sandbox(params: &BuildParams, no_sandbox: bool) -> SandboxConfig {
    if no_sandbox {
        return SandboxConfig::Disabled;
    }
    SandboxConfig::Enforce {
        read_base: params.base_dir.clone(),
        extra_read_dirs: params.fonts.font_dirs(),
    }
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Build a sandboxed renderer: prepare images (Fork 1) + fork renderer (Fork 2).
///
/// Returns `(renderer, child_handle)` without waiting for metadata.
/// The caller must call [`TileRenderer::wait_for_meta`] to receive the first message.
pub fn build_renderer(
    params: &BuildParams,
    no_sandbox: bool,
    log_buffer: &LogBuffer,
) -> Result<(TileRenderer, ChildProcess)> {
    let (prescan, remote_images) = prepare_remote_images(params, no_sandbox, log_buffer)?;
    let sandbox = build_sandbox(params, no_sandbox);

    let params = params.clone();

    let log_buf = log_buffer.clone();
    let (tx, rx, child) =
        fork_sandboxed::<Request, ChildMessage, _>(sandbox, move |mut req_rx, mut resp_tx| {
            // Discard log entries inherited from the parent via fork COW.
            log_buf.drain();

            // Load local images (Landlock read scope allows git root)
            let (mut images, errors) = crate::compile::load_images(
                &prescan.image_paths,
                params.base_dir.as_deref(),
                false,
            );
            for err in &errors {
                log::warn!("{err}");
            }

            // Merge pre-fetched remote images from parent
            images.extend(remote_images);

            // Font cache inherited from parent via fork COW (static lifetime)
            let doc = match compile_and_tile(&params, &prescan, images) {
                Ok(doc) => doc,
                Err(e) => {
                    log::error!("child: build failed: {e:#}");
                    let _ =
                        send_with_logs(&mut resp_tx, Response::Error(format!("{e:#}")), &log_buf);
                    return;
                }
            };

            // Send metadata as first response
            let meta = doc.metadata();
            if send_with_logs(&mut resp_tx, Response::Meta(meta), &log_buf).is_err() {
                return;
            }

            // Request loop
            loop {
                let req = match req_rx.recv() {
                    Ok(r) => r,
                    Err(_) => break, // Parent closed channel
                };
                match req {
                    Request::RenderTile(idx) => {
                        let resp =
                            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                                doc.render_tile_pair(idx)
                            })) {
                                Ok(Ok(pngs)) => Response::Tile { idx, pngs },
                                Ok(Err(e)) => Response::Error(format!("render tile {idx}: {e:#}")),
                                Err(_) => Response::Error(format!("render tile {idx}: panic")),
                            };
                        if send_with_logs(&mut resp_tx, resp, &log_buf).is_err() {
                            break;
                        }
                    }
                    Request::FindHighlightRects { idx, spec } => {
                        let resp =
                            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                                doc.find_tile_highlight_rects(idx, &spec)
                            })) {
                                Ok(rects) => Response::Rects { idx, rects },
                                Err(_) => Response::Error(format!(
                                    "find highlight rects tile {idx}: panic"
                                )),
                            };
                        if send_with_logs(&mut resp_tx, resp, &log_buf).is_err() {
                            break;
                        }
                    }
                    Request::Shutdown => break,
                }
            }
        })?;

    Ok((
        TileRenderer {
            tx,
            rx,
            log_buffer: log_buffer.clone(),
        },
        child,
    ))
}

/// Build a sandboxed renderer and wait for metadata.
///
/// Convenience wrapper around [`build_renderer`] that also receives the initial
/// `Response::Meta` message. Used by `render` mode where no loading UI is needed.
///
/// Returns `(metadata, renderer, child_handle)`.
pub fn build_renderer_blocking(
    params: &BuildParams,
    no_sandbox: bool,
    log_buffer: &LogBuffer,
) -> Result<(DocumentMeta, TileRenderer, ChildProcess)> {
    let (mut renderer, child) = build_renderer(params, no_sandbox, log_buffer)?;
    let meta = renderer.wait_for_meta()?;
    Ok((meta, renderer, child))
}

/// Build and dump: prepare images (Fork 1) + fork dump (Fork 2).
///
/// The child compiles the document and writes the generated Typst source
/// and frame tree to stderr, then exits.
pub fn build_dump(
    params: &BuildParams,
    no_sandbox: bool,
    log_buffer: &LogBuffer,
) -> Result<ChildProcess> {
    let (prescan, remote_images) = prepare_remote_images(params, no_sandbox, log_buffer)?;
    let sandbox = build_sandbox(params, no_sandbox);

    let params = params.clone();

    // Fork 2 for dump: no IPC responses to piggyback logs on. Child writes to
    // stderr and exits immediately, so Fork 2 logs are not forwarded. Fork 1
    // logs (via prepare_remote_images) are still forwarded.
    let (_, _, child) = fork_sandboxed::<(), (), _>(sandbox, move |_, _| {
        // (No log forwarding in dump mode, but drain for consistency.)

        // Load local images (Landlock read scope allows git root)
        let (mut images, errors) =
            crate::compile::load_images(&prescan.image_paths, params.base_dir.as_deref(), false);
        for err in &errors {
            log::warn!("{err}");
        }

        // Merge pre-fetched remote images from parent
        images.extend(remote_images);

        // Font cache inherited from parent via fork COW (static lifetime)
        if let Err(e) = compile_and_dump(&params, &prescan, images) {
            eprintln!("{e:#}");
            unsafe { nix::libc::_exit(1) }
        }
    })?;

    Ok(child)
}

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

    #[test]
    fn request_response_serde_roundtrip() {
        let req = Request::RenderTile(42);
        let encoded = bincode::serde::encode_to_vec(&req, bincode::config::standard()).unwrap();
        let (decoded, _): (Request, _) =
            bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
        match decoded {
            Request::RenderTile(idx) => assert_eq!(idx, 42),
            _ => panic!("wrong variant"),
        }

        let req2 = Request::Shutdown;
        let encoded2 = bincode::serde::encode_to_vec(&req2, bincode::config::standard()).unwrap();
        let (decoded2, _): (Request, _) =
            bincode::serde::decode_from_slice(&encoded2, bincode::config::standard()).unwrap();
        assert!(matches!(decoded2, Request::Shutdown));

        let req3 = Request::FindHighlightRects {
            idx: 7,
            spec: HighlightSpec {
                target_ranges: vec![10..20, 30..40],
                active_ranges: vec![10..20],
            },
        };
        let encoded3 = bincode::serde::encode_to_vec(&req3, bincode::config::standard()).unwrap();
        let (decoded3, _): (Request, _) =
            bincode::serde::decode_from_slice(&encoded3, bincode::config::standard()).unwrap();
        match decoded3 {
            Request::FindHighlightRects { idx, spec } => {
                assert_eq!(idx, 7);
                assert_eq!(spec.target_ranges.len(), 2);
                assert_eq!(spec.target_ranges[0], 10..20);
                assert_eq!(spec.target_ranges[1], 30..40);
            }
            _ => panic!("wrong variant"),
        }
    }

    #[test]
    fn response_error_serde_roundtrip() {
        let resp = Response::Error("test error".into());
        let encoded = bincode::serde::encode_to_vec(&resp, bincode::config::standard()).unwrap();
        let (decoded, _): (Response, _) =
            bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
        match decoded {
            Response::Error(msg) => assert_eq!(msg, "test error"),
            _ => panic!("wrong variant"),
        }
    }

    #[test]
    fn child_message_serde_roundtrip() {
        let msg = ChildMessage {
            response: Response::Error("oops".into()),
            logs: vec![LogEntry {
                timestamp: std::time::SystemTime::now(),
                level: log::Level::Warn,
                target: "mlux::test".into(),
                message: "warning".into(),
            }],
        };
        let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap();
        let (decoded, _): (ChildMessage, _) =
            bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
        assert_eq!(decoded.logs.len(), 1);
        assert_eq!(decoded.logs[0].level, log::Level::Warn);
        assert_eq!(decoded.logs[0].message, "warning");
        match decoded.response {
            Response::Error(e) => assert_eq!(e, "oops"),
            _ => panic!("wrong variant"),
        }
    }
}
