#[cfg(not(test))]
use crate::adapters::proxifyre::ProxiFyreAdapter;
#[cfg(not(test))]
use crate::adapters::proxy_router::{
    ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
    ProxyRouterRequest,
};
#[cfg(not(test))]
use crate::adapters::singbox::{
    SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker,
    SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest,
};
use crate::component_detection::{
    detect_proxyfier_install, detect_proxyfier_install_with_host, detect_singbox_install,
    proxyfier_component_from_detection, singbox_component_from_detection, DetectedProxyfier,
    DetectedSingBox, ProxyfierDetectionHost, SystemProxyfierDetectionHost,
};
use crate::models::{
    ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig,
    Profile, ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol,
    SubscriptionCache, SubscriptionServer, Target, TargetInput, TargetKind,
};
#[cfg(test)]
use crate::proxifyre::ProxiFyreAdapter;
#[cfg(test)]
use crate::proxy_router::{
    ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
    ProxyRouterRequest,
};
#[cfg(test)]
use crate::singbox::{
    SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker,
    SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest,
};
use crate::singbox_service::{
    build_singbox_setup_status, ensure_safe_singbox_install_dir,
    parse_service_command_output as parse_singbox_service_command_output, service_control_script,
    ServiceCommandOutput as SingBoxServiceCommandOutput, SingBoxServiceAction, SingBoxSetupStatus,
};
use crate::storage::{default_config_root, JsonStorage};
use crate::subscription;
use crate::validation::{normalize_profile, normalize_target, ValidationError};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::net::{IpAddr, TcpStream, ToSocketAddrs, UdpSocket};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{Duration, Instant};
use std::time::{SystemTime, UNIX_EPOCH};

const PROXIFYRE_INSTALL_DIR: &str = r"C:\Tools\ProxiFyre";
const PROXIFYRE_RELEASE_API_URL: &str =
    "https://api.github.com/repos/wiresock/proxifyre/releases/latest";
const NDISAPI_RELEASE_API_URL: &str =
    "https://api.github.com/repos/wiresock/ndisapi/releases/latest";
const VC_REDIST_X64_URL: &str = "https://aka.ms/vc14/vc_redist.x64.exe";
const VC_REDIST_X86_URL: &str = "https://aka.ms/vc14/vc_redist.x86.exe";

#[derive(Debug, Clone)]
pub struct CommandState {
    root: PathBuf,
}

impl CommandState {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    pub fn storage(&self) -> JsonStorage {
        JsonStorage::new(self.root.clone())
    }
}

impl Default for CommandState {
    fn default() -> Self {
        Self::new(default_config_root())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandError {
    pub code: String,
    pub message: String,
    #[serde(default)]
    pub details: Vec<ValidationIssue>,
}

impl CommandError {
    fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
            details: Vec::new(),
        }
    }

    fn with_details(
        code: impl Into<String>,
        message: impl Into<String>,
        details: Vec<ValidationIssue>,
    ) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
            details,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationIssue {
    pub field: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusResponse {
    pub route_line: String,
    pub active_profile_count: usize,
    pub routed_app_count: usize,
    pub active_target: Option<TargetDto>,
    pub components: Vec<ComponentStatusDto>,
    pub recent_activity: Vec<ActivityEntryDto>,
    pub generated_config_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SavedStateResponse {
    pub profiles: Vec<ProfileDto>,
    pub targets: Vec<TargetDto>,
    pub generated_config_path: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxiFyreSetupStatusDto {
    pub ready: bool,
    pub missing_count: usize,
    pub items: Vec<ProxiFyreSetupItemDto>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxiFyreSetupItemDto {
    pub id: String,
    pub name: String,
    pub installed: bool,
    pub version: Option<String>,
    pub details: String,
}

pub type SingBoxSetupStatusDto = SingBoxSetupStatus;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalSingBoxStatusResponse {
    pub config: LocalSingBoxConfigDto,
    pub cache: Option<SubscriptionCacheDto>,
    pub component: ComponentStatusDto,
    pub generated_config_path: String,
    pub lan_listen_host: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalSingBoxConfigDto {
    pub subscription_display_url: Option<String>,
    pub has_subscription: bool,
    pub selected_server_tag: Option<String>,
    pub listen_host: String,
    pub listen_port: u16,
    pub service_name: String,
    pub install_root: String,
    pub updated_at: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionCacheDto {
    pub servers: Vec<SubscriptionServerDto>,
    pub user_info: serde_json::Map<String, serde_json::Value>,
    pub fetched_at: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionServerDto {
    pub tag: String,
    #[serde(rename = "type")]
    pub server_type: String,
    pub server: String,
    pub server_port: u16,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveSingBoxSubscriptionInputDto {
    pub subscription_url: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SelectSingBoxServerInputDto {
    pub tag: String,
    #[serde(default)]
    pub server: Option<String>,
    #[serde(default)]
    pub server_port: Option<u16>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PingSingBoxServerInputDto {
    pub tag: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PingProxyTargetInputDto {
    pub host: String,
    pub port: u16,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PingServerResponse {
    pub tag: String,
    pub server: String,
    pub server_port: u16,
    pub ok: bool,
    pub latency: Option<u128>,
    pub error: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateSingBoxConfigResponse {
    pub success: bool,
    pub message: String,
    pub adapter_id: String,
    pub generated_config_path: String,
    pub selected_server_tag: String,
    pub listen_host: String,
    pub listen_port: u16,
    pub check: Option<SingBoxCheckResult>,
    pub activity: ActivityEntryDto,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInputDto {
    pub id: Option<String>,
    pub name: String,
    #[serde(default)]
    pub enabled: Option<bool>,
    #[serde(default)]
    pub target_id: Option<String>,
    #[serde(default)]
    pub protocols: Option<Vec<String>>,
    #[serde(default)]
    pub items: Option<Vec<ProfileItemInputDto>>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileItemInputDto {
    #[serde(rename = "type")]
    pub item_type: String,
    pub value: String,
    #[serde(default)]
    pub recursive: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetInputDto {
    pub id: Option<String>,
    pub name: String,
    #[serde(default)]
    pub kind: Option<String>,
    #[serde(default)]
    pub protocol: Option<String>,
    pub host: String,
    pub port: u32,
    #[serde(default)]
    pub requires_component: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileDto {
    pub id: String,
    pub name: String,
    pub enabled: bool,
    pub target_id: String,
    pub protocols: Vec<Protocol>,
    pub items: Vec<ProfileItemDto>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileItemDto {
    #[serde(rename = "type")]
    pub item_type: ProfileItemType,
    pub value: String,
    pub recursive: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetDto {
    pub id: String,
    pub name: String,
    pub kind: TargetKind,
    pub protocol: ProxyProtocol,
    pub host: String,
    pub port: u16,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub requires_component: Option<ComponentId>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComponentStatusDto {
    pub id: ComponentId,
    pub name: String,
    pub state: ComponentState,
    pub installed: bool,
    pub running: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    pub problems: Vec<String>,
    pub actions: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityEntryDto {
    pub id: String,
    pub at: String,
    pub level: ActivityLevel,
    pub title: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveProfilePreviewResponse {
    pub profile_id: String,
    pub apps: Vec<ResolvedAppDto>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedAppDto {
    pub source_type: ProfileItemType,
    pub source_value: String,
    pub app_name: String,
    pub notes: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplyProfilesResponse {
    pub success: bool,
    pub changed: bool,
    pub message: String,
    pub adapter_id: String,
    pub generated_config_path: String,
    pub enabled_profiles: usize,
    pub routed_apps: usize,
    pub helper: HelperApplyResult,
    pub activity: ActivityEntryDto,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HelperApplyResult {
    pub success: bool,
    pub changed: bool,
    pub action: String,
    pub message: String,
}

pub struct HelperApplyRequest<'a> {
    pub adapter_id: &'a str,
    pub config_path: &'a Path,
    pub config_contents: &'a str,
}

pub trait ProxyApplyHelper {
    fn apply_proxy_config(
        &self,
        request: HelperApplyRequest<'_>,
    ) -> Result<HelperApplyResult, CommandError>;
}

pub trait SubscriptionFetcher {
    fn fetch_subscription(
        &self,
        url: &str,
    ) -> Result<SubscriptionCache, subscription::SubscriptionError>;
}

pub struct SystemSubscriptionFetcher;

impl SubscriptionFetcher for SystemSubscriptionFetcher {
    fn fetch_subscription(
        &self,
        url: &str,
    ) -> Result<SubscriptionCache, subscription::SubscriptionError> {
        subscription::fetch_subscription(url)
    }
}

pub trait Clock {
    fn now(&self) -> String;
}

pub struct SystemClock;

impl Clock for SystemClock {
    fn now(&self) -> String {
        let seconds = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|duration| duration.as_secs())
            .unwrap_or(0);
        format!("unix:{seconds}")
    }
}

pub struct StagedApplyHelper;

impl ProxyApplyHelper for StagedApplyHelper {
    fn apply_proxy_config(
        &self,
        request: HelperApplyRequest<'_>,
    ) -> Result<HelperApplyResult, CommandError> {
        Ok(HelperApplyResult {
            success: true,
            changed: true,
            action: format!("{}.stage-generated-config", request.adapter_id),
            message: format!(
                "Сгенерированный конфиг подготовлен в {}; интеграция привилегированного помощника еще не подключена",
                request.config_path.display()
            ),
        })
    }
}

pub struct DetectedProxyApplyHelper<H = SystemProxyfierDetectionHost> {
    host: H,
}

impl DetectedProxyApplyHelper<SystemProxyfierDetectionHost> {
    pub fn system() -> Self {
        Self {
            host: SystemProxyfierDetectionHost,
        }
    }
}

impl<H> DetectedProxyApplyHelper<H> {
    pub fn new(host: H) -> Self {
        Self { host }
    }
}

impl<H> ProxyApplyHelper for DetectedProxyApplyHelper<H>
where
    H: ProxyfierDetectionHost,
{
    fn apply_proxy_config(
        &self,
        request: HelperApplyRequest<'_>,
    ) -> Result<HelperApplyResult, CommandError> {
        let Some(detected) = detect_proxyfier_install_with_host(&self.host) else {
            return staged_apply_result(request);
        };

        apply_to_detected_proxyfier(request, &detected)
    }
}

#[tauri::command]
pub async fn get_status(
    state: tauri::State<'_, CommandState>,
) -> Result<StatusResponse, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || build_status(&storage))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub fn get_saved_state(
    state: tauri::State<'_, CommandState>,
) -> Result<SavedStateResponse, CommandError> {
    read_saved_state(&state.storage())
}

#[tauri::command]
pub fn get_profiles(
    state: tauri::State<'_, CommandState>,
) -> Result<Vec<ProfileDto>, CommandError> {
    read_profiles(&state.storage())
}

#[tauri::command]
pub fn save_profile(
    state: tauri::State<'_, CommandState>,
    input: ProfileInputDto,
) -> Result<ProfileDto, CommandError> {
    save_profile_to_storage(&state.storage(), input)
}

#[tauri::command]
pub fn get_targets(state: tauri::State<'_, CommandState>) -> Result<Vec<TargetDto>, CommandError> {
    read_targets(&state.storage())
}

#[tauri::command]
pub fn save_target(
    state: tauri::State<'_, CommandState>,
    input: TargetInputDto,
) -> Result<TargetDto, CommandError> {
    save_target_to_storage(&state.storage(), input)
}

#[tauri::command]
pub async fn get_components(
    state: tauri::State<'_, CommandState>,
) -> Result<Vec<ComponentStatusDto>, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || read_components(&storage))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn get_proxifyre_setup_status() -> Result<ProxiFyreSetupStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(build_proxifyre_setup_status)
        .await
        .map_err(background_task_error)
}

#[tauri::command]
pub async fn get_singbox_status(
    state: tauri::State<'_, CommandState>,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || read_singbox_status(&storage))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn get_singbox_setup_status() -> Result<SingBoxSetupStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(|| {
        build_singbox_setup_status(detect_singbox_install().as_ref())
    })
    .await
    .map_err(background_task_error)
}

#[tauri::command]
pub fn resolve_profile_preview(
    input: ProfileInputDto,
) -> Result<ResolveProfilePreviewResponse, CommandError> {
    resolve_preview(input)
}

#[tauri::command]
pub fn save_singbox_subscription(
    state: tauri::State<'_, CommandState>,
    input: SaveSingBoxSubscriptionInputDto,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    save_singbox_subscription_to_storage(&state.storage(), input, &SystemClock)
}

#[tauri::command]
pub async fn fetch_singbox_subscription(
    state: tauri::State<'_, CommandState>,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || {
        fetch_singbox_subscription_with_fetcher(&storage, &SystemSubscriptionFetcher, &SystemClock)
    })
    .await
    .map_err(background_task_error)?
}

#[tauri::command]
pub fn forget_singbox_subscription(
    state: tauri::State<'_, CommandState>,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    forget_singbox_subscription_in_storage(&state.storage(), &SystemClock)
}

#[tauri::command]
pub fn select_singbox_server(
    state: tauri::State<'_, CommandState>,
    input: SelectSingBoxServerInputDto,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    select_singbox_server_in_storage(&state.storage(), input, &SystemClock)
}

#[tauri::command]
pub fn ping_singbox_server(
    state: tauri::State<'_, CommandState>,
    input: PingSingBoxServerInputDto,
) -> Result<PingServerResponse, CommandError> {
    ping_singbox_server_in_storage(&state.storage(), input)
}

#[tauri::command]
pub fn ping_all_singbox_servers(
    state: tauri::State<'_, CommandState>,
) -> Result<Vec<PingServerResponse>, CommandError> {
    ping_all_singbox_servers_in_storage(&state.storage())
}

#[tauri::command]
pub fn ping_proxy_target(input: PingProxyTargetInputDto) -> Result<PingServerResponse, CommandError> {
    ping_proxy_target_endpoint(input)
}

#[tauri::command]
pub fn generate_singbox_config(
    state: tauri::State<'_, CommandState>,
) -> Result<GenerateSingBoxConfigResponse, CommandError> {
    let detected = detect_singbox_install();
    let binary_path = detected
        .as_ref()
        .map(|detected| detected.executable_path.as_path());
    generate_singbox_config_with_services(
        &state.storage(),
        &SingBoxAdapter::default(),
        &SingBoxCommandChecker,
        &SystemClock,
        binary_path,
    )
}

#[tauri::command]
pub fn apply_profiles(
    state: tauri::State<'_, CommandState>,
) -> Result<ApplyProfilesResponse, CommandError> {
    let storage = state.storage();
    let adapter = ProxiFyreAdapter::default();
    let helper = DetectedProxyApplyHelper::system();
    let clock = SystemClock;

    apply_profiles_with_services(&storage, &adapter, &helper, &clock)
}

#[tauri::command]
pub fn get_logs(
    state: tauri::State<'_, CommandState>,
) -> Result<Vec<ActivityEntryDto>, CommandError> {
    read_activity(&state.storage())
}

#[tauri::command]
pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result<String, CommandError> {
    let storage = state.storage();
    let generated_path = storage
        .paths()
        .generated_dir
        .join("proxifyre-app-config.json");
    let config_path = detect_proxyfier_install()
        .and_then(|detected| detected.config_path)
        .filter(|path| path.exists())
        .or_else(|| generated_path.exists().then_some(generated_path.clone()));

    let Some(config_path) = config_path else {
        return open_folder(&storage.paths().generated_dir);
    };

    open_file_or_select(&config_path)?;
    Ok(config_path.display().to_string())
}

#[tauri::command]
pub async fn start_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Start))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn stop_proxifyre_service() -> Result<ComponentStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Stop))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn install_proxifyre(
    state: tauri::State<'_, CommandState>,
) -> Result<ComponentStatusDto, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || install_proxifyre_component(&storage))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn uninstall_proxifyre() -> Result<ComponentStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(uninstall_proxifyre_component)
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn start_singbox_service(
    state: tauri::State<'_, CommandState>,
) -> Result<ComponentStatusDto, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || {
        let detected = detect_singbox_install();
        let binary_path = detected
            .as_ref()
            .map(|detected| detected.executable_path.as_path());
        let generated = generate_singbox_config_with_services(
            &storage,
            &SingBoxAdapter::default(),
            &SingBoxCommandChecker,
            &SystemClock,
            binary_path,
        )?;
        let generated_path = PathBuf::from(generated.generated_config_path);
        control_singbox_service(SingBoxServiceAction::Start, Some(generated_path.as_path()))
    })
    .await
    .map_err(background_task_error)?
}

#[tauri::command]
pub async fn stop_singbox_service() -> Result<ComponentStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(|| control_singbox_service(SingBoxServiceAction::Stop, None))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn install_singbox(
    state: tauri::State<'_, CommandState>,
) -> Result<ComponentStatusDto, CommandError> {
    let storage = state.storage();
    tauri::async_runtime::spawn_blocking(move || install_singbox_component(&storage))
        .await
        .map_err(background_task_error)?
}

#[tauri::command]
pub async fn uninstall_singbox() -> Result<ComponentStatusDto, CommandError> {
    tauri::async_runtime::spawn_blocking(uninstall_singbox_component)
        .await
        .map_err(background_task_error)?
}

pub fn build_status(storage: &JsonStorage) -> Result<StatusResponse, CommandError> {
    let profiles = storage.read_profiles().map_err(storage_error)?;
    let targets = storage.read_targets().map_err(storage_error)?;
    let components = components_or_defaults(storage)?;
    let activity = storage.read_activity().map_err(storage_error)?;
    let active_profile_count = profiles.iter().filter(|profile| profile.enabled).count();
    let routed_app_count = profiles
        .iter()
        .filter(|profile| profile.enabled)
        .map(|profile| profile.items.len())
        .sum();
    let active_target = profiles
        .iter()
        .find(|profile| profile.enabled)
        .and_then(|profile| targets.iter().find(|target| target.id == profile.target_id));
    let route_line = route_line(active_target);

    Ok(StatusResponse {
        route_line,
        active_profile_count,
        routed_app_count,
        active_target: active_target.map(TargetDto::from),
        components: components.iter().map(ComponentStatusDto::from).collect(),
        recent_activity: activity
            .iter()
            .take(10)
            .map(ActivityEntryDto::from)
            .collect(),
        generated_config_path: storage
            .paths()
            .generated_dir
            .join("proxifyre-app-config.json")
            .display()
            .to_string(),
    })
}

pub fn read_profiles(storage: &JsonStorage) -> Result<Vec<ProfileDto>, CommandError> {
    storage
        .read_profiles()
        .map_err(storage_error)
        .map(|profiles| profiles.iter().map(ProfileDto::from).collect())
}

pub fn save_profile_to_storage(
    storage: &JsonStorage,
    input: ProfileInputDto,
) -> Result<ProfileDto, CommandError> {
    let profile = normalize_profile(input.into()).map_err(validation_error)?;
    let mut profiles = storage.read_profiles().map_err(storage_error)?;

    match profiles
        .iter()
        .position(|existing| existing.id == profile.id)
    {
        Some(index) => profiles[index] = profile.clone(),
        None => profiles.push(profile.clone()),
    }

    storage.write_profiles(&profiles).map_err(storage_error)?;
    Ok(ProfileDto::from(&profile))
}

pub fn read_targets(storage: &JsonStorage) -> Result<Vec<TargetDto>, CommandError> {
    storage
        .read_targets()
        .map_err(storage_error)
        .map(|targets| targets.iter().map(TargetDto::from).collect())
}

pub fn save_target_to_storage(
    storage: &JsonStorage,
    input: TargetInputDto,
) -> Result<TargetDto, CommandError> {
    let target = normalize_target(input.into()).map_err(validation_error)?;
    let mut targets = storage.read_targets().map_err(storage_error)?;

    match targets.iter().position(|existing| existing.id == target.id) {
        Some(index) => targets[index] = target.clone(),
        None => targets.push(target.clone()),
    }

    storage.write_targets(&targets).map_err(storage_error)?;
    Ok(TargetDto::from(&target))
}

pub fn read_components(storage: &JsonStorage) -> Result<Vec<ComponentStatusDto>, CommandError> {
    components_or_defaults(storage).map(|components| {
        components
            .iter()
            .map(ComponentStatusDto::from)
            .collect::<Vec<_>>()
    })
}

pub fn read_activity(storage: &JsonStorage) -> Result<Vec<ActivityEntryDto>, CommandError> {
    storage
        .read_activity()
        .map_err(storage_error)
        .map(|entries| entries.iter().map(ActivityEntryDto::from).collect())
}

pub fn read_saved_state(storage: &JsonStorage) -> Result<SavedStateResponse, CommandError> {
    Ok(SavedStateResponse {
        profiles: read_profiles(storage)?,
        targets: read_targets(storage)?,
        generated_config_path: storage
            .paths()
            .generated_dir
            .join("proxifyre-app-config.json")
            .display()
            .to_string(),
    })
}

pub fn resolve_preview(
    input: ProfileInputDto,
) -> Result<ResolveProfilePreviewResponse, CommandError> {
    let profile = normalize_profile(input.into()).map_err(validation_error)?;
    let mut warnings = Vec::new();
    let apps = profile
        .items
        .iter()
        .map(|item| resolved_app(item, &mut warnings))
        .collect();

    Ok(ResolveProfilePreviewResponse {
        profile_id: profile.id,
        apps,
        warnings,
    })
}

pub fn apply_profiles_with_services(
    storage: &JsonStorage,
    adapter: &impl ProxyRouterAdapter,
    helper: &impl ProxyApplyHelper,
    clock: &impl Clock,
) -> Result<ApplyProfilesResponse, CommandError> {
    let profiles = storage.read_profiles().map_err(storage_error)?;
    let targets = storage.read_targets().map_err(storage_error)?;
    let components = components_or_defaults(storage)?;
    let generated =
        match adapter.generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) {
            Ok(generated) => generated,
            Err(error) => {
                let command_error = adapter_error(error);
                let activity = activity_for_apply_error(clock, &command_error);
                storage.append_activity(activity).map_err(storage_error)?;
                return Err(command_error);
            }
        };

    let generated_path = storage
        .paths()
        .generated_dir
        .join(generated.output_file_name.as_str());
    write_generated_config(&generated_path, &generated.contents)?;

    let helper_result = helper.apply_proxy_config(HelperApplyRequest {
        adapter_id: generated.adapter_id.as_str(),
        config_path: &generated_path,
        config_contents: generated.contents.as_str(),
    })?;

    let activity = activity_for_apply(clock, &generated, &generated_path, &helper_result);
    storage
        .append_activity(activity.clone())
        .map_err(storage_error)?;

    Ok(ApplyProfilesResponse {
        success: helper_result.success,
        changed: helper_result.changed,
        message: helper_result.message.clone(),
        adapter_id: generated.adapter_id,
        generated_config_path: generated_path.display().to_string(),
        enabled_profiles: generated.enabled_profiles,
        routed_apps: generated.routed_apps,
        helper: helper_result,
        activity: ActivityEntryDto::from(&activity),
    })
}

pub fn read_singbox_status(
    storage: &JsonStorage,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let config = storage.read_local_singbox_config().map_err(storage_error)?;
    let cache = storage
        .read_singbox_subscription_cache()
        .map_err(storage_error)?;
    let detected = detect_singbox_install();
    let component = singbox_component_from_detection(detected.as_ref());

    Ok(LocalSingBoxStatusResponse {
        config: LocalSingBoxConfigDto::from(&config),
        cache: cache.as_ref().map(SubscriptionCacheDto::from),
        component: ComponentStatusDto::from(&component),
        generated_config_path: storage
            .paths()
            .generated_dir
            .join("sing-box-config.json")
            .display()
            .to_string(),
        lan_listen_host: local_lan_ipv4(),
    })
}

pub fn save_singbox_subscription_to_storage(
    storage: &JsonStorage,
    input: SaveSingBoxSubscriptionInputDto,
    clock: &impl Clock,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let subscription_url = input.subscription_url.trim().to_string();
    validate_subscription_url(&subscription_url)?;

    let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
    config.subscription_url = Some(subscription_url);
    config.updated_at = Some(clock.now());
    storage
        .write_local_singbox_config(&config)
        .map_err(storage_error)?;

    read_singbox_status(storage)
}

pub fn fetch_singbox_subscription_with_fetcher(
    storage: &JsonStorage,
    fetcher: &impl SubscriptionFetcher,
    clock: &impl Clock,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
    let subscription_url = config
        .subscription_url
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .ok_or_else(|| {
            CommandError::new(
                "singbox_subscription_missing",
                "Ссылка на подписку Local sing-box не сохранена.",
            )
        })?;

    let cache = fetcher
        .fetch_subscription(&subscription_url)
        .map_err(|error| CommandError::new("singbox_subscription_fetch_failed", error.message))?;
    let selected_tag = config
        .selected_server_tag
        .as_deref()
        .filter(|tag| cache.servers.iter().any(|server| server.tag == *tag))
        .map(str::to_string)
        .or_else(|| cache.servers.first().map(|server| server.tag.clone()));

    config.selected_server_tag = selected_tag;
    config.updated_at = Some(clock.now());
    storage
        .write_singbox_subscription_cache(&cache)
        .map_err(storage_error)?;
    storage
        .write_local_singbox_config(&config)
        .map_err(storage_error)?;
    storage
        .append_activity(ActivityEntry {
            id: "singbox-subscription-fetched".to_string(),
            at: clock.now(),
            level: ActivityLevel::Success,
            title: "Подписка Local sing-box обновлена".to_string(),
            message: format!("Серверов найдено: {}", cache.servers.len()),
        })
        .map_err(storage_error)?;

    read_singbox_status(storage)
}

pub fn forget_singbox_subscription_in_storage(
    storage: &JsonStorage,
    clock: &impl Clock,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
    config.subscription_url = None;
    config.selected_server_tag = None;
    config.updated_at = Some(clock.now());
    storage
        .write_local_singbox_config(&config)
        .map_err(storage_error)?;
    storage
        .remove_singbox_subscription_cache()
        .map_err(storage_error)?;

    read_singbox_status(storage)
}

pub fn select_singbox_server_in_storage(
    storage: &JsonStorage,
    input: SelectSingBoxServerInputDto,
    clock: &impl Clock,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
    let requested_tag = input.tag.trim().to_string();
    if requested_tag.is_empty() {
        return Err(CommandError::new(
            "singbox_server_tag_missing",
            "Сервер Local sing-box не выбран.",
        ));
    }

    let cache = storage
        .read_singbox_subscription_cache()
        .map_err(storage_error)?
        .ok_or_else(|| {
            CommandError::new(
                "singbox_subscription_cache_missing",
                "Сначала нужно загрузить подписку Local sing-box.",
            )
        })?;
    let Some(server) = find_subscription_server(
        &cache,
        &requested_tag,
        input.server.as_deref(),
        input.server_port,
    ) else {
        return Err(CommandError::new(
            "singbox_server_not_found",
            format!("Сервер Local sing-box '{requested_tag}' не найден в текущей подписке."),
        ));
    };
    let selected_tag = server.tag.clone();

    let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
    config.selected_server_tag = Some(selected_tag);
    config.updated_at = Some(clock.now());
    storage
        .write_local_singbox_config(&config)
        .map_err(storage_error)?;

    read_singbox_status(storage)
}

pub fn ping_singbox_server_in_storage(
    storage: &JsonStorage,
    input: PingSingBoxServerInputDto,
) -> Result<PingServerResponse, CommandError> {
    let tag = input.tag.trim();
    let cache = read_required_singbox_cache(storage)?;
    let server = find_subscription_server(&cache, tag, None, None).ok_or_else(|| {
        CommandError::new(
            "singbox_server_not_found",
            format!("Сервер Local sing-box '{tag}' не найден в текущей подписке."),
        )
    })?;

    Ok(ping_subscription_server(server))
}

pub fn ping_all_singbox_servers_in_storage(
    storage: &JsonStorage,
) -> Result<Vec<PingServerResponse>, CommandError> {
    let cache = read_required_singbox_cache(storage)?;
    Ok(cache.servers.iter().map(ping_subscription_server).collect())
}

pub fn ping_proxy_target_endpoint(
    input: PingProxyTargetInputDto,
) -> Result<PingServerResponse, CommandError> {
    let host = input.host.trim();
    if host.is_empty() {
        return Err(CommandError::new(
            "proxy_target_host_missing",
            "Хост внешнего прокси не указан.",
        ));
    }

    Ok(ping_endpoint("external-proxy", host, input.port))
}

pub fn generate_singbox_config_with_services<C>(
    storage: &JsonStorage,
    adapter: &SingBoxAdapter,
    checker: &C,
    clock: &impl Clock,
    binary_path: Option<&Path>,
) -> Result<GenerateSingBoxConfigResponse, CommandError>
where
    C: SingBoxConfigChecker,
{
    let config = storage.read_local_singbox_config().map_err(storage_error)?;
    let cache = read_required_singbox_cache(storage)?;
    let generated = adapter
        .generate_config(
            SingBoxGenerationRequest::new(&config, &cache, binary_path),
            checker,
        )
        .map_err(singbox_adapter_error)?;
    let generated_path = storage
        .paths()
        .generated_dir
        .join(generated.output_file_name.as_str());

    write_generated_config(&generated_path, &generated.contents)?;
    ensure_local_singbox_target(storage, &config)?;

    let activity = activity_for_singbox_generate(clock, &generated, &generated_path);
    storage
        .append_activity(activity.clone())
        .map_err(storage_error)?;

    Ok(GenerateSingBoxConfigResponse {
        success: true,
        message: "Конфиг Local sing-box создан".to_string(),
        adapter_id: generated.adapter_id,
        generated_config_path: generated_path.display().to_string(),
        selected_server_tag: generated.selected_server_tag,
        listen_host: generated.listen,
        listen_port: generated.listen_port,
        check: generated.check,
        activity: ActivityEntryDto::from(&activity),
    })
}

fn read_required_singbox_cache(storage: &JsonStorage) -> Result<SubscriptionCache, CommandError> {
    storage
        .read_singbox_subscription_cache()
        .map_err(storage_error)?
        .ok_or_else(|| {
            CommandError::new(
                "singbox_subscription_cache_missing",
                "Сначала нужно загрузить подписку Local sing-box.",
            )
        })
}

fn validate_subscription_url(subscription_url: &str) -> Result<(), CommandError> {
    if subscription_url.is_empty() {
        return Err(CommandError::new(
            "singbox_subscription_url_missing",
            "Ссылка на подписку Local sing-box не указана.",
        ));
    }

    let parsed = url::Url::parse(subscription_url).map_err(|_| {
        CommandError::new(
            "singbox_subscription_url_invalid",
            "Ссылка на подписку Local sing-box должна быть корректным URL.",
        )
    })?;
    if !matches!(parsed.scheme(), "http" | "https") {
        return Err(CommandError::new(
            "singbox_subscription_url_invalid",
            "Ссылка на подписку Local sing-box должна начинаться с http:// или https://.",
        ));
    }

    Ok(())
}

fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse {
    ping_endpoint(&server.tag, &server.server, server.server_port)
}

fn ping_endpoint(tag: &str, server: &str, server_port: u16) -> PingServerResponse {
    let started = Instant::now();
    let addresses = match (server, server_port).to_socket_addrs() {
        Ok(addresses) => addresses.collect::<Vec<_>>(),
        Err(error) => {
            return PingServerResponse {
                tag: tag.to_string(),
                server: server.to_string(),
                server_port,
                ok: false,
                latency: None,
                error: Some(format!("DNS/адрес недоступен: {error}")),
            };
        }
    };

    if addresses.is_empty() {
        return PingServerResponse {
            tag: tag.to_string(),
            server: server.to_string(),
            server_port,
            ok: false,
            latency: None,
            error: Some("DNS не вернул адреса".to_string()),
        };
    }

    let timeout = Duration::from_secs(2);
    let mut last_error = None;
    for address in addresses {
        match TcpStream::connect_timeout(&address, timeout) {
            Ok(_) => {
                return PingServerResponse {
                    tag: tag.to_string(),
                    server: server.to_string(),
                    server_port,
                    ok: true,
                    latency: Some(started.elapsed().as_millis()),
                    error: None,
                };
            }
            Err(error) => last_error = Some(error.to_string()),
        }
    }

    PingServerResponse {
        tag: tag.to_string(),
        server: server.to_string(),
        server_port,
        ok: false,
        latency: None,
        error: last_error,
    }
}

fn local_lan_ipv4() -> Option<String> {
    let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
    socket.connect("8.8.8.8:80").ok()?;
    let IpAddr::V4(address) = socket.local_addr().ok()?.ip() else {
        return None;
    };
    if address.is_loopback() || address.is_link_local() || address.is_unspecified() {
        return None;
    }
    Some(address.to_string())
}

fn find_subscription_server<'a>(
    cache: &'a SubscriptionCache,
    requested_tag: &str,
    requested_server: Option<&str>,
    requested_port: Option<u16>,
) -> Option<&'a SubscriptionServer> {
    cache
        .servers
        .iter()
        .find(|server| server.tag == requested_tag)
        .or_else(|| {
            let requested = comparable_server_tag(requested_tag);
            cache
                .servers
                .iter()
                .find(|server| comparable_server_tag(&server.tag) == requested)
        })
        .or_else(|| {
            let server_name = requested_server?.trim();
            let server_port = requested_port?;
            cache.servers.iter().find(|server| {
                server.server.eq_ignore_ascii_case(server_name) && server.server_port == server_port
            })
        })
}

fn comparable_server_tag(value: &str) -> String {
    value
        .chars()
        .filter(|ch| !matches!(ch, '\u{fe0e}' | '\u{fe0f}' | '\u{200d}'))
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

fn ensure_local_singbox_target(
    storage: &JsonStorage,
    config: &LocalSingBoxConfig,
) -> Result<(), CommandError> {
    let mut targets = storage.read_targets().map_err(storage_error)?;
    let target = Target {
        id: "local-singbox".to_string(),
        name: "Локальный sing-box".to_string(),
        kind: TargetKind::Local,
        protocol: ProxyProtocol::Socks5,
        host: config.listen_host.clone(),
        port: config.listen_port,
        requires_component: Some(ComponentId::Singbox),
    };

    match targets.iter().position(|existing| existing.id == target.id) {
        Some(index) => targets[index] = target,
        None => targets.push(target),
    }

    storage.write_targets(&targets).map_err(storage_error)
}

fn activity_for_singbox_generate(
    clock: &impl Clock,
    generated: &SingBoxGeneratedConfig,
    generated_path: &Path,
) -> ActivityEntry {
    ActivityEntry {
        id: "singbox-config-generated".to_string(),
        at: clock.now(),
        level: ActivityLevel::Success,
        title: "Конфиг Local sing-box создан".to_string(),
        message: format!(
            "Сервер: {}, listen: {}:{}, конфиг: {}",
            generated.selected_server_tag,
            generated.listen,
            generated.listen_port,
            generated_path.display()
        ),
    }
}

fn singbox_adapter_error(error: SingBoxConfigError) -> CommandError {
    let code = match error.kind {
        SingBoxConfigErrorKind::MissingSelectedServer => "singbox_server_not_selected",
        SingBoxConfigErrorKind::MissingSelectedOutbound => "singbox_selected_server_missing",
        SingBoxConfigErrorKind::UnsupportedSelectedOutbound => {
            "singbox_selected_server_unsupported"
        }
        SingBoxConfigErrorKind::Serialization => "serialization_error",
        SingBoxConfigErrorKind::CheckFailed => "singbox_check_failed",
    };

    CommandError::new(code, error.message)
}

fn control_singbox_service(
    action: SingBoxServiceAction,
    config_source: Option<&Path>,
) -> Result<ComponentStatusDto, CommandError> {
    let Some(detected) = detect_singbox_install() else {
        return Err(CommandError::new(
            "singbox_not_found",
            "Local sing-box не найден на компьютере.",
        ));
    };

    let config_target = config_source.map(|_| detected.install_dir.join("config.json"));
    let script = service_control_script(
        action,
        &detected.service_name,
        config_source,
        config_target.as_deref(),
    );
    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            script.as_str(),
        ])
        .output()
        .map_err(|error| {
            CommandError::new(
                singbox_service_error_code(action),
                format!(
                    "Не удалось {} службу Local sing-box: {error}",
                    action.label()
                ),
            )
        })?;
    let result = parse_singbox_service_command_output(&output.stdout).ok_or_else(|| {
        CommandError::new(
            singbox_service_error_code(action),
            singbox_service_script_failed_message(action, output.status.code()),
        )
    })?;

    if result.success {
        let refreshed = detect_singbox_install();
        let component = singbox_component_from_detection(refreshed.as_ref());
        return Ok(ComponentStatusDto::from(&component));
    }

    if matches!(
        result.code.as_str(),
        "start_failed" | "stop_failed" | "config_sync_failed"
    ) {
        run_elevated_singbox_service_command(
            action,
            &detected.service_name,
            config_source,
            config_target.as_deref(),
            &result,
        )?;
        let refreshed = detect_singbox_install();
        let component = singbox_component_from_detection(refreshed.as_ref());
        return Ok(ComponentStatusDto::from(&component));
    }

    Err(CommandError::new(
        singbox_service_error_code(action),
        singbox_service_command_failed_message(action, &result),
    ))
}

fn run_elevated_singbox_service_command(
    action: SingBoxServiceAction,
    service_name: &str,
    config_source: Option<&Path>,
    config_target: Option<&Path>,
    direct_result: &SingBoxServiceCommandOutput,
) -> Result<(), CommandError> {
    let script_path =
        write_elevated_singbox_service_script(action, service_name, config_source, config_target)?;
    let launch_script = format!(
        "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
        escape_powershell_single(&script_path.display().to_string())
    );
    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            launch_script.as_str(),
        ])
        .output();

    let _ = fs::remove_file(&script_path);

    match output {
        Ok(output) if output.status.success() => Ok(()),
        Ok(output) => Err(CommandError::new(
            singbox_service_error_code(action),
            elevated_singbox_service_failed_message(action, direct_result, output.status.code()),
        )),
        Err(error) => Err(CommandError::new(
            singbox_service_error_code(action),
            format!(
                "Не удалось запросить права администратора, чтобы {} службу Local sing-box: {error}",
                action.label()
            ),
        )),
    }
}

fn write_elevated_singbox_service_script(
    action: SingBoxServiceAction,
    service_name: &str,
    config_source: Option<&Path>,
    config_target: Option<&Path>,
) -> Result<PathBuf, CommandError> {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or(0);
    let script_path = env::temp_dir().join(format!("vpn-proxy-singbox-service-{nonce}.ps1"));
    let script = elevated_singbox_service_script(action, service_name, config_source, config_target);

    write_powershell_script(&script_path, &script).map_err(|error| {
        CommandError::new(
            singbox_service_error_code(action),
            format!(
                "Не удалось подготовить временный скрипт для управления Local sing-box '{}': {error}",
                script_path.display()
            ),
        )
    })?;

    Ok(script_path)
}

fn elevated_singbox_service_script(
    action: SingBoxServiceAction,
    service_name: &str,
    config_source: Option<&Path>,
    config_target: Option<&Path>,
) -> String {
    let action_name = action.action_name();
    let escaped_service_name = escape_powershell_single(service_name);
    let escaped_config_source = config_source
        .map(|path| escape_powershell_single(&path.display().to_string()))
        .unwrap_or_default();
    let escaped_config_target = config_target
        .map(|path| escape_powershell_single(&path.display().to_string()))
        .unwrap_or_default();

    format!(
        r#"
$ErrorActionPreference = 'SilentlyContinue'
$serviceName = '{escaped_service_name}'
$action = '{action_name}'
$configSource = '{escaped_config_source}'
$configTarget = '{escaped_config_target}'

if ($action -eq 'start') {{
  if (-not [string]::IsNullOrWhiteSpace($configSource)) {{
    if (-not (Test-Path -LiteralPath $configSource)) {{ exit 5 }}
    if (-not [string]::IsNullOrWhiteSpace($configTarget)) {{
      try {{
        Copy-Item -LiteralPath $configSource -Destination $configTarget -Force -ErrorAction Stop
      }} catch {{
        exit 6
      }}
    }}
  }}

  $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
  if ($null -eq $service) {{ exit 2 }}
  if ($service.Status -eq 'Running') {{ exit 0 }}

  Start-Service -Name $serviceName -ErrorAction SilentlyContinue
  $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
  if ($null -ne $service) {{
    try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
    if ($service.Status -eq 'Running') {{ exit 0 }}
  }}

  exit 3
}}

$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($null -eq $service) {{ exit 2 }}
if ($service.Status -eq 'Stopped') {{ exit 0 }}

Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($null -ne $service) {{
  try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(15)) }} catch {{}}
  if ($service.Status -eq 'Stopped') {{ exit 0 }}
}}

exit 4
"#
    )
}

fn install_singbox_component(storage: &JsonStorage) -> Result<ComponentStatusDto, CommandError> {
    let generated_config_path = storage.paths().generated_dir.join("sing-box-config.json");
    run_elevated_singbox_package_script(
        SingBoxPackageAction::Install,
        include_str!("../../scripts/install-singbox.ps1"),
        vec![
            "-ConfigSource".to_string(),
            generated_config_path.display().to_string(),
        ],
        &storage.paths().state_dir,
    )?;

    let refreshed = detect_singbox_install();
    let Some(detected) = refreshed.as_ref() else {
        return Err(CommandError::new(
            SingBoxPackageAction::Install.error_code(),
            "Установка Local sing-box завершилась, но приложение не найдено после проверки.",
        ));
    };

    Ok(ComponentStatusDto::from(&singbox_component_from_detection(
        Some(detected),
    )))
}

fn uninstall_singbox_component() -> Result<ComponentStatusDto, CommandError> {
    let Some(detected) = detect_singbox_install() else {
        let component = singbox_component_from_detection(None);
        return Ok(ComponentStatusDto::from(&component));
    };

    ensure_safe_singbox_install_dir(&detected.install_dir).map_err(|message| {
        CommandError::new(SingBoxPackageAction::Uninstall.error_code(), message)
    })?;
    let artifact_dir = default_config_root().join("state");
    run_elevated_singbox_package_script(
        SingBoxPackageAction::Uninstall,
        include_str!("../../scripts/install-singbox.ps1"),
        vec![
            "-InstallRoot".to_string(),
            detected.install_dir.display().to_string(),
            "-ServiceName".to_string(),
            detected.service_name,
            "-Uninstall".to_string(),
        ],
        &artifact_dir,
    )?;

    let refreshed = detect_singbox_install();
    if refreshed.is_some() {
        return Err(CommandError::new(
            SingBoxPackageAction::Uninstall.error_code(),
            "Удаление Local sing-box завершилось, но приложение все еще найдено на компьютере.",
        ));
    }

    let component = singbox_component_from_detection(None);
    Ok(ComponentStatusDto::from(&component))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SingBoxPackageAction {
    Install,
    Uninstall,
}

impl SingBoxPackageAction {
    fn error_code(self) -> &'static str {
        match self {
            SingBoxPackageAction::Install => "singbox_install_failed",
            SingBoxPackageAction::Uninstall => "singbox_uninstall_failed",
        }
    }

    fn label(self) -> &'static str {
        match self {
            SingBoxPackageAction::Install => "установить",
            SingBoxPackageAction::Uninstall => "удалить",
        }
    }

    fn file_label(self) -> &'static str {
        match self {
            SingBoxPackageAction::Install => "install",
            SingBoxPackageAction::Uninstall => "uninstall",
        }
    }
}

fn run_elevated_singbox_package_script(
    action: SingBoxPackageAction,
    installer_body: &str,
    installer_args: Vec<String>,
    artifact_dir: &Path,
) -> Result<(), CommandError> {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or(0);
    fs::create_dir_all(artifact_dir).map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось создать папку для временных файлов Local sing-box '{}': {error}",
                artifact_dir.display()
            ),
        )
    })?;

    let installer_path = artifact_dir.join(format!(
        "vpn-proxy-singbox-{}-{nonce}.ps1",
        action.file_label()
    ));
    let runner_path = artifact_dir.join(format!(
        "vpn-proxy-singbox-{}-{nonce}.runner.ps1",
        action.file_label()
    ));
    let result_path = artifact_dir.join(format!(
        "vpn-proxy-singbox-{}-{nonce}.log",
        action.file_label()
    ));

    write_powershell_script(&installer_path, installer_body).map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось подготовить установщик Local sing-box '{}': {error}",
                installer_path.display()
            ),
        )
    })?;
    write_powershell_script(
        &runner_path,
        &singbox_installer_runner_script(&installer_path, &result_path, &installer_args),
    )
    .map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось подготовить runner Local sing-box '{}': {error}",
                runner_path.display()
            ),
        )
    })?;

    let launch_script = format!(
        r#"
$ErrorActionPreference = 'Stop'
$resultPath = '{}'
try {{
  $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
  if ($null -eq $p) {{
    Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
    exit 1
  }}
  exit $p.ExitCode
}} catch {{
  Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
  exit 1
}}
"#,
        escape_powershell_single(&result_path.display().to_string()),
        escape_powershell_single(&runner_path.display().to_string())
    );
    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            launch_script.as_str(),
        ])
        .output();

    let _ = fs::remove_file(&installer_path);
    let _ = fs::remove_file(&runner_path);

    match output {
        Ok(output) if output.status.success() => {
            let _ = fs::remove_file(&result_path);
            Ok(())
        }
        Ok(output) => {
            let details = package_failure_details(&result_path, &output);
            let _ = fs::remove_file(&result_path);
            Err(CommandError::new(
                action.error_code(),
                format!(
                    "Не удалось {} Local sing-box. Код elevated-команды: {}. {details}",
                    action.label(),
                    output.status.code().unwrap_or(-1),
                ),
            ))
        }
        Err(error) => Err(CommandError::new(
            action.error_code(),
            format!(
                "Не удалось запросить права администратора, чтобы {} Local sing-box: {error}",
                action.label()
            ),
        )),
    }
}

pub(crate) fn singbox_installer_runner_script(
    installer_path: &Path,
    result_path: &Path,
    installer_args: &[String],
) -> String {
    let args = installer_args
        .iter()
        .map(|arg| format!("'{}'", escape_powershell_single(arg)))
        .collect::<Vec<_>>()
        .join(", ");

    format!(
        r#"
$ErrorActionPreference = 'Stop'
$installerPath = '{}'
$resultPath = '{}'
$stdoutPath = "$resultPath.stdout.log"
$stderrPath = "$resultPath.stderr.log"
$installerArgs = @({args})
try {{
  $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs 2>&1
  $exitCode = $LASTEXITCODE
  Set-Content -LiteralPath $stdoutPath -Value ($output | Out-String) -Encoding UTF8
  if ($exitCode -ne 0) {{
    $stdout = if (Test-Path -LiteralPath $stdoutPath) {{ Get-Content -LiteralPath $stdoutPath -Raw }} else {{ '' }}
    $stderr = if (Test-Path -LiteralPath $stderrPath) {{ Get-Content -LiteralPath $stderrPath -Raw }} else {{ '' }}
    throw "install-singbox.ps1 завершился с кодом $exitCode. stdout: $stdout stderr: $stderr"
  }}
  Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
  exit 0
}} catch {{
  Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
  exit 1
}} finally {{
  Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
}}
"#,
        escape_powershell_single(&installer_path.display().to_string()),
        escape_powershell_single(&result_path.display().to_string())
    )
}

fn singbox_service_error_code(action: SingBoxServiceAction) -> &'static str {
    match action {
        SingBoxServiceAction::Start => "singbox_service_start_failed",
        SingBoxServiceAction::Stop => "singbox_service_stop_failed",
    }
}

fn singbox_service_script_failed_message(
    action: SingBoxServiceAction,
    exit_code: Option<i32>,
) -> String {
    let exit_code = exit_code
        .map(|code| format!(" Код выхода PowerShell: {code}."))
        .unwrap_or_default();

    format!(
        "Не удалось {} службу Local sing-box: команда управления службой не вернула корректный результат.{exit_code}",
        action.label()
    )
}

fn singbox_service_command_failed_message(
    action: SingBoxServiceAction,
    result: &SingBoxServiceCommandOutput,
) -> String {
    let service_name = result
        .service_name
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("VpnProxySingBox");
    let status = result
        .status
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("неизвестен");
    let pid = result
        .process_id
        .filter(|value| *value > 0)
        .map(|value| format!(", PID: {value}"))
        .unwrap_or_default();

    match result.code.as_str() {
        "service_not_found" => "Служба Local sing-box не найдена.".to_string(),
        "config_source_missing" => {
            "Сгенерированный конфиг Local sing-box не найден перед запуском службы.".to_string()
        }
        "config_sync_failed" => {
            "Не удалось обновить config.json службы Local sing-box перед запуском. Попробуй запустить приложение от имени администратора.".to_string()
        }
        "start_failed" => format!(
            "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
        ),
        "stop_failed" => format!(
            "Не удалось остановить службу {service_name}. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
        ),
        _ => format!(
            "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
            action.label()
        ),
    }
}

fn elevated_singbox_service_failed_message(
    action: SingBoxServiceAction,
    direct_result: &SingBoxServiceCommandOutput,
    exit_code: Option<i32>,
) -> String {
    let exit_code = exit_code
        .map(|code| format!(" Код выхода elevated PowerShell: {code}."))
        .unwrap_or_default();
    format!(
        "{} Попытка с правами администратора тоже не сработала.{exit_code}",
        singbox_service_command_failed_message(action, direct_result)
    )
}

fn components_or_defaults(storage: &JsonStorage) -> Result<Vec<ComponentStatus>, CommandError> {
    let components = storage.read_components().map_err(storage_error)?;
    Ok(resolve_component_statuses(
        components,
        detect_proxyfier_install(),
        detect_singbox_install(),
    ))
}

pub fn resolve_component_statuses(
    stored_components: Vec<ComponentStatus>,
    detected_proxyfier: Option<DetectedProxyfier>,
    detected_singbox: Option<DetectedSingBox>,
) -> Vec<ComponentStatus> {
    let mut components = default_components();

    for component in stored_components {
        upsert_component(&mut components, component);
    }

    if detected_proxyfier.is_some() {
        upsert_component(
            &mut components,
            proxyfier_component_from_detection(detected_proxyfier.as_ref()),
        );
    }
    if detected_singbox.is_some() {
        upsert_component(
            &mut components,
            singbox_component_from_detection(detected_singbox.as_ref()),
        );
    }

    components
}

fn default_components() -> Vec<ComponentStatus> {
    vec![
        ComponentStatus {
            id: ComponentId::ControlApp,
            name: "Приложение управления".to_string(),
            state: ComponentState::Running,
            installed: true,
            running: true,
            version: None,
            path: None,
            problems: Vec::new(),
            actions: vec![
                "Открыть журнал".to_string(),
                "Скопировать диагностику".to_string(),
            ],
        },
        ComponentStatus {
            id: ComponentId::Proxyfier,
            name: "ProxiFyre".to_string(),
            state: ComponentState::Missing,
            installed: false,
            running: false,
            version: None,
            path: None,
            problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()],
            actions: vec!["Установить ProxiFyre".to_string()],
        },
        ComponentStatus {
            id: ComponentId::Singbox,
            name: "Локальный sing-box".to_string(),
            state: ComponentState::Missing,
            installed: false,
            running: false,
            version: None,
            path: None,
            problems: Vec::new(),
            actions: vec!["Установить локальный sing-box".to_string()],
        },
    ]
}

fn upsert_component(components: &mut Vec<ComponentStatus>, component: ComponentStatus) {
    match components
        .iter()
        .position(|existing| existing.id == component.id)
    {
        Some(index) => components[index] = component,
        None => components.push(component),
    }
}

fn route_line(active_target: Option<&Target>) -> String {
    match active_target {
        Some(target) if target.id == "local-singbox" => {
            format!(
                "Выбранные приложения -> ProxiFyre -> локальный sing-box {}:{} -> VPN",
                target.host, target.port
            )
        }
        Some(target) => format!(
            "Выбранные приложения -> ProxiFyre -> внешний прокси {}:{}",
            target.host, target.port
        ),
        None => "Выбранные приложения -> ProxiFyre -> внешний прокси".to_string(),
    }
}

fn resolved_app(item: &ProfileItem, warnings: &mut Vec<String>) -> ResolvedAppDto {
    let mut notes = Vec::new();
    match item.item_type {
        ProfileItemType::Process => notes.push("Имя процесса используется напрямую".to_string()),
        ProfileItemType::Folder => {
            let note = "Сканирование папок отложено; ProxiFyre получает путь к папке";
            notes.push(note.to_string());
            warnings.push(note.to_string());
        }
        ProfileItemType::Exe => {
            notes.push("Путь к EXE сохраняется для сопоставления в ProxiFyre".to_string())
        }
    }

    ResolvedAppDto {
        source_type: item.item_type.clone(),
        source_value: item.value.clone(),
        app_name: item.value.clone(),
        notes,
    }
}

fn write_generated_config(path: &Path, contents: &str) -> Result<(), CommandError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(storage_error)?;
    }
    fs::write(path, contents).map_err(storage_error)
}

fn open_file_or_select(path: &Path) -> Result<(), CommandError> {
    let status = Command::new("notepad.exe")
        .arg(path)
        .spawn()
        .map_err(|error| {
            CommandError::new(
                "open_config_failed",
                format!("Не удалось открыть конфиг '{}': {error}", path.display()),
            )
        })?;

    drop(status);
    Ok(())
}

fn open_folder(path: &Path) -> Result<String, CommandError> {
    fs::create_dir_all(path).map_err(storage_error)?;
    Command::new("explorer.exe")
        .arg(path)
        .spawn()
        .map_err(|error| {
            CommandError::new(
                "open_config_failed",
                format!("Не удалось открыть папку '{}': {error}", path.display()),
            )
        })?;

    Ok(path.display().to_string())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ServiceControlAction {
    Start,
    Stop,
}

impl ServiceControlAction {
    fn error_code(self) -> &'static str {
        match self {
            ServiceControlAction::Start => "proxifyre_service_start_failed",
            ServiceControlAction::Stop => "proxifyre_service_stop_failed",
        }
    }

    fn label(self) -> &'static str {
        match self {
            ServiceControlAction::Start => "запустить",
            ServiceControlAction::Stop => "остановить",
        }
    }
}

fn control_proxifyre_service(
    action: ServiceControlAction,
) -> Result<ComponentStatusDto, CommandError> {
    let Some(detected) = detect_proxyfier_install() else {
        return Err(CommandError::new(
            "proxifyre_not_found",
            "ProxiFyre не найден на компьютере.",
        ));
    };

    run_proxifyre_service_command(action, &service_name_candidates(&detected))?;
    let refreshed = detect_proxyfier_install();
    let component = proxyfier_component_from_detection(refreshed.as_ref());

    Ok(ComponentStatusDto::from(&component))
}

fn service_name_candidates(detected: &DetectedProxyfier) -> Vec<String> {
    let mut names = Vec::new();

    if let Some(service_name) = &detected.service_name {
        names.push(service_name.clone());
    }
    for service_name in ["ProxiFyreService", "ProxiFyre"] {
        if !names
            .iter()
            .any(|existing| existing.eq_ignore_ascii_case(service_name))
        {
            names.push(service_name.to_string());
        }
    }

    names
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceCommandOutput {
    success: bool,
    code: String,
    service_name: Option<String>,
    status: Option<String>,
    process_id: Option<u32>,
}

fn run_proxifyre_service_command(
    action: ServiceControlAction,
    service_names: &[String],
) -> Result<(), CommandError> {
    let names = service_names
        .iter()
        .map(|name| format!("'{}'", escape_powershell_single(name)))
        .collect::<Vec<_>>()
        .join(", ");
    let action_name = match action {
        ServiceControlAction::Start => "start",
        ServiceControlAction::Stop => "stop",
    };
    let script = format!(
        r#"
$ErrorActionPreference = 'Stop'
$names = @({names})
$action = '{action_name}'
$service = $null

function Find-ProxiFyreService {{
  foreach ($name in $names) {{
    $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
    if ($null -ne $candidate) {{ return $candidate }}
  }}

  return Get-Service |
    Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
    Select-Object -First 1
}}

function Get-ServiceProcessId([string]$name) {{
  $escapedName = $name.Replace("'", "''")
  $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
  if ($null -eq $record) {{ return 0 }}
  return [int]$record.ProcessId
}}

function Get-ServiceStatus([string]$name) {{
  $current = Get-Service -Name $name -ErrorAction SilentlyContinue
  if ($null -eq $current) {{ return $null }}
  return $current.Status.ToString()
}}

function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{
  [PSCustomObject]@{{
    success = $success
    code = $code
    serviceName = if ($null -ne $service) {{ $service.Name }} else {{ $null }}
    status = $status
    processId = $processId
  }} | ConvertTo-Json -Compress
  exit 0
}}

$service = Find-ProxiFyreService
if ($null -eq $service) {{
  Write-ServiceResult $false 'service_not_found' $null 0
}}

$status = $service.Status.ToString()
$processId = Get-ServiceProcessId $service.Name

if ($action -eq 'start') {{
  if ($status -eq 'Running') {{
    Write-ServiceResult $true 'already_running' $status $processId
  }}

  try {{
    Start-Service -Name $service.Name -ErrorAction Stop
    $service = Get-Service -Name $service.Name
    $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15))
  }} catch {{
    Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $service.Name) (Get-ServiceProcessId $service.Name)
  }}

  Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $service.Name)
}}

if ($status -eq 'Stopped') {{
  Write-ServiceResult $true 'already_stopped' $status $processId
}}

try {{
  if ($service.CanStop) {{
    Stop-Service -Name $service.Name -Force -ErrorAction Stop
  }}
}} catch {{}}

try {{
  $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
  if ($null -ne $service -and $service.Status -ne 'Stopped') {{
    $null = & sc.exe stop $service.Name 2>$null
  }}
}} catch {{}}

try {{
  $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
  if ($null -ne $service -and $service.Status -ne 'Stopped') {{
    $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
  }}
}} catch {{}}

$status = Get-ServiceStatus $service.Name
$processId = Get-ServiceProcessId $service.Name
if ($status -ne 'Stopped' -and $processId -gt 0) {{
  try {{
    $null = & taskkill.exe /PID $processId /F 2>$null
    Start-Sleep -Milliseconds 700
    $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
    if ($null -ne $service) {{
      $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
    }}
  }} catch {{}}
}}

$status = Get-ServiceStatus $service.Name
$processId = Get-ServiceProcessId $service.Name
if ($status -eq 'Stopped') {{
  Write-ServiceResult $true 'stopped' $status $processId
}}

Write-ServiceResult $false 'stop_failed' $status $processId
"#
    );

    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            script.as_str(),
        ])
        .output()
        .map_err(|error| {
            CommandError::new(
                action.error_code(),
                format!("Не удалось {} службу ProxiFyre: {error}", action.label()),
            )
        })?;

    let result = parse_service_command_output(&output.stdout).ok_or_else(|| {
        CommandError::new(
            action.error_code(),
            service_script_failed_message(action, output.status.code()),
        )
    })?;

    if result.success {
        return Ok(());
    }

    if matches!(result.code.as_str(), "start_failed" | "stop_failed") {
        run_elevated_proxifyre_service_command(action, service_names, &result)?;
        return Ok(());
    }

    Err(CommandError::new(
        action.error_code(),
        service_command_failed_message(action, &result),
    ))
}

fn run_elevated_proxifyre_service_command(
    action: ServiceControlAction,
    service_names: &[String],
    direct_result: &ServiceCommandOutput,
) -> Result<(), CommandError> {
    let script_path = write_elevated_service_script(action, service_names)?;
    let launch_script = format!(
        "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
        escape_powershell_single(&script_path.display().to_string())
    );
    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            launch_script.as_str(),
        ])
        .output();

    let _ = fs::remove_file(&script_path);

    match output {
        Ok(output) if output.status.success() => Ok(()),
        Ok(output) => Err(CommandError::new(
            action.error_code(),
            elevated_service_failed_message(action, direct_result, output.status.code()),
        )),
        Err(error) => Err(CommandError::new(
            action.error_code(),
            format!(
                "Не удалось запросить права администратора, чтобы {} службу ProxiFyre: {error}",
                action.label()
            ),
        )),
    }
}

fn write_elevated_service_script(
    action: ServiceControlAction,
    service_names: &[String],
) -> Result<PathBuf, CommandError> {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or(0);
    let script_path = env::temp_dir().join(format!("vpn-proxy-proxifyre-service-{nonce}.ps1"));
    let script = elevated_service_script(action, service_names);

    write_powershell_script(&script_path, &script).map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось подготовить временный скрипт для управления ProxiFyre '{}': {error}",
                script_path.display()
            ),
        )
    })?;

    Ok(script_path)
}

fn elevated_service_script(action: ServiceControlAction, service_names: &[String]) -> String {
    let names = service_names
        .iter()
        .map(|name| format!("'{}'", escape_powershell_single(name)))
        .collect::<Vec<_>>()
        .join(", ");
    let action_name = match action {
        ServiceControlAction::Start => "start",
        ServiceControlAction::Stop => "stop",
    };

    format!(
        r#"
$ErrorActionPreference = 'SilentlyContinue'
$names = @({names})
$action = '{action_name}'
$service = $null

foreach ($name in $names) {{
  $service = Get-Service -Name $name -ErrorAction SilentlyContinue
  if ($null -ne $service) {{ break }}
}}

if ($null -eq $service) {{
  $service = Get-Service |
    Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
    Select-Object -First 1
}}

if ($null -eq $service) {{ exit 2 }}

function Get-ServiceProcessId([string]$name) {{
  $escapedName = $name.Replace("'", "''")
  $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
  if ($null -eq $record) {{ return 0 }}
  return [int]$record.ProcessId
}}

if ($action -eq 'start') {{
  if ($service.Status -eq 'Running') {{ exit 0 }}
  Start-Service -Name $service.Name -ErrorAction SilentlyContinue
  $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
  if ($null -ne $service) {{
    try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
    if ($service.Status -eq 'Running') {{ exit 0 }}
  }}
  exit 3
}}

if ($service.Status -eq 'Stopped') {{ exit 0 }}

if ($service.CanStop) {{
  Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
}}

$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
  $null = & sc.exe stop $service.Name 2>$null
}}

$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
  try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
}}

$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service -and $service.Status -ne 'Stopped') {{
  $processId = Get-ServiceProcessId $service.Name
  if ($processId -gt 0) {{
    $null = & taskkill.exe /PID $processId /F 2>$null
    Start-Sleep -Milliseconds 700
    $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
    if ($null -ne $service) {{
      try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
    }}
  }}
}}

$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -eq $service -or $service.Status -eq 'Stopped') {{ exit 0 }}
exit 4
"#
    )
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProxiFyrePackageAction {
    Install,
    Uninstall,
}

impl ProxiFyrePackageAction {
    fn error_code(self) -> &'static str {
        match self {
            ProxiFyrePackageAction::Install => "proxifyre_install_failed",
            ProxiFyrePackageAction::Uninstall => "proxifyre_uninstall_failed",
        }
    }

    fn label(self) -> &'static str {
        match self {
            ProxiFyrePackageAction::Install => "установить",
            ProxiFyrePackageAction::Uninstall => "удалить",
        }
    }

    fn file_label(self) -> &'static str {
        match self {
            ProxiFyrePackageAction::Install => "install",
            ProxiFyrePackageAction::Uninstall => "uninstall",
        }
    }
}

fn install_proxifyre_component(storage: &JsonStorage) -> Result<ComponentStatusDto, CommandError> {
    let generated_config_path = storage
        .paths()
        .generated_dir
        .join("proxifyre-app-config.json");
    let script = install_proxifyre_script(&generated_config_path);

    run_elevated_package_script(
        ProxiFyrePackageAction::Install,
        script,
        &storage.paths().state_dir,
    )?;

    let refreshed = detect_proxyfier_install();
    let Some(detected) = refreshed.as_ref() else {
        return Err(CommandError::new(
            ProxiFyrePackageAction::Install.error_code(),
            "Установка ProxiFyre завершилась, но приложение не найдено после проверки.",
        ));
    };

    Ok(ComponentStatusDto::from(
        &proxyfier_component_from_detection(Some(detected)),
    ))
}

fn uninstall_proxifyre_component() -> Result<ComponentStatusDto, CommandError> {
    let Some(detected) = detect_proxyfier_install() else {
        let component = proxyfier_component_from_detection(None);
        return Ok(ComponentStatusDto::from(&component));
    };

    ensure_safe_proxifyre_install_dir(&detected.install_dir)?;
    let script = uninstall_proxifyre_script(&detected.install_dir, &detected.executable_path);

    let artifact_dir = default_config_root().join("state");
    run_elevated_package_script(ProxiFyrePackageAction::Uninstall, script, &artifact_dir)?;

    let refreshed = detect_proxyfier_install();
    if refreshed.is_some() {
        return Err(CommandError::new(
            ProxiFyrePackageAction::Uninstall.error_code(),
            "Удаление ProxiFyre завершилось, но приложение все еще найдено на компьютере.",
        ));
    }

    let component = proxyfier_component_from_detection(None);
    Ok(ComponentStatusDto::from(&component))
}

fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto {
    let vc_runtime = detect_vc_runtime();
    let packet_filter = detect_windows_packet_filter();
    let proxifyre = detect_proxyfier_install();

    let vc_runtime_item = setup_item_from_program(
        "vc-runtime",
        &format!("Microsoft Visual C++ Runtime ({})", runtime_arch_label()),
        vc_runtime,
        "Нужен для запуска ProxiFyre.exe. Установщик скачает официальный vc_redist от Microsoft.",
    );
    let packet_filter_item = setup_item_from_program(
        "packet-filter",
        "Windows Packet Filter",
        packet_filter,
        "Сетевой драйвер NT Kernel/WireSock, через который ProxiFyre перехватывает трафик приложений.",
    );
    let proxifyre_item = match proxifyre {
        Some(detected) => ProxiFyreSetupItemDto {
            id: "proxifyre".to_string(),
            name: "ProxiFyre".to_string(),
            installed: true,
            version: Some(if detected.running {
                "служба запущена".to_string()
            } else {
                "служба не запущена".to_string()
            }),
            details: detected.install_dir.display().to_string(),
        },
        None => ProxiFyreSetupItemDto {
            id: "proxifyre".to_string(),
            name: "ProxiFyre".to_string(),
            installed: false,
            version: None,
            details: format!(
                "Будет скачан с GitHub и распакован в {}.",
                PROXIFYRE_INSTALL_DIR
            ),
        },
    };

    let items = vec![vc_runtime_item, packet_filter_item, proxifyre_item];
    let missing_count = items.iter().filter(|item| !item.installed).count();

    ProxiFyreSetupStatusDto {
        ready: missing_count == 0,
        missing_count,
        items,
    }
}

fn setup_item_from_program(
    id: &str,
    name: &str,
    program: Option<InstalledProgram>,
    missing_details: &str,
) -> ProxiFyreSetupItemDto {
    match program {
        Some(program) => ProxiFyreSetupItemDto {
            id: id.to_string(),
            name: name.to_string(),
            installed: true,
            version: program.display_version,
            details: program.display_name,
        },
        None => ProxiFyreSetupItemDto {
            id: id.to_string(),
            name: name.to_string(),
            installed: false,
            version: None,
            details: missing_details.to_string(),
        },
    }
}

#[derive(Debug, Clone)]
struct InstalledProgram {
    display_name: String,
    display_version: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct InstalledProgramJson {
    display_name: Option<String>,
    display_version: Option<String>,
}

fn detect_vc_runtime() -> Option<InstalledProgram> {
    installed_program(&vc_runtime_registry_pattern())
}

fn detect_windows_packet_filter() -> Option<InstalledProgram> {
    installed_program("Windows Packet Filter|WinpkFilter|NDISAPI")
}

fn installed_program(pattern: &str) -> Option<InstalledProgram> {
    let script = format!(
        r#"
$paths = @(
  'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$program = Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
  Where-Object {{ $_.DisplayName -match '{}' }} |
  Select-Object -First 1 DisplayName, DisplayVersion
if ($null -ne $program) {{
  $program | ConvertTo-Json -Compress
}}
"#,
        escape_powershell_single(pattern)
    );

    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            script.as_str(),
        ])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let payload = stdout.trim();
    if payload.is_empty() || payload.eq_ignore_ascii_case("null") {
        return None;
    }

    let parsed: InstalledProgramJson = serde_json::from_str(payload).ok()?;
    let display_name = parsed.display_name?.trim().to_string();
    if display_name.is_empty() {
        return None;
    }

    Some(InstalledProgram {
        display_name,
        display_version: parsed
            .display_version
            .map(|version| version.trim().to_string())
            .filter(|version| !version.is_empty()),
    })
}

fn vc_runtime_registry_pattern() -> String {
    let arch = runtime_arch_label();
    if arch == "ARM64" {
        return r"Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)".to_string();
    }

    format!(r"Microsoft Visual C\+\+.*Redistributable.*\({arch}\)")
}

fn runtime_arch_label() -> &'static str {
    if cfg!(target_arch = "aarch64") {
        "ARM64"
    } else if cfg!(target_arch = "x86") {
        "x86"
    } else {
        "x64"
    }
}

fn run_elevated_package_script(
    action: ProxiFyrePackageAction,
    body: String,
    artifact_dir: &Path,
) -> Result<(), CommandError> {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or(0);
    fs::create_dir_all(artifact_dir).map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось создать папку для временных файлов ProxiFyre '{}': {error}",
                artifact_dir.display()
            ),
        )
    })?;
    let script_path = artifact_dir.join(format!(
        "vpn-proxy-proxifyre-{}-{nonce}.ps1",
        action.file_label()
    ));
    let result_path = artifact_dir.join(format!(
        "vpn-proxy-proxifyre-{}-{nonce}.log",
        action.file_label()
    ));
    let script = wrap_elevated_package_script(&body, &result_path);

    write_powershell_script(&script_path, &script).map_err(|error| {
        CommandError::new(
            action.error_code(),
            format!(
                "Не удалось подготовить временный скрипт, чтобы {} ProxiFyre '{}': {error}",
                action.label(),
                script_path.display()
            ),
        )
    })?;

    let launch_script = format!(
        r#"
$ErrorActionPreference = 'Stop'
$resultPath = '{}'
try {{
  $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
  if ($null -eq $p) {{
    Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
    exit 1
  }}
  exit $p.ExitCode
}} catch {{
  Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
  exit 1
}}
"#,
        escape_powershell_single(&result_path.display().to_string()),
        escape_powershell_single(&script_path.display().to_string())
    );
    let output = Command::new("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            launch_script.as_str(),
        ])
        .output();

    let _ = fs::remove_file(&script_path);

    match output {
        Ok(output) if output.status.success() => {
            let _ = fs::remove_file(&result_path);
            Ok(())
        }
        Ok(output) => {
            let details = package_failure_details(&result_path, &output);
            let _ = fs::remove_file(&result_path);
            Err(CommandError::new(
                action.error_code(),
                format!(
                    "Не удалось {} ProxiFyre. Код elevated-команды: {}. {details}",
                    action.label(),
                    output.status.code().unwrap_or(-1),
                ),
            ))
        }
        Err(error) => Err(CommandError::new(
            action.error_code(),
            format!(
                "Не удалось запросить права администратора, чтобы {} ProxiFyre: {error}",
                action.label()
            ),
        )),
    }
}

pub(crate) fn wrap_elevated_package_script(body: &str, result_path: &Path) -> String {
    let mut script = String::new();
    script.push_str("$ErrorActionPreference = 'Stop'\n");
    script.push_str(&format!(
        "$resultPath = '{}'\n",
        escape_powershell_single(&result_path.display().to_string())
    ));
    script.push_str("try {\n");
    script.push_str(body);
    script.push_str(
        r#"
  Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
  exit 0
} catch {
  $message = ($_ | Out-String)
  Set-Content -LiteralPath $resultPath -Value $message -Encoding UTF8
  exit 1
}
"#,
    );

    script
}

fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> {
    let mut bytes = Vec::with_capacity(script.len() + 3);
    bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]);
    bytes.extend_from_slice(script.as_bytes());
    fs::write(path, bytes)
}

pub(crate) fn install_proxifyre_script(generated_config_path: &Path) -> String {
    let mut script = String::new();
    script.push_str(&format!(
        "$targetDir = '{}'\n",
        escape_powershell_single(PROXIFYRE_INSTALL_DIR)
    ));
    script.push_str(&format!(
        "$generatedConfigPath = '{}'\n",
        escape_powershell_single(&generated_config_path.display().to_string())
    ));
    script.push_str(&format!(
        "$proxifyreReleaseApi = '{}'\n",
        escape_powershell_single(PROXIFYRE_RELEASE_API_URL)
    ));
    script.push_str(&format!(
        "$ndisapiReleaseApi = '{}'\n",
        escape_powershell_single(NDISAPI_RELEASE_API_URL)
    ));
    script.push_str(&format!(
        "$vcRedistX64Url = '{}'\n",
        escape_powershell_single(VC_REDIST_X64_URL)
    ));
    script.push_str(&format!(
        "$vcRedistX86Url = '{}'\n",
        escape_powershell_single(VC_REDIST_X86_URL)
    ));
    script.push_str(
        r#"
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  function Get-NativeArchitecture {
    $processor = Get-CimInstance Win32_Processor | Select-Object -First 1
    if ($null -ne $processor -and $processor.Architecture -eq 12) { return 'ARM64' }
    if ([Environment]::Is64BitOperatingSystem) { return 'x64' }
    return 'x86'
  }

  function Invoke-Download([string]$uri, [string]$path) {
    Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
  }

  function Select-Asset($assets, [string]$pattern, [string]$label) {
    $asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1
    if ($null -eq $asset) { throw "Не найден подходящий asset для $label ($pattern)." }
    return $asset
  }

  function Verify-AssetHash([string]$path, $asset) {
    if ($asset.digest -match '^sha256:(.+)$') {
      $expected = $Matches[1].ToLowerInvariant()
      $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
      if ($actual -ne $expected) {
        throw "SHA256 не совпал для $($asset.name). Ожидалось $expected, получилось $actual."
      }
    }
  }

  function Assert-ExitCode($process, [string]$label) {
    if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
      throw "$label завершился с кодом $($process.ExitCode)."
    }
  }

  function Get-InstalledProgram([string]$pattern) {
    $paths = @(
      'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
      'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
      'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )
    return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
      Where-Object { $_.DisplayName -match $pattern } |
      Select-Object -First 1
  }

  function Test-VcRuntime([string]$arch) {
    $pattern = if ($arch -eq 'ARM64') {
      'Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)'
    } else {
      "Microsoft Visual C\+\+.*Redistributable.*\($arch\)"
    }

    return $null -ne (Get-InstalledProgram $pattern)
  }

  function Test-WindowsPacketFilter {
    return $null -ne (Get-InstalledProgram 'Windows Packet Filter|WinpkFilter|NDISAPI')
  }

  function Get-LogTail([string]$path) {
    if (-not (Test-Path -LiteralPath $path)) { return '' }
    return (Get-Content -LiteralPath $path -Tail 40 -ErrorAction SilentlyContinue) -join ' '
  }

  $arch = Get-NativeArchitecture
  $workDir = Join-Path ([IO.Path]::GetTempPath()) 'vpn-proxy-proxifyre-install'
  $extractDir = Join-Path $workDir 'proxifyre'
  Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue
  New-Item -ItemType Directory -Force -Path $workDir, $extractDir, $targetDir | Out-Null

  if (-not (Test-VcRuntime $arch)) {
    $vcRedistPath = Join-Path $workDir 'vc_redist.exe'
    $vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url }
    Invoke-Download $vcRedistUrl $vcRedistPath
    $vcProcess = Start-Process -FilePath $vcRedistPath -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru -WindowStyle Hidden
    if ($vcProcess.ExitCode -ne 0 -and $vcProcess.ExitCode -ne 3010 -and $vcProcess.ExitCode -ne 1638 -and -not (Test-VcRuntime $arch)) {
      throw "Visual C++ Runtime завершился с кодом $($vcProcess.ExitCode)."
    }
  }

  if (-not (Test-WindowsPacketFilter)) {
    $ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
    $ndisPattern = if ($arch -eq 'ARM64') { 'ARM64\.msi$' } elseif ($arch -eq 'x86') { 'x86\.msi$' } else { 'x64\.msi$' }
    $ndisAsset = Select-Asset $ndisRelease.assets $ndisPattern 'Windows Packet Filter'
    $ndisPath = Join-Path $workDir $ndisAsset.name
    $ndisLogPath = Join-Path $workDir 'windows-packet-filter-install.log'
    Invoke-Download $ndisAsset.browser_download_url $ndisPath
    Verify-AssetHash $ndisPath $ndisAsset
    $ndisProcess = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', $ndisPath, '/qn', '/norestart', '/L*v', $ndisLogPath) -Wait -PassThru -WindowStyle Hidden
    if ($ndisProcess.ExitCode -ne 0 -and $ndisProcess.ExitCode -ne 3010 -and -not (Test-WindowsPacketFilter)) {
      $ndisLogTail = Get-LogTail $ndisLogPath
      throw "Windows Packet Filter завершился с кодом $($ndisProcess.ExitCode). MSI log: $ndisLogPath $ndisLogTail"
    }
  }

  $proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
  $proxifyrePattern = if ($arch -eq 'ARM64') { 'ARM64-signed\.zip$' } elseif ($arch -eq 'x86') { 'x86-signed\.zip$' } else { 'x64-signed\.zip$' }
  $proxifyreAsset = Select-Asset $proxifyreRelease.assets $proxifyrePattern 'ProxiFyre'
  $proxifyreZipPath = Join-Path $workDir $proxifyreAsset.name
  Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath
  Verify-AssetHash $proxifyreZipPath $proxifyreAsset

  Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force
  $proxifyreExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'ProxiFyre.exe' | Select-Object -First 1
  if ($null -eq $proxifyreExe) { throw 'В архиве ProxiFyre не найден ProxiFyre.exe.' }

  Copy-Item -Path (Join-Path $proxifyreExe.Directory.FullName '*') -Destination $targetDir -Recurse -Force

  $configTarget = Join-Path $targetDir 'app-config.json'
  if (Test-Path -LiteralPath $generatedConfigPath) {
    Copy-Item -LiteralPath $generatedConfigPath -Destination $configTarget -Force
  } elseif (-not (Test-Path -LiteralPath $configTarget)) {
    $emptyConfig = '{"logLevel":"Info","bypassLan":true,"proxies":[]}'
    Set-Content -LiteralPath $configTarget -Value $emptyConfig -Encoding UTF8
  }

  Push-Location $targetDir
  try {
    & .\ProxiFyre.exe stop | Out-Null
    & .\ProxiFyre.exe uninstall | Out-Null
    & .\ProxiFyre.exe install
    if ($LASTEXITCODE -ne 0) { throw "ProxiFyre.exe install завершился с кодом $LASTEXITCODE." }
    & .\ProxiFyre.exe start
    if ($LASTEXITCODE -ne 0) {
      Start-Service -Name 'ProxiFyreService' -ErrorAction Stop
    }
  } finally {
    Pop-Location
  }
"#,
    );

    script
}

fn uninstall_proxifyre_script(install_dir: &Path, executable_path: &Path) -> String {
    let mut script = String::new();
    script.push_str(&format!(
        "$installDir = '{}'\n",
        escape_powershell_single(&install_dir.display().to_string())
    ));
    script.push_str(&format!(
        "$exePath = '{}'\n",
        escape_powershell_single(&executable_path.display().to_string())
    ));
    script.push_str(
        r#"
  function Find-ProxiFyreService {
    foreach ($name in @('ProxiFyreService', 'ProxiFyre')) {
      $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
      if ($null -ne $candidate) { return $candidate }
    }

    return Get-Service |
      Where-Object { $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' } |
      Select-Object -First 1
  }

  function Get-ServiceProcessId([string]$name) {
    $escapedName = $name.Replace("'", "''")
    $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
    if ($null -eq $record) { return 0 }
    return [int]$record.ProcessId
  }

  $service = Find-ProxiFyreService
  if ($null -ne $service -and $service.Status -ne 'Stopped') {
    try {
      if ($service.CanStop) { Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue }
      $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
      if ($null -ne $service) { $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }
    } catch {}
  }

  $service = Find-ProxiFyreService
  if ($null -ne $service -and $service.Status -ne 'Stopped') {
    $processId = Get-ServiceProcessId $service.Name
    if ($processId -gt 0) {
      taskkill.exe /PID $processId /F | Out-Null
      Start-Sleep -Milliseconds 700
    }
  }

  if (Test-Path -LiteralPath $exePath) {
    Push-Location (Split-Path -Parent $exePath)
    try {
      & $exePath uninstall | Out-Null
    } finally {
      Pop-Location
    }
  }

  $service = Find-ProxiFyreService
  if ($null -ne $service) {
    sc.exe delete $service.Name | Out-Null
  }

  Get-Process -Name 'ProxiFyre' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue

  if (Test-Path -LiteralPath $installDir) {
    Remove-Item -LiteralPath $installDir -Recurse -Force
  }
"#,
    );

    script
}

fn ensure_safe_proxifyre_install_dir(path: &Path) -> Result<(), CommandError> {
    let name = path
        .file_name()
        .and_then(|value| value.to_str())
        .map(|value| value.to_ascii_lowercase())
        .unwrap_or_default();

    if path.parent().is_some() && name.contains("proxifyre") {
        return Ok(());
    }

    Err(CommandError::new(
        ProxiFyrePackageAction::Uninstall.error_code(),
        format!(
            "Отказываюсь рекурсивно удалять папку ProxiFyre с небезопасным путем: {}",
            path.display()
        ),
    ))
}

fn apply_to_detected_proxyfier(
    request: HelperApplyRequest<'_>,
    detected: &DetectedProxyfier,
) -> Result<HelperApplyResult, CommandError> {
    let Some(config_path) = &detected.config_path else {
        return staged_apply_result(request);
    };

    if let Some(parent) = config_path.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            CommandError::new(
                "proxyfier_apply_failed",
                format!(
                    "Не удалось создать папку конфига ProxiFyre '{}': {error}",
                    parent.display()
                ),
            )
        })?;
    }

    if config_path.exists() {
        let backup_path = config_path.with_file_name(format!(
            "{}.bak",
            config_path
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or("app-config.json")
        ));
        fs::copy(config_path, backup_path).map_err(|error| {
            CommandError::new(
                "proxyfier_apply_failed",
                format!(
                    "Не удалось создать backup текущего конфига ProxiFyre '{}': {error}",
                    config_path.display()
                ),
            )
        })?;
    }

    fs::write(config_path, request.config_contents).map_err(|error| {
        CommandError::new(
            "proxyfier_apply_failed",
            format!(
                "Не удалось записать конфиг ProxiFyre '{}': {error}",
                config_path.display()
            ),
        )
    })?;

    Ok(HelperApplyResult {
        success: true,
        changed: true,
        action: "proxifyre.apply-detected-config".to_string(),
        message: format!(
            "Сгенерированный конфиг записан в найденную установку ProxiFyre: {}",
            config_path.display()
        ),
    })
}

fn staged_apply_result(request: HelperApplyRequest<'_>) -> Result<HelperApplyResult, CommandError> {
    Ok(HelperApplyResult {
        success: true,
        changed: true,
        action: format!("{}.stage-generated-config", request.adapter_id),
        message: format!(
            "Сгенерированный конфиг подготовлен в {}; совместимая установка ProxiFyre не найдена",
            request.config_path.display()
        ),
    })
}

fn activity_for_apply(
    clock: &impl Clock,
    generated: &ProxyRouterGeneratedConfig,
    generated_path: &Path,
    helper_result: &HelperApplyResult,
) -> ActivityEntry {
    let level = if helper_result.success {
        ActivityLevel::Success
    } else {
        ActivityLevel::Error
    };

    ActivityEntry {
        id: format!("apply-{}", generated.adapter_id),
        at: clock.now(),
        level,
        title: "Конфиг ProxiFyre создан".to_string(),
        message: format!(
            "Профилей: {}, приложений: {}, конфиг: {}",
            generated.enabled_profiles,
            generated.routed_apps,
            generated_path.display()
        ),
    }
}

fn activity_for_apply_error(clock: &impl Clock, error: &CommandError) -> ActivityEntry {
    ActivityEntry {
        id: format!("apply-error-{}", error.code),
        at: clock.now(),
        level: ActivityLevel::Error,
        title: "Применение ProxiFyre заблокировано".to_string(),
        message: error.message.clone(),
    }
}

fn storage_error(error: std::io::Error) -> CommandError {
    CommandError::new("storage_error", error.to_string())
}

fn background_task_error(error: impl std::fmt::Display) -> CommandError {
    CommandError::new(
        "background_task_failed",
        format!("Фоновая проверка не завершилась: {error}"),
    )
}

fn package_failure_details(result_path: &Path, output: &Output) -> String {
    let mut parts = Vec::new();

    if let Ok(contents) = fs::read_to_string(result_path) {
        let details = compact_error_text(&contents);
        if !details.is_empty() && !details.eq_ignore_ascii_case("ok") {
            parts.push(details);
        }
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stdout = compact_error_text(&stdout);
    if !stdout.is_empty() {
        parts.push(format!("stdout: {stdout}"));
    }

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stderr = compact_error_text(&stderr);
    if !stderr.is_empty() {
        parts.push(format!("stderr: {stderr}"));
    }

    if parts.is_empty() {
        parts.push(
            "Лог elevated-скрипта не создан. Обычно это значит, что окно UAC было отменено или Windows не дала запустить elevated PowerShell."
                .to_string(),
        );
    }

    parts.join(" ")
}

fn compact_error_text(value: &str) -> String {
    let text = value
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .collect::<Vec<_>>()
        .join(" ");

    let max_chars = 1400;
    if text.chars().count() <= max_chars {
        return text;
    }

    let truncated = text.chars().take(max_chars).collect::<String>();
    format!("{truncated}...")
}

fn parse_service_command_output(stdout: &[u8]) -> Option<ServiceCommandOutput> {
    let stdout = String::from_utf8_lossy(stdout);
    let payload = stdout
        .lines()
        .rev()
        .map(str::trim)
        .find(|line| line.starts_with('{') && line.ends_with('}'))?;

    serde_json::from_str(payload).ok()
}

fn service_script_failed_message(action: ServiceControlAction, exit_code: Option<i32>) -> String {
    let exit_code = exit_code
        .map(|code| format!(" Код выхода PowerShell: {code}."))
        .unwrap_or_default();

    format!(
        "Не удалось {} службу ProxiFyre: команда управления службой не вернула корректный результат.{exit_code}",
        action.label()
    )
}

fn service_command_failed_message(
    action: ServiceControlAction,
    result: &ServiceCommandOutput,
) -> String {
    let service_name = result
        .service_name
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("ProxiFyre");
    let status = result
        .status
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("неизвестен");
    let pid = result
        .process_id
        .filter(|value| *value > 0)
        .map(|value| format!(", PID: {value}"))
        .unwrap_or_default();

    match result.code.as_str() {
        "service_not_found" => "Служба ProxiFyre не найдена.".to_string(),
        "start_failed" => format!(
            "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
        ),
        "stop_failed" => format!(
            "Не удалось остановить службу {service_name} даже после принудительной попытки. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
        ),
        _ => format!(
            "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
            action.label()
        ),
    }
}

fn elevated_service_failed_message(
    action: ServiceControlAction,
    direct_result: &ServiceCommandOutput,
    exit_code: Option<i32>,
) -> String {
    let service_name = direct_result
        .service_name
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("ProxiFyre");
    let status = direct_result
        .status
        .as_deref()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("неизвестен");
    let pid = direct_result
        .process_id
        .filter(|value| *value > 0)
        .map(|value| format!(", PID: {value}"))
        .unwrap_or_default();
    let exit_code = exit_code
        .map(|code| format!(" Код elevated-команды: {code}."))
        .unwrap_or_default();

    format!(
        "Не удалось {} службу {service_name} даже после запроса прав администратора. До запроса UAC статус был: {status}{pid}.{exit_code} Если появлялось окно UAC, проверь, что оно было подтверждено.",
        action.label()
    )
}

fn escape_powershell_single(value: &str) -> String {
    value.replace('\'', "''")
}

fn validation_error(errors: Vec<ValidationError>) -> CommandError {
    CommandError::with_details(
        "validation_error",
        "Проверка введенных данных не прошла",
        errors
            .into_iter()
            .map(|error| ValidationIssue {
                field: error.field,
                message: error.message,
            })
            .collect(),
    )
}

fn adapter_error(error: ProxyRouterError) -> CommandError {
    let code = match error.kind {
        ProxyRouterErrorKind::EmptyProfileItems => "empty_profile_items",
        ProxyRouterErrorKind::MissingTarget => "missing_target",
        ProxyRouterErrorKind::MissingRequiredComponent => "missing_required_component",
        ProxyRouterErrorKind::RequiredComponentNotRunning => "required_component_not_running",
        ProxyRouterErrorKind::UnsupportedTargetProtocol => "unsupported_target_protocol",
        ProxyRouterErrorKind::Serialization => "serialization_error",
    };

    CommandError::new(code, error.message)
}

impl From<ProfileInputDto> for ProfileInput {
    fn from(input: ProfileInputDto) -> Self {
        Self {
            id: input.id,
            name: input.name,
            enabled: input.enabled.unwrap_or(true),
            target_id: input
                .target_id
                .unwrap_or_else(|| "local-singbox".to_string()),
            protocols: input
                .protocols
                .unwrap_or_else(|| vec!["TCP".to_string(), "UDP".to_string()]),
            items: input
                .items
                .unwrap_or_default()
                .into_iter()
                .map(ProfileItemInput::from)
                .collect(),
        }
    }
}

impl From<ProfileItemInputDto> for ProfileItemInput {
    fn from(input: ProfileItemInputDto) -> Self {
        Self {
            item_type: input.item_type,
            value: input.value,
            recursive: input.recursive,
        }
    }
}

impl From<TargetInputDto> for TargetInput {
    fn from(input: TargetInputDto) -> Self {
        Self {
            id: input.id,
            name: input.name,
            kind: input.kind.unwrap_or_else(|| "external".to_string()),
            protocol: input.protocol.unwrap_or_else(|| "socks5".to_string()),
            host: input.host,
            port: input.port,
            requires_component: input.requires_component,
        }
    }
}

impl From<&Profile> for ProfileDto {
    fn from(profile: &Profile) -> Self {
        Self {
            id: profile.id.clone(),
            name: profile.name.clone(),
            enabled: profile.enabled,
            target_id: profile.target_id.clone(),
            protocols: profile.protocols.clone(),
            items: profile.items.iter().map(ProfileItemDto::from).collect(),
        }
    }
}

impl From<&ProfileItem> for ProfileItemDto {
    fn from(item: &ProfileItem) -> Self {
        Self {
            item_type: item.item_type.clone(),
            value: item.value.clone(),
            recursive: item.recursive,
        }
    }
}

impl From<&Target> for TargetDto {
    fn from(target: &Target) -> Self {
        Self {
            id: target.id.clone(),
            name: target.name.clone(),
            kind: target.kind.clone(),
            protocol: target.protocol.clone(),
            host: target.host.clone(),
            port: target.port,
            requires_component: target.requires_component.clone(),
        }
    }
}

impl From<&ComponentStatus> for ComponentStatusDto {
    fn from(component: &ComponentStatus) -> Self {
        Self {
            id: component.id.clone(),
            name: component.name.clone(),
            state: component.state.clone(),
            installed: component.installed,
            running: component.running,
            version: component.version.clone(),
            path: component.path.clone(),
            problems: component.problems.clone(),
            actions: component.actions.clone(),
        }
    }
}

impl From<&ActivityEntry> for ActivityEntryDto {
    fn from(entry: &ActivityEntry) -> Self {
        Self {
            id: entry.id.clone(),
            at: entry.at.clone(),
            level: entry.level.clone(),
            title: entry.title.clone(),
            message: entry.message.clone(),
        }
    }
}

impl From<&LocalSingBoxConfig> for LocalSingBoxConfigDto {
    fn from(config: &LocalSingBoxConfig) -> Self {
        Self {
            subscription_display_url: config.subscription_display_url(),
            has_subscription: config
                .subscription_url
                .as_deref()
                .is_some_and(|value| !value.trim().is_empty()),
            selected_server_tag: config.selected_server_tag.clone(),
            listen_host: config.listen_host.clone(),
            listen_port: config.listen_port,
            service_name: config.service_name.clone(),
            install_root: config.install_root.clone(),
            updated_at: config.updated_at.clone(),
        }
    }
}

impl From<&SubscriptionCache> for SubscriptionCacheDto {
    fn from(cache: &SubscriptionCache) -> Self {
        Self {
            servers: cache
                .servers
                .iter()
                .map(SubscriptionServerDto::from)
                .collect(),
            user_info: cache.user_info.clone(),
            fetched_at: cache.fetched_at.clone(),
        }
    }
}

impl From<&SubscriptionServer> for SubscriptionServerDto {
    fn from(server: &SubscriptionServer) -> Self {
        Self {
            tag: server.tag.clone(),
            server_type: server.server_type.clone(),
            server: server.server.clone(),
            server_port: server.server_port,
        }
    }
}
