//! Scoped IO wrappers — all IO accessed through TxScope.
//!
//! Design philosophy: every IO operation goes through `scope.fs()`, `scope.llm()`,
//! or `scope.shell()`. The scattered IO functions are `pub(crate)`, inaccessible
//! to engine code. Critical results from scoped IO carry tx_context (trace_id +
//! tx_name) automatically — suppress logs always show which transaction spawned them.

use std::path::{Path, PathBuf};
use std::time::Duration;

use crate::critical::{Critical, Transaction, TxScope};

// ─── ScopedFs ───

/// Filesystem IO scoped to a transaction.
///
/// All `Critical` results carry tx_context (trace_id + tx_name) automatically.
/// Non-Critical returns (like `exists`) are passed through as-is.
pub struct ScopedFs<'a, T: Transaction> {
    scope: &'a TxScope<T>,
}

impl<T: Transaction> ScopedFs<'_, T> {
    /// Inject tx_context into a Critical value.
    fn inject<V>(&self, c: Critical<V>) -> Critical<V> {
        c.with_tx_context(self.scope.raw_trace_id(), T::name())
    }

    // ─── fs.rs wrappers ───

    /// Read the entire contents of a file into a string.
    pub fn read_to_string(&self, path: impl AsRef<Path>) -> Critical<String> {
        self.inject(crate::fs::read_to_string(path))
    }

    /// Read the entire contents of a file into a byte vector.
    pub fn read(&self, path: impl AsRef<Path>) -> Critical<Vec<u8>> {
        self.inject(crate::fs::read(path))
    }

    /// Write contents to a file (atomic: write tmp then rename).
    pub fn write(&self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Critical<()> {
        self.inject(crate::fs::write(path, contents))
    }

    /// Recursively create directories.
    pub fn create_dir_all(&self, path: impl AsRef<Path>) -> Critical<()> {
        self.inject(crate::fs::create_dir_all(path))
    }

    /// List directory entries.
    pub fn read_dir(&self, path: impl AsRef<Path>) -> Critical<Vec<std::fs::DirEntry>> {
        self.inject(crate::fs::read_dir(path))
    }

    /// Open a file in append mode.
    pub fn open_append(&self, path: impl AsRef<Path>) -> Critical<std::fs::File> {
        self.inject(crate::fs::open_append(path))
    }

    /// Check if a path exists. Returns bool (no Critical).
    pub fn exists(&self, path: impl AsRef<Path>) -> bool {
        crate::fs::exists(path)
    }

    /// Remove a file.
    pub fn remove_file(&self, path: impl AsRef<Path>) -> Critical<()> {
        self.inject(crate::fs::remove_file(path))
    }

    /// Rename a file or directory.
    pub fn rename(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> Critical<()> {
        self.inject(crate::fs::rename(from, to))
    }

    /// Remove a directory and all its contents.
    pub fn remove_dir_all(&self, path: impl AsRef<Path>) -> Critical<()> {
        self.inject(crate::fs::remove_dir_all(path))
    }

    // ─── ioutil.rs wrappers ───

    /// Safely resolve a relative path against a base directory.
    /// Prevents path traversal attacks.
    pub fn safe_resolve(&self, base: &Path, relative: &str) -> Critical<PathBuf> {
        self.inject(crate::ioutil::safe_resolve(base, relative))
    }

    /// Read file to string with path info in error messages.
    pub fn ioutil_read_to_string(&self, path: impl AsRef<Path>) -> Critical<String> {
        self.inject(crate::ioutil::read_to_string(path))
    }

    /// Get metadata for a directory entry (follows symlinks).
    pub fn entry_metadata(&self, entry: &std::fs::DirEntry) -> Critical<std::fs::Metadata> {
        self.inject(crate::ioutil::entry_metadata(entry))
    }

    /// List subdirectory names in a directory.
    pub fn list_dirs(&self, path: &Path) -> Critical<Vec<String>> {
        self.inject(crate::ioutil::list_dirs(path))
    }

    /// Move a file/directory to a .trash subdirectory.
    /// Returns the trash path as a string.
    pub fn move_to_trash(&self, parent_dir: &Path, source_name: &str) -> Critical<String> {
        self.inject(crate::ioutil::move_to_trash(parent_dir, source_name))
    }

    /// Get disk usage information. Returns None on failure.
    pub fn disk_usage(&self, path: &Path) -> Option<crate::ioutil::DiskUsage> {
        crate::ioutil::disk_usage(path)
    }
}

// ─── ScopedLlm ───

/// LLM inference scoped to a transaction.
///
/// Infer functions return `Result` (not `Critical`), so no tx_context injection.
/// The wrapper provides access control — engine must go through scope.
pub struct ScopedLlm<'a, T: Transaction> {
    scope: &'a TxScope<T>,
}

impl<T: Transaction> ScopedLlm<'_, T> {
    /// Single-output inference (with automatic retry and logging).
    pub async fn infer<Req, Resp>(
        &self,
        channel: &dyn crate::llm::LlmChannel,
        request: &Req,
        allow_planning: bool,
    ) -> Result<(Resp, Option<crate::llm::InferUsage>), crate::llm::InferError>
    where
        Req: crate::llm::Promptable,
        Resp: crate::llm::Parseable,
    {
        crate::llm::infer(channel, request, allow_planning).await
    }

    /// Single-output inference with optional callbacks.
    pub async fn infer_with_on_text<Req, Resp>(
        &self,
        channel: &dyn crate::llm::LlmChannel,
        request: &Req,
        on_text: Option<Box<dyn FnMut(&str) + Send>>,
        on_input: Option<Box<dyn FnOnce(&str) + Send>>,
        cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
        on_thinking: Option<Box<dyn FnMut(&str) + Send>>,
        allow_planning: bool,
    ) -> Result<(Resp, Option<crate::llm::InferUsage>), crate::llm::InferError>
    where
        Req: crate::llm::Promptable,
        Resp: crate::llm::Parseable,
    {
        crate::llm::infer_with_on_text(
            channel, request, on_text, on_input, cancel, on_thinking, allow_planning,
        ).await
    }

    /// Stream inference.
    pub async fn stream_infer<Req, Resp>(
        &self,
        channel: &dyn crate::llm::LlmChannel,
        request: &Req,
        allow_planning: bool,
    ) -> Result<crate::llm::StreamInfer<Resp>, crate::llm::InferError>
    where
        Req: crate::llm::Promptable,
        Resp: crate::llm::Parseable,
    {
        crate::llm::stream_infer(channel, request, allow_planning).await
    }

    /// Stream inference with optional callbacks.
    pub async fn stream_infer_with_on_text<Req, Resp>(
        &self,
        channel: &dyn crate::llm::LlmChannel,
        request: &Req,
        on_text: Option<Box<dyn FnMut(&str) + Send>>,
        on_input: Option<Box<dyn FnOnce(&str) + Send>>,
        cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
        on_thinking: Option<Box<dyn FnMut(&str) + Send>>,
        allow_planning: bool,
    ) -> Result<crate::llm::StreamInfer<Resp>, crate::llm::InferError>
    where
        Req: crate::llm::Promptable,
        Resp: crate::llm::Parseable,
    {
        crate::llm::stream_infer_with_on_text(
            channel, request, on_text, on_input, cancel, on_thinking, allow_planning,
        ).await
    }

    /// Get cache statistics for a time window.
    pub fn cache_stats(&self, window_secs: u64) -> crate::llm::CacheStats {
        crate::llm::cache_stats(window_secs)
    }

    /// Cancel an ongoing inference by key.
    pub fn cancel_infer(&self, key: &str) -> bool {
        crate::llm::cancel_infer(key)
    }

    /// HTTP POST JSON request via shared connection pool.
    /// Wraps `reqwest` internally — engine never touches raw HTTP client.
    /// Non-2xx responses are Critical errors.
    pub async fn http_post_json(
        &self,
        url: &str,
        headers: &[(&str, &str)],
        body: &serde_json::Value,
    ) -> Critical<serde_json::Value> {
        let c = crate::llm::http_post_json(url, headers, body).await;
        c.with_tx_context(self.scope.raw_trace_id(), T::name())
    }


}

// ─── ScopedShell ───

/// Shell execution scoped to a transaction.
///
/// Wraps `Shell` with builder pattern. Engine must construct shells through scope.
pub struct ScopedShell<'a, T: Transaction> {
    #[allow(dead_code)]
    scope: &'a TxScope<T>,
    shell: crate::shell::Shell,
}

impl<T: Transaction> ScopedShell<'_, T> {
    /// Set execution timeout.
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.shell = self.shell.with_timeout(timeout);
        self
    }

    /// Set maximum output size in bytes.
    pub fn with_max_output(mut self, max: usize) -> Self {
        self.shell = self.shell.with_max_output(max);
        self
    }

    /// Execute a shell script.
    pub async fn exec(&self, script: &str) -> crate::shell::ShellResult<crate::shell::ShellOutput> {
        self.shell.exec(script).await
    }

    /// Execute a shell script in a specific directory.
    pub async fn exec_in_dir(&self, script: &str, dir: &Path) -> crate::shell::ShellResult<crate::shell::ShellOutput> {
        self.shell.exec_in_dir(script, dir).await
    }

    /// Read a file via shell (cat).
    pub async fn read_file(&self, path: &str) -> crate::shell::ShellResult<crate::shell::ShellOutput> {
        self.shell.read_file(path).await
    }

    /// Write content to a file via shell (tee).
    pub async fn write_file(&self, path: &str, content: &str) -> crate::shell::ShellResult<crate::shell::ShellOutput> {
        self.shell.write_file(path, content).await
    }
}

// ─── TxScope extensions ───

impl<T: Transaction> TxScope<T> {
    /// Get a scoped filesystem IO handle.
    ///
    /// All `Critical` results from this handle carry tx_context automatically.
    /// ```ignore
    /// let content = scope.fs().read_to_string("config.json")
    ///     .suppress(token, "config read");
    /// ```
    pub fn fs(&self) -> ScopedFs<'_, T> {
        ScopedFs { scope: self }
    }

    /// Get a scoped LLM inference handle.
    ///
    /// ```ignore
    /// let (resp, usage) = scope.llm().infer(channel, &request, false).await?;
    /// ```
    pub fn llm(&self) -> ScopedLlm<'_, T> {
        ScopedLlm { scope: self }
    }

    /// Create a scoped shell with the given working directory and optional sandbox user.
    ///
    /// ```ignore
    /// let output = scope.shell(work_dir, sandbox_user)
    ///     .with_timeout(Duration::from_secs(30))
    ///     .exec("ls -la").await?;
    /// ```
    pub fn shell(&self, working_dir: PathBuf, sandbox_user: Option<String>) -> ScopedShell<'_, T> {
        ScopedShell {
            scope: self,
            shell: crate::shell::Shell::new(working_dir, sandbox_user),
        }
    }
}