//! Safe Rust wrappers around PHP embed SAPI FFI.
//!
//! These are low-level but safe wrappers. Higher-level runtime logic
//! (worker threads, channels, etc.) lives in other modules.
#![allow(unsafe_code)]

use std::ffi::CString;
use std::ptr;

use anyhow::{Context, Result, bail};
use tracing::{debug, warn};

use crate::ffi;

/// A PHP interpreter instance. One per thread.
///
/// # Safety
///
/// Must be created and used on a single OS thread. PHP globals are
/// thread-local (with NTS builds) but NOT safe to share across threads.
/// Each worker thread must own its own `PhpInstance`.
pub struct PhpInstance {
    /// Whether we're inside a request cycle.
    in_request: bool,
    /// Whether this instance uses the custom Folk SAPI (vs. embed SAPI).
    custom_sapi: bool,
    /// Whether this instance owns the module (should shutdown on drop).
    owns_module: bool,
    /// TSRM context for ZTS builds (NULL for NTS or module owner).
    tsrm_ctx: *mut std::ffi::c_void,
}

// SAFETY: PhpInstance is only used on the thread that created it.
// The raw pointer is a TSRM context owned by this thread.
// Sync is needed because EmbedRuntime holds the module owner instance
// while worker threads have their own attached instances.
unsafe impl Send for PhpInstance {}
unsafe impl Sync for PhpInstance {}

impl PhpInstance {
    /// Boot the PHP interpreter using the default embed SAPI.
    ///
    /// This is the original POC path — simple but no HTTP request support.
    pub fn boot() -> Result<Self> {
        debug!("booting PHP embed SAPI");

        let result = unsafe { ffi::php_embed_init(0, ptr::null_mut()) };

        if result != 0 {
            bail!("php_embed_init() failed with code {result}");
        }

        // Override output handler for embed SAPI
        unsafe { ffi::folk_install_output_handler() };

        debug!("PHP embed SAPI initialized");

        Ok(Self {
            in_request: false,
            custom_sapi: false,
            owns_module: true,
            tsrm_ctx: ptr::null_mut(),
        })
    }

    /// Boot the PHP interpreter using the custom Folk SAPI.
    ///
    /// Calls `folk_sapi_init()` which does `php_module_startup()`.
    /// Must be called exactly ONCE per process. Worker threads should
    /// use `attach()` instead.
    pub fn boot_custom_sapi() -> Result<Self> {
        debug!("booting PHP with Folk custom SAPI");

        // Save signal handlers before PHP init (PHP overwrites them)
        unsafe { ffi::folk_signals_save() };

        let result = unsafe { ffi::folk_sapi_init() };

        // Restore signal handlers (tokio needs SIGTERM/SIGINT)
        unsafe { ffi::folk_signals_restore() };

        // Install our SIGSEGV handler for worker thread protection
        unsafe { ffi::folk_sigsegv_handler_install() };

        if result != 0 {
            bail!("folk_sapi_init() failed with code {result}");
        }

        debug!("Folk custom SAPI initialized");

        Ok(Self {
            in_request: false,
            custom_sapi: true,
            owns_module: true,
            tsrm_ctx: ptr::null_mut(),
        })
    }

    /// Attach to an already-initialized PHP module (for worker threads).
    ///
    /// For ZTS builds: allocates a new TSRM interpreter context so this
    /// thread gets its own copy of PHP globals. For NTS builds: no-op
    /// (globals are shared, only works with single worker thread).
    pub fn attach() -> Self {
        debug!("attaching to existing PHP module");

        let ctx = unsafe { ffi::folk_thread_init() };
        if !ctx.is_null() {
            unsafe { ffi::folk_thread_set_ctx(ctx) };
            debug!("TSRM context allocated for worker thread");
        }

        Self {
            in_request: false,
            custom_sapi: true,
            owns_module: false,
            tsrm_ctx: ctx,
        }
    }

    /// Set the request context before calling `request_startup`.
    ///
    /// The `RequestContext` must live until `request_shutdown` is called.
    pub fn set_request_context(&self, ctx: &mut RequestContext) {
        ctx.build_ffi();
        unsafe { ffi::folk_request_context_set(&mut ctx.ffi) };
    }

    /// Clear the request context (called after `request_shutdown`).
    pub fn clear_request_context(&self) {
        unsafe { ffi::folk_request_context_clear() };
    }

    /// Start a new request cycle.
    pub fn request_startup(&mut self) -> Result<()> {
        if self.in_request {
            warn!("request_startup called while already in request — shutting down first");
            self.request_shutdown();
        }

        unsafe { ffi::folk_clear_output() };

        if self.custom_sapi {
            unsafe { ffi::folk_response_clear() };
        }

        let result = unsafe { ffi::folk_request_startup_safe() };
        match result {
            0 => {
                self.in_request = true;
                Ok(())
            },
            -1 => bail!("php_request_startup: fatal error (bailout)"),
            -2 => bail!("php_request_startup: startup failed"),
            code => bail!("php_request_startup: unknown error {code}"),
        }
    }

    /// End the current request cycle.
    pub fn request_shutdown(&mut self) {
        if !self.in_request {
            return;
        }

        let result = unsafe { ffi::folk_request_shutdown_safe() };
        if result != 0 {
            warn!("php_request_shutdown returned {result}");
        }

        if self.custom_sapi {
            self.clear_request_context();
        }

        self.in_request = false;
    }

    /// Execute a PHP script file (proper file execution, not eval).
    pub fn execute_script(&mut self, filename: &str) -> Result<String> {
        let c_filename = CString::new(filename).context("filename contains null byte")?;

        let result = unsafe { ffi::folk_execute_script_safe(c_filename.as_ptr()) };
        let output = self.take_output();

        match result {
            0 => Ok(output),
            -1 => {
                if output.is_empty() {
                    bail!("PHP script fatal error (bailout) in: {filename}")
                } else {
                    bail!("PHP script fatal error: {output}")
                }
            },
            other => bail!("PHP script error {other} in: {filename}"),
        }
    }

    /// Evaluate a PHP code string and return the output.
    pub fn eval(&mut self, code: &str) -> Result<EvalResult> {
        let c_code = CString::new(code).context("PHP code contains null byte")?;
        let mut retval = ffi::zval::new_undef();

        let result = unsafe { ffi::folk_eval_string_safe(c_code.as_ptr(), &mut retval) };

        let output = self.take_output();

        let return_value = if result == 0 {
            ZvalValue::from_raw(&mut retval)
        } else {
            ZvalValue::Null
        };

        unsafe { ffi::folk_zval_dtor(&mut retval) };

        match result {
            0 => Ok(EvalResult {
                output,
                return_value,
            }),
            -1 => {
                if output.is_empty() {
                    bail!("PHP eval fatal error (bailout) in: {code}")
                } else {
                    bail!("PHP eval fatal error (bailout): {output}")
                }
            },
            other => bail!("PHP eval error {other} in: {code}"),
        }
    }

    /// Call a PHP function by name with the given string arguments.
    pub fn call(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
        let c_func = CString::new(func_name).context("function name contains null byte")?;

        let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();

        let mut params: Vec<ffi::zval> = c_args
            .iter()
            .map(|s| {
                let mut z = ffi::zval::new_undef();
                unsafe {
                    ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
                }
                z
            })
            .collect();

        let mut retval = ffi::zval::new_undef();

        let result = unsafe {
            ffi::folk_call_function_safe(
                c_func.as_ptr(),
                &mut retval,
                u32::try_from(params.len()).expect("too many params"),
                if params.is_empty() {
                    ptr::null_mut()
                } else {
                    params.as_mut_ptr()
                },
            )
        };

        let return_value = ZvalValue::from_raw(&mut retval);

        unsafe { ffi::folk_zval_dtor(&mut retval) };
        for p in &mut params {
            unsafe { ffi::folk_zval_dtor(p) };
        }

        match result {
            0 => Ok(return_value),
            -1 => bail!("PHP call_user_function fatal error (bailout) in: {func_name}"),
            -2 => bail!("PHP call_user_function failed: {func_name}"),
            code => bail!("PHP call_user_function error {code}: {func_name}"),
        }
    }

    /// Evaluate PHP code with SIGSEGV protection.
    ///
    /// Like `eval()` but also catches segfaults in C extensions.
    /// Returns error on SIGSEGV instead of crashing the process.
    pub fn eval_protected(&mut self, code: &str) -> Result<EvalResult> {
        let c_code = CString::new(code).context("PHP code contains null byte")?;
        let mut retval = ffi::zval::new_undef();

        let result = unsafe { ffi::folk_eval_string_protected(c_code.as_ptr(), &mut retval) };

        let output = self.take_output();

        let return_value = if result == 0 {
            ZvalValue::from_raw(&mut retval)
        } else {
            ZvalValue::Null
        };

        unsafe { ffi::folk_zval_dtor(&mut retval) };

        match result {
            0 => Ok(EvalResult {
                output,
                return_value,
            }),
            -1 => bail!("PHP eval fatal error (bailout) in: {code}"),
            -3 => bail!("PHP eval SIGSEGV caught in: {code}"),
            code => bail!("PHP eval error {code} in: {code}"),
        }
    }

    /// Call a PHP function with SIGSEGV protection.
    ///
    /// Like `call()` but also catches segfaults in C extensions.
    pub fn call_protected(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
        let c_func = CString::new(func_name).context("function name contains null byte")?;

        let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();

        let mut params: Vec<ffi::zval> = c_args
            .iter()
            .map(|s| {
                let mut z = ffi::zval::new_undef();
                unsafe {
                    ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
                }
                z
            })
            .collect();

        let mut retval = ffi::zval::new_undef();

        let result = unsafe {
            ffi::folk_call_function_protected(
                c_func.as_ptr(),
                &mut retval,
                u32::try_from(params.len()).expect("too many params"),
                if params.is_empty() {
                    ptr::null_mut()
                } else {
                    params.as_mut_ptr()
                },
            )
        };

        let return_value = ZvalValue::from_raw(&mut retval);

        unsafe { ffi::folk_zval_dtor(&mut retval) };
        for p in &mut params {
            unsafe { ffi::folk_zval_dtor(p) };
        }

        match result {
            0 => Ok(return_value),
            -1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
            -2 => bail!("PHP call failed: {func_name}"),
            -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
            code => bail!("PHP call error {code}: {func_name}"),
        }
    }

    /// Call a PHP function with raw binary data (Phase A — no base64).
    ///
    /// Passes method name and binary params directly via FFI pointers.
    /// PHP function signature: `function($method, $params): string`
    /// where `$params` is raw binary (e.g. msgpack).
    pub fn call_binary(&mut self, func_name: &str, method: &str, params: &[u8]) -> Result<Vec<u8>> {
        let c_func = CString::new(func_name).context("func_name contains null byte")?;

        let mut response_buf: *mut std::ffi::c_char = ptr::null_mut();
        let mut response_len: usize = 0;

        let result = unsafe {
            ffi::folk_call_with_binary(
                c_func.as_ptr(),
                method.as_ptr().cast(),
                method.len(),
                params.as_ptr().cast(),
                params.len(),
                &mut response_buf,
                &mut response_len,
            )
        };

        let response = if !response_buf.is_null() && response_len > 0 {
            let bytes =
                unsafe { std::slice::from_raw_parts(response_buf.cast::<u8>(), response_len) };
            let owned = bytes.to_vec();
            unsafe { ffi::folk_free_buffer(response_buf) };
            owned
        } else {
            if !response_buf.is_null() {
                unsafe { ffi::folk_free_buffer(response_buf) };
            }
            Vec::new()
        };

        match result {
            0 => Ok(response),
            -1 => {
                let output = self.take_output();
                if output.is_empty() {
                    bail!("PHP call fatal error (bailout) in: {func_name}")
                } else {
                    bail!("PHP call fatal error (bailout): {output}")
                }
            },
            -2 => bail!("PHP call failed: {func_name}"),
            -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
            code => bail!("PHP call error {code}: {func_name}"),
        }
    }

    /// Take captured output, returning it as a string and clearing the buffer.
    pub fn take_output(&self) -> String {
        let mut len: usize = 0;
        let ptr = unsafe { ffi::folk_get_output(&mut len) };
        let output = if ptr.is_null() || len == 0 {
            String::new()
        } else {
            let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
            String::from_utf8_lossy(bytes).into_owned()
        };
        unsafe { ffi::folk_clear_output() };
        output
    }

    /// Get the captured response data (custom SAPI only).
    pub fn take_response(&self) -> ResponseData {
        let status = unsafe { ffi::folk_response_status_code() };
        let status = u16::try_from(status).unwrap_or(500);
        let header_count = unsafe { ffi::folk_response_header_count() };

        let mut headers = Vec::with_capacity(header_count);
        for i in 0..header_count {
            let mut len: usize = 0;
            let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
            if !ptr.is_null() && len > 0 {
                let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
                headers.push(String::from_utf8_lossy(bytes).into_owned());
            }
        }

        let body = self.take_output();

        ResponseData {
            status_code: status,
            headers,
            body,
        }
    }
}

impl Drop for PhpInstance {
    fn drop(&mut self) {
        if self.in_request {
            self.request_shutdown();
        }

        unsafe {
            ffi::folk_free_output();

            if self.custom_sapi {
                ffi::folk_response_free();
            }
        }

        // Free TSRM context for worker threads
        if !self.tsrm_ctx.is_null() {
            unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
            self.tsrm_ctx = ptr::null_mut();
        }

        // Only shutdown the module if we own it (boot, not attach)
        if self.owns_module {
            debug!("shutting down PHP SAPI");
            unsafe {
                if self.custom_sapi {
                    ffi::folk_sapi_shutdown();
                } else {
                    ffi::php_embed_shutdown();
                }
            }
        }
    }
}

/// HTTP request context for the custom Folk SAPI.
///
/// Build this in Rust, pass it to `PhpInstance::set_request_context()`,
/// then call `request_startup()`. PHP will see the request data in
/// `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`.
pub struct RequestContext {
    // Owned strings (kept alive for the duration of the request)
    method: CString,
    uri: CString,
    query_string: Option<CString>,
    content_type: Option<CString>,
    content_length: usize,
    path_translated: Option<CString>,
    post_data: Vec<u8>,
    cookie: Option<CString>,
    server_name: Option<CString>,
    server_port: i32,
    protocol: Option<CString>,

    // Headers
    header_names_c: Vec<CString>,
    header_values_c: Vec<CString>,
    header_name_ptrs: Vec<*const std::ffi::c_char>,
    header_value_ptrs: Vec<*const std::ffi::c_char>,

    // The FFI struct we pass to C
    ffi: ffi::folk_request_context,
}

impl RequestContext {
    pub fn new(method: &str, uri: &str) -> Self {
        Self {
            method: CString::new(method).expect("method contains null"),
            uri: CString::new(uri).expect("uri contains null"),
            query_string: None,
            content_type: None,
            content_length: 0,
            path_translated: None,
            post_data: Vec::new(),
            cookie: None,
            server_name: None,
            server_port: 0,
            protocol: None,
            header_names_c: Vec::new(),
            header_values_c: Vec::new(),
            header_name_ptrs: Vec::new(),
            header_value_ptrs: Vec::new(),
            ffi: unsafe { std::mem::zeroed() },
        }
    }

    #[must_use]
    pub fn query_string(mut self, qs: &str) -> Self {
        self.query_string = Some(CString::new(qs).expect("query_string contains null"));
        self
    }

    #[must_use]
    pub fn content_type(mut self, ct: &str) -> Self {
        self.content_type = Some(CString::new(ct).expect("content_type contains null"));
        self
    }

    #[must_use]
    pub fn body(mut self, data: &[u8]) -> Self {
        self.post_data = data.to_vec();
        self.content_length = data.len();
        self
    }

    #[must_use]
    pub fn cookie(mut self, cookie: &str) -> Self {
        self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
        self
    }

    #[must_use]
    pub fn path_translated(mut self, path: &str) -> Self {
        self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
        self
    }

    #[must_use]
    pub fn server(mut self, name: &str, port: i32) -> Self {
        self.server_name = Some(CString::new(name).expect("server_name contains null"));
        self.server_port = port;
        self
    }

    #[must_use]
    pub fn protocol(mut self, proto: &str) -> Self {
        self.protocol = Some(CString::new(proto).expect("protocol contains null"));
        self
    }

    #[must_use]
    pub fn header(mut self, name: &str, value: &str) -> Self {
        self.header_names_c
            .push(CString::new(name).expect("header name contains null"));
        self.header_values_c
            .push(CString::new(value).expect("header value contains null"));
        self
    }

    /// Build the FFI struct from our owned data.
    /// Must be called before passing to C — after all builder methods.
    fn build_ffi(&mut self) {
        // Build pointer arrays for headers
        self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
        self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();

        self.ffi = ffi::folk_request_context {
            request_method: self.method.as_ptr(),
            request_uri: self.uri.as_ptr(),
            query_string: self
                .query_string
                .as_ref()
                .map_or(ptr::null(), |s| s.as_ptr()),
            content_type: self
                .content_type
                .as_ref()
                .map_or(ptr::null(), |s| s.as_ptr()),
            content_length: self.content_length,
            path_translated: self
                .path_translated
                .as_ref()
                .map_or(ptr::null(), |s| s.as_ptr()),
            post_data: if self.post_data.is_empty() {
                ptr::null()
            } else {
                self.post_data.as_ptr().cast()
            },
            post_data_len: self.post_data.len(),
            post_data_read: 0,
            cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
            header_names: if self.header_name_ptrs.is_empty() {
                ptr::null()
            } else {
                self.header_name_ptrs.as_ptr()
            },
            header_values: if self.header_value_ptrs.is_empty() {
                ptr::null()
            } else {
                self.header_value_ptrs.as_ptr()
            },
            header_count: self.header_names_c.len(),
            server_name: self
                .server_name
                .as_ref()
                .map_or(ptr::null(), |s| s.as_ptr()),
            server_port: self.server_port,
            protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
        };
    }
}

/// Captured response data from PHP.
#[derive(Debug, Clone)]
pub struct ResponseData {
    /// HTTP status code (200, 404, etc.)
    pub status_code: u16,
    /// Response headers as "Name: Value" strings.
    pub headers: Vec<String>,
    /// Response body (output from echo/print).
    pub body: String,
}

/// Result of evaluating PHP code.
#[derive(Debug)]
pub struct EvalResult {
    /// Captured output from echo/print.
    pub output: String,
    /// The return value of the expression (if any).
    pub return_value: ZvalValue,
}

/// A Rust-native representation of a PHP zval value.
#[derive(Debug, Clone, PartialEq)]
pub enum ZvalValue {
    Null,
    Bool(bool),
    Long(i64),
    Double(f64),
    String(String),
    /// Type we don't handle yet (array, object, etc.)
    Other(i32),
}

impl ZvalValue {
    fn from_raw(z: &mut ffi::zval) -> Self {
        let ztype = unsafe { ffi::folk_zval_type(z) };
        match ztype {
            ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
            ffi::IS_FALSE => Self::Bool(false),
            ffi::IS_TRUE => Self::Bool(true),
            ffi::IS_LONG => {
                let v = unsafe { ffi::folk_zval_get_long(z) };
                Self::Long(v)
            },
            ffi::IS_STRING => {
                let mut len: usize = 0;
                let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
                if ptr.is_null() {
                    Self::Null
                } else {
                    let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
                    Self::String(String::from_utf8_lossy(bytes).into_owned())
                }
            },
            other => Self::Other(other),
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            Self::String(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_long(&self) -> Option<i64> {
        match self {
            Self::Long(v) => Some(*v),
            _ => None,
        }
    }
}
