//! Python application loading and initialization

use super::py_context::PythonContext;
use crate::config::ProtocolType;
use crate::errors::{FalcornError, Result};

use pyo3::types::{PyModule, PyString};
use pyo3::{IntoPyObjectExt, prelude::*};
use std::path::PathBuf;

/// Loads Python WSGI applications
pub struct AppLoader {
    pythonpath: Vec<PathBuf>,
    // logger: Logger
}

impl AppLoader {
    /// Create a new app loader
    pub fn new(pythonpath: Vec<PathBuf>) -> Self {
        Self {
            pythonpath,
            // logger: Logger::new(level, access_log_path, error_log_path)
        }
    }

    /// Load a Python WSGI application
    pub fn load(
        &self,
        module_name: &str,
        callable_name: &str,
        protocol: &ProtocolType,
    ) -> Result<Py<PyAny>> {
        Python::attach(|py| {
            // Add paths to sys.path
            self.configure_python_path(py)?;

            // Import the module
            let module = py.import(module_name)?; //self.import_module(py, module_name)?;

            // Get the callable
            let app = self.get_callable(py, &module, callable_name)?;

            // Validate app
            match protocol {
                ProtocolType::Wsgi => self.validate_wsgi_app(py, &app)?,
                ProtocolType::Asgi => self.validate_asgi_app(py, &app)?,
            }

            // load necessary modules
            self.load_needed_modules(py);

            Ok(app)
        })
    }

    /// Configure Python sys.path
    fn configure_python_path(&self, py: Python) -> Result<()> {
        let sys = py
            .import("sys")
            .map_err(|e| FalcornError::python(format!("Failed to import sys: {}", e)))?;

        let path_list = sys
            .getattr("path")
            .map_err(|e| FalcornError::python(format!("Failed to get sys.path: {}", e)))?;

        for path in &self.pythonpath {
            let path_str = path.to_string_lossy().to_string();
            let py_path = PyString::new(py, &path_str);

            path_list
                .call_method1("insert", (0, py_path))
                .map_err(|e| FalcornError::python(format!("Failed to add to sys.path: {}", e)))?;
        }

        // Add current dir to the sys pth
        path_list
            .call_method1("insert", (0, PyString::new(py, &".")))
            .map_err(|e| FalcornError::python(format!("Failed to add to sys.path: {}", e)))?;

        Ok(())
    }

    /// Import Python module
    fn import_module<'a>(&self, py: Python<'a>, module_name: &str) -> Result<Bound<'a, PyModule>> {
        py.import(module_name)
            .map_err(|e| FalcornError::PythonImportError {
                reason: format!("Failed to import '{}': {}", module_name, e),
            })
    }

    // Import necessary modules
    // and store them the context
    fn load_needed_modules(&self, py: Python<'_>) {
        let ctx = PythonContext::global();
        let modules = vec!["io", "sys", "inspect"];

        for name in modules.iter() {
            // import
            let module = self.import_module(py, name);

            match module {
                Ok(mod_) => {
                    ctx.register(name, mod_.into());
                }
                _ => {
                    println!("failed to load module {:?}", name);
                }
            }
        }

        // Cache common attributes for faster access
        if let Some(io_mod) = ctx.get("io") {
            let io_mod = io_mod.bind(py);
            if let Ok(bytes_io) = io_mod.getattr("BytesIO") {
                ctx.register("io.BytesIO", bytes_io.into_any().unbind());
            }
        }
        if let Some(sys_mod) = ctx.get("sys") {
            let sys_mod = sys_mod.bind(py);
            if let Ok(stderr) = sys_mod.getattr("stderr") {
                ctx.register("sys.stderr", stderr.into_any().unbind());
            }
        }
    }

    /// Get callable from module
    fn get_callable(
        &self,
        py: Python,
        module: &Bound<'_, PyModule>,
        callable_name: &str,
    ) -> Result<Py<PyAny>> {
        let callale = module
            .getattr(callable_name)
            .map(|obj| obj.into_py_any(py))?
            .map_err(|_| FalcornError::PythonAppNotFound {
                module: module.name().unwrap().to_string(),
                callable: callable_name.to_string(),
            })?;

        Ok(callale)
    }

    /// Validate WSGI application
    pub fn validate_wsgi_app(&self, py: Python, app: &Py<PyAny>) -> Result<()> {
        // Python::attach(|py| {
        let app_ref = app.as_ref();

        // Check if it's callable
        if !app_ref.clone().into_bound(py).is_callable() {
            return Err(FalcornError::WsgiInterfaceError);
        }

        // Try to inspect signature (optional)
        if let Ok(inspect) = py.import("inspect") {
            if let Ok(signature) = inspect.call_method1("signature", (app_ref,)) {
                let params = signature
                    .getattr("parameters")
                    .map_err(|e| FalcornError::python(format!("{}", e)))?;

                let param_count = params
                    .len()
                    .map_err(|e| FalcornError::python(format!("{}", e)))?;

                // WSGI apps should accept 2 parameters: environ and start_response
                if param_count != 2 {
                    tracing::warn!("WSGI app has {} parameters, expected 2", param_count);
                }
            }
        }

        Ok(())
        // })
    }

    /// Validate ASGI application
    pub fn validate_asgi_app(&self, py: Python, app: &Py<PyAny>) -> Result<()> {
        // Python::attach(|py| {
        let app_ref = app.bind(py);

        // Check if it's callable
        if !app_ref.is_callable() {
            return Err(FalcornError::AsgiInterfaceError);
        }

        // Check if it's a coroutine function
        let inspect = py.import("inspect")?;
        let is_coroutine = inspect
            .call_method1("iscoroutinefunction", (app_ref,))?
            .extract::<bool>()?;

        if !is_coroutine {
            tracing::warn!(
                "ASGI app is not an async function. \
                     It should be defined with 'async def'."
            );
        }

        // Try to inspect signature
        if let Ok(signature) = inspect.call_method1("signature", (app_ref,)) {
            let params = signature.getattr("parameters")?;
            let param_count = params.len()?;

            // ASGI apps should accept 3 parameters: scope, receive, send
            if param_count != 3 {
                tracing::warn!(
                    "ASGI app has {} parameters, expected 3 (scope, receive, send)",
                    param_count
                );
            }
        }

        Ok(())
        // })
    }
}

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

    #[test]
    fn test_app_loader_new() {
        let loader = AppLoader::new(vec![PathBuf::from(".")]);
        assert_eq!(loader.pythonpath.len(), 1);
    }
}
