//! Plugin Tauri commands and asset protocol.

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

use application::{ReviewPluginPackageInput, SetPluginEnabledInput, UninstallPluginInput};
use backend::dto::{
    ErrorDto, PluginAdminDto, PluginInstallResultDto, PluginReviewDto,
    PluginRuntimeContributionCatalogDto, PluginUninstallResultDto, ReviewPluginPackageDto,
};
use domain::ports::{PluginManifestValidator, PluginPackageStore, PluginRegistryStore};
use domain::{PluginId, RelativePath};
use http::{header, Response, StatusCode};
use tauri::{AppHandle, Manager, State};

use crate::state::AppState;

/// Lists installed plugins for the admin surface.
#[tauri::command]
pub async fn plugin_list_plugins(
    state: State<'_, AppState>,
) -> Result<Vec<PluginAdminDto>, ErrorDto> {
    state
        .list_plugins
        .execute()
        .await
        .map(|plugins| plugins.into_iter().map(PluginAdminDto::from).collect())
        .map_err(ErrorDto::from)
}

/// Reviews a local plugin package without committing it.
#[tauri::command]
pub async fn plugin_review_package(
    input: ReviewPluginPackageDto,
    state: State<'_, AppState>,
) -> Result<PluginReviewDto, ErrorDto> {
    let input = match input.source_kind.as_str() {
        "archive" => ReviewPluginPackageInput::Archive { path: input.path },
        "directory" => ReviewPluginPackageInput::Directory { path: input.path },
        other => {
            return Err(ErrorDto::invalid(format!(
                "invalid plugin source kind: {other}"
            )))
        }
    };
    state
        .review_plugin_package
        .execute(input)
        .await
        .map(PluginReviewDto::from)
        .map_err(ErrorDto::from)
}

/// Installs a plugin from a local archive.
#[tauri::command]
pub async fn plugin_install_from_archive(
    path: String,
    state: State<'_, AppState>,
) -> Result<PluginInstallResultDto, ErrorDto> {
    state
        .install_plugin_from_archive
        .execute(path)
        .await
        .map(PluginInstallResultDto::from)
        .map_err(ErrorDto::from)
}

/// Installs a plugin from a local directory snapshot.
#[tauri::command]
pub async fn plugin_install_from_directory(
    path: String,
    state: State<'_, AppState>,
) -> Result<PluginInstallResultDto, ErrorDto> {
    state
        .install_plugin_from_directory
        .execute(path)
        .await
        .map(PluginInstallResultDto::from)
        .map_err(ErrorDto::from)
}

/// Enables or disables a plugin.
#[tauri::command]
pub async fn plugin_set_enabled(
    plugin_id: String,
    enabled: bool,
    state: State<'_, AppState>,
) -> Result<PluginAdminDto, ErrorDto> {
    state
        .set_plugin_enabled
        .execute(SetPluginEnabledInput { plugin_id, enabled })
        .await
        .map(PluginAdminDto::from)
        .map_err(ErrorDto::from)
}

/// Uninstalls a plugin.
#[tauri::command]
pub async fn plugin_uninstall(
    plugin_id: String,
    state: State<'_, AppState>,
) -> Result<PluginUninstallResultDto, ErrorDto> {
    state
        .uninstall_plugin
        .execute(UninstallPluginInput { plugin_id })
        .await
        .map(PluginUninstallResultDto::from)
        .map_err(ErrorDto::from)
}

/// Lists active runtime contributions for frontend bootstrap.
#[tauri::command]
pub async fn plugin_list_runtime_contributions(
    state: State<'_, AppState>,
) -> Result<PluginRuntimeContributionCatalogDto, ErrorDto> {
    state
        .list_plugin_runtime_contributions
        .execute()
        .await
        .map(PluginRuntimeContributionCatalogDto::from)
        .map_err(ErrorDto::from)
}

/// Opens the plugin store folder, or one plugin folder when an id is provided.
#[tauri::command]
pub fn plugin_open_plugins_folder(
    plugin_id: Option<String>,
    app: AppHandle,
) -> Result<(), ErrorDto> {
    let mut path = app
        .path()
        .app_data_dir()
        .map_err(|e| ErrorDto::invalid(e.to_string()))?
        .join("plugins");
    if let Some(id) = plugin_id {
        let id = PluginId::new(id).map_err(|e| ErrorDto::invalid(e.to_string()))?;
        path = path.join("installed").join(id.as_str());
    }
    open_folder(&path)
}

/// Serves `idea-plugin://<pluginId>/<version>/<contentHash>/<relativePath>` after
/// checking registry state, hash and root confinement.
pub fn plugin_asset_protocol(
    app: &AppHandle,
    request: http::Request<Vec<u8>>,
) -> Response<Vec<u8>> {
    match plugin_asset_response(app, request) {
        Ok(response) => response,
        Err((status, message)) => Response::builder()
            .status(status)
            .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
            .body(message.into_bytes())
            .expect("valid protocol error response"),
    }
}

fn plugin_asset_response(
    app: &AppHandle,
    request: http::Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, (StatusCode, String)> {
    let uri = request.uri();
    let plugin_id = uri
        .host()
        .ok_or_else(|| (StatusCode::BAD_REQUEST, "missing plugin id".to_owned()))
        .and_then(|raw| {
            PluginId::new(raw.to_owned()).map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))
        })?;
    let mut parts = uri.path().trim_start_matches('/').splitn(3, '/');
    let version_segment = parts.next().unwrap_or_default();
    let hash = parts.next().unwrap_or_default();
    let rel = parts.next().unwrap_or_default();
    if version_segment.is_empty() || hash.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            "invalid plugin asset URL".to_owned(),
        ));
    }
    let rel =
        RelativePath::new(rel.to_owned()).map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
    let state = app.state::<AppState>();
    let allowed = tauri::async_runtime::block_on(asset_allowed(
        &plugin_id,
        hash,
        &rel,
        state.plugin_registry_store.as_ref(),
        state.plugin_package_store.as_ref(),
        state.plugin_manifest_validator.as_ref(),
    ))?;
    if !allowed {
        return Err((
            StatusCode::FORBIDDEN,
            "plugin asset is not active".to_owned(),
        ));
    }
    let app_data = app
        .path()
        .app_data_dir()
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    let root = app_data
        .join("plugins")
        .join("installed")
        .join(plugin_id.as_str());
    let target = root.join(rel.as_str());
    let root = root
        .canonicalize()
        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
    let target = target
        .canonicalize()
        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
    if !target.starts_with(&root) {
        return Err((
            StatusCode::FORBIDDEN,
            "asset escapes plugin root".to_owned(),
        ));
    }
    let bytes = std::fs::read(&target).map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, content_type(&target))
        .body(bytes)
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}

async fn asset_allowed(
    plugin_id: &PluginId,
    hash: &str,
    rel: &RelativePath,
    registry_store: &dyn PluginRegistryStore,
    package_store: &dyn PluginPackageStore,
    validator: &dyn PluginManifestValidator,
) -> Result<bool, (StatusCode, String)> {
    let registry = registry_store
        .load_registry()
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    let Some(entry) = registry.find(plugin_id) else {
        return Ok(false);
    };
    if !entry.lifecycle_state.is_runtime_active() || entry.content_hash.as_str() != hash {
        return Ok(false);
    }
    let package = domain::PluginPackageRef {
        plugin_id: Some(plugin_id.clone()),
        root: plugin_id.as_str().to_owned(),
    };
    let manifest_bytes = package_store
        .read_manifest(&package)
        .await
        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
    let manifest = validator
        .validate(&manifest_bytes.bytes, &package)
        .map_err(|e| (StatusCode::FORBIDDEN, e.to_string()))?;
    let declared_icon = manifest.icon.as_ref() == Some(rel);
    let declared_main = manifest.main == *rel;
    Ok(declared_main || declared_icon || rel.as_str().starts_with("assets/"))
}

fn content_type(path: &Path) -> &'static str {
    match path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or_default()
    {
        "js" | "mjs" => "text/javascript; charset=utf-8",
        "json" => "application/json; charset=utf-8",
        "svg" => "image/svg+xml",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "webp" => "image/webp",
        "css" => "text/css; charset=utf-8",
        _ => "application/octet-stream",
    }
}

fn open_folder(path: &PathBuf) -> Result<(), ErrorDto> {
    let mut command = if cfg!(target_os = "macos") {
        let mut c = std::process::Command::new("open");
        c.arg(path);
        c
    } else if cfg!(target_os = "windows") {
        let mut c = std::process::Command::new("cmd");
        c.args(["/C", "start", ""]).arg(path);
        c
    } else {
        let mut c = std::process::Command::new("xdg-open");
        c.arg(path);
        c
    };
    command
        .spawn()
        .map(|_| ())
        .map_err(|e| ErrorDto::invalid(e.to_string()))
}
