use agent_contracts::backend::{
    capability::{
        OperationExec, OperationExport, OperationFileSystem, OperationPathResolver, OperationSearch,
    },
    BackendPath, OperationBackend, OperationBackendCapabilities, OperationError,
};
use async_trait::async_trait;
use base64::Engine;
use reqwest::header::{HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Method, StatusCode};
use serde::Deserialize;
use serde_json::Value;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;

use super::error::E2bFailure;
use super::exec::E2bExec;
use super::filesystem::E2bFileSystem;
use super::path::E2bPathResolver;
use super::search::E2bSearch;

pub(crate) const E2B_PROVIDER_KIND: &str = "e2b";
pub(crate) const DEFAULT_DOMAIN: &str = "e2b.app";
pub(crate) const DEFAULT_API_BASE: &str = "https://api.e2b.app";
pub(crate) const DEFAULT_TEMPLATE_ID: &str = "base";
pub(crate) const DEFAULT_ENVD_PORT: u16 = 49983;
pub(crate) const DEFAULT_WORKSPACE_ROOT: &str = "/home/user/workspace";
pub(crate) const DEFAULT_HOME_DIR: &str = "/home/user";
pub(crate) const DEFAULT_TEMP_ROOT: &str = "/tmp";
pub(crate) const DEFAULT_SHELL: &str = "/bin/sh";
pub(crate) const DEFAULT_TIMEOUT_SECS: u64 = 3600;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum E2bLifecycle {
    Active,
    ShuttingDown,
    Closed,
}

pub(crate) struct E2bBackendState {
    pub(crate) backend_id: String,
    pub(crate) api_base: String,
    pub(crate) api_key: String,
    pub(crate) sandbox_id: String,
    pub(crate) sandbox_domain: String,
    pub(crate) envd_access_token: Option<String>,
    pub(crate) envd_port: u16,
    pub(crate) envd_scheme: String,
    pub(crate) workspace_root: BackendPath,
    pub(crate) home_dir: Option<BackendPath>,
    pub(crate) temp_root: BackendPath,
    pub(crate) default_shell: Option<String>,
    pub(crate) username: Option<String>,
    /// When true, POST /files uses multipart (legacy envd); default is octet-stream.
    pub(crate) envd_file_upload_multipart: bool,
    pub(crate) http: reqwest::Client,
    pub(crate) lifecycle: Mutex<E2bLifecycle>,
}

pub struct E2bOperationBackend {
    backend_id: String,
    capabilities: OperationBackendCapabilities,
    state: Arc<E2bBackendState>,
    paths: E2bPathResolver,
    files: E2bFileSystem,
    search: E2bSearch,
    exec: E2bExec,
}

impl E2bOperationBackend {
    pub(crate) fn new(state: Arc<E2bBackendState>) -> Self {
        Self {
            backend_id: state.backend_id.clone(),
            capabilities: OperationBackendCapabilities {
                supports_atomic_write: false,
                supports_grep: true,
                supports_export_file: true,
                supports_lsp: false,
            },
            paths: E2bPathResolver::new(Arc::clone(&state)),
            files: E2bFileSystem::new(Arc::clone(&state)),
            search: E2bSearch::new(Arc::clone(&state)),
            exec: E2bExec::new(state.clone()),
            state,
        }
    }
}

impl E2bBackendState {
    pub(crate) fn ensure_active(&self) -> Result<(), OperationError> {
        let lifecycle = self
            .lifecycle
            .lock()
            .map_err(|_| OperationError::Transport {
                message: "e2b backend state lock poisoned".to_string(),
            })?;
        match *lifecycle {
            E2bLifecycle::Active => Ok(()),
            E2bLifecycle::ShuttingDown => Err(OperationError::Transport {
                message: format!("e2b backend {} is shutting down", self.backend_id),
            }),
            E2bLifecycle::Closed => Err(OperationError::Transport {
                message: format!("e2b backend {} is already closed", self.backend_id),
            }),
        }
    }

    fn begin_shutdown(&self) -> Result<bool, OperationError> {
        let mut lifecycle = self
            .lifecycle
            .lock()
            .map_err(|_| OperationError::Transport {
                message: "e2b backend state lock poisoned".to_string(),
            })?;
        match *lifecycle {
            E2bLifecycle::Active => {
                *lifecycle = E2bLifecycle::ShuttingDown;
                Ok(true)
            }
            E2bLifecycle::ShuttingDown | E2bLifecycle::Closed => Ok(false),
        }
    }

    fn finish_shutdown(&self) -> Result<(), OperationError> {
        let mut lifecycle = self
            .lifecycle
            .lock()
            .map_err(|_| OperationError::Transport {
                message: "e2b backend state lock poisoned".to_string(),
            })?;
        *lifecycle = E2bLifecycle::Closed;
        Ok(())
    }

    fn abort_shutdown(&self) -> Result<(), OperationError> {
        let mut lifecycle = self
            .lifecycle
            .lock()
            .map_err(|_| OperationError::Transport {
                message: "e2b backend state lock poisoned".to_string(),
            })?;
        if *lifecycle == E2bLifecycle::ShuttingDown {
            *lifecycle = E2bLifecycle::Active;
        }
        Ok(())
    }

    pub(crate) fn resolve_backend_path(
        &self,
        raw_path: &str,
        base: &BackendPath,
    ) -> Result<BackendPath, OperationError> {
        if raw_path == "~" || raw_path.starts_with("~/") {
            let home_dir = self
                .home_dir
                .as_ref()
                .ok_or_else(|| OperationError::Unsupported {
                    message: "home_dir is not configured".to_string(),
                })?;
            let suffix = raw_path.strip_prefix("~/").unwrap_or_default();
            return normalize_backend_path(Path::new(home_dir.0.as_str()).join(suffix).as_path());
        }

        let candidate = Path::new(raw_path);
        if candidate.is_absolute() {
            return normalize_backend_path(candidate);
        }
        normalize_backend_path(Path::new(base.0.as_str()).join(candidate).as_path())
    }

    pub(crate) fn envd_url(&self, path: &str) -> String {
        let path = path.trim_start_matches('/');
        format!(
            "{}://{}/{}",
            self.envd_scheme,
            envd_host(
                self.envd_port,
                self.sandbox_id.as_str(),
                self.sandbox_domain.as_str()
            ),
            path
        )
    }

    pub(crate) fn platform_url(&self, path: &str) -> String {
        join_url(self.api_base.as_str(), path)
    }

    pub(crate) fn envd_request(&self, method: Method, path: &str) -> reqwest::RequestBuilder {
        let mut request = self.http.request(method, self.envd_url(path));
        if let Some(token) = self
            .envd_access_token
            .as_deref()
            .filter(|token| !token.trim().is_empty())
        {
            request = request.header("X-Access-Token", token);
        }
        if let Some(username) = self.username.as_deref().filter(|name| !name.is_empty()) {
            let encoded = base64::engine::general_purpose::STANDARD.encode(format!("{username}:"));
            if let Ok(value) = HeaderValue::from_str(format!("Basic {encoded}").as_str()) {
                request = request.header(AUTHORIZATION, value);
            }
        }
        request
    }

    pub(crate) async fn delete_sandbox(&self) -> Result<(), OperationError> {
        let started_at = Instant::now();
        let response = self
            .http
            .delete(self.platform_url(format!("/sandboxes/{}", self.sandbox_id).as_str()))
            .header("X-API-Key", self.api_key.as_str())
            .send()
            .await
            .map_err(|error| {
                let failure = E2bFailure::from_reqwest("failed to call e2b delete sandbox", &error);
                failure.log(
                    "delete_sandbox",
                    Some(self.sandbox_id.as_str()),
                    None,
                    started_at.elapsed().as_millis(),
                );
                OperationError::Transport {
                    message: failure.message,
                }
            })?;

        if response.status() == StatusCode::NO_CONTENT || response.status() == StatusCode::NOT_FOUND
        {
            return Ok(());
        }

        Err(http_error("delete e2b sandbox", response).await)
    }
}

#[async_trait]
impl OperationBackend for E2bOperationBackend {
    fn backend_id(&self) -> &str {
        self.backend_id.as_str()
    }

    fn capabilities(&self) -> OperationBackendCapabilities {
        self.capabilities
    }

    fn paths(&self) -> &dyn OperationPathResolver {
        &self.paths as &dyn OperationPathResolver
    }

    fn files(&self) -> &dyn OperationFileSystem {
        &self.files as &dyn OperationFileSystem
    }

    fn search(&self) -> &dyn OperationSearch {
        &self.search as &dyn OperationSearch
    }

    fn exec(&self) -> &dyn OperationExec {
        &self.exec as &dyn OperationExec
    }

    fn export(&self) -> &dyn OperationExport {
        &self.files as &dyn OperationExport
    }

    async fn shutdown(&self) -> Result<(), OperationError> {
        if !self.state.begin_shutdown()? {
            return Ok(());
        }
        match self.state.delete_sandbox().await {
            Ok(()) => {
                self.state.finish_shutdown()?;
                Ok(())
            }
            Err(error) => {
                self.state.abort_shutdown()?;
                Err(error)
            }
        }
    }
}

pub(crate) fn normalize_backend_path(path: &Path) -> Result<BackendPath, OperationError> {
    if !path.is_absolute() {
        return Err(OperationError::InvalidPath {
            message: format!("path must be absolute: {}", path.display()),
        });
    }

    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::RootDir => normalized.push(Path::new("/")),
            Component::CurDir => {}
            Component::ParentDir => {
                if !normalized.pop() {
                    return Err(OperationError::InvalidPath {
                        message: format!("path escapes root: {}", path.display()),
                    });
                }
            }
            Component::Normal(part) => normalized.push(part),
            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
        }
    }

    let text = normalized
        .to_str()
        .ok_or_else(|| OperationError::InvalidPath {
            message: format!("path is not valid utf-8: {}", normalized.display()),
        })?;
    Ok(BackendPath(text.to_string()))
}

pub(crate) fn shell_quote(value: &str) -> String {
    if value.is_empty() {
        return "''".to_string();
    }
    format!("'{}'", value.replace('\'', "'\\''"))
}

pub(crate) fn join_url(base: &str, path: &str) -> String {
    format!(
        "{}/{}",
        base.trim_end_matches('/'),
        path.trim_start_matches('/')
    )
}

pub(crate) fn envd_host(port: u16, sandbox_id: &str, domain: &str) -> String {
    format!("{port}-{sandbox_id}.{domain}")
}

pub(crate) async fn http_error(context: &str, response: reqwest::Response) -> OperationError {
    let status = response.status();
    let text = response.text().await.unwrap_or_default();
    let message = parse_error_message(text.as_str()).unwrap_or(text);
    match status {
        StatusCode::NOT_FOUND => OperationError::NotFound { path: message },
        StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
            OperationError::PermissionDenied { path: message }
        }
        _ => OperationError::Transport {
            message: format!("{context} failed with HTTP {status}: {message}"),
        },
    }
}

pub(crate) fn parse_error_message(text: &str) -> Option<String> {
    serde_json::from_str::<E2bError>(text)
        .ok()
        .map(|error| error.message)
        .filter(|message| !message.is_empty())
}

pub(crate) async fn connect_json<T: for<'de> Deserialize<'de>>(
    state: &E2bBackendState,
    path: &str,
    body: Value,
) -> Result<T, OperationError> {
    let started_at = Instant::now();
    let response = state
        .envd_request(Method::POST, path)
        .header("Connect-Protocol-Version", "1")
        .header(CONTENT_TYPE, "application/json")
        .header(ACCEPT, "application/json")
        .json(&body)
        .send()
        .await
        .map_err(|error| {
            let failure = E2bFailure::from_reqwest(
                format!("failed to call e2b envd {path}").as_str(),
                &error,
            );
            failure.log(
                "envd_json",
                Some(state.sandbox_id.as_str()),
                None,
                started_at.elapsed().as_millis(),
            );
            OperationError::Transport {
                message: failure.message,
            }
        })?;

    if !response.status().is_success() {
        return Err(http_error(path, response).await);
    }

    response
        .json::<T>()
        .await
        .map_err(|error| OperationError::Transport {
            message: format!("failed to decode e2b envd {path} response: {error}"),
        })
}

#[derive(Debug, Deserialize)]
struct E2bError {
    message: String,
}