use crate::models::{
    ComponentId, ComponentState, ComponentStatus, DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT,
    DEFAULT_LOCAL_SINGBOX_SERVICE_NAME,
};
use crate::process::command_no_window;
use serde::Deserialize;
use std::{
    env,
    path::{Path, PathBuf},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyfierEngine {
    ProxiFyre,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedProxyfier {
    pub engine: ProxyfierEngine,
    pub name: String,
    pub install_dir: PathBuf,
    pub executable_path: PathBuf,
    pub config_path: Option<PathBuf>,
    pub running: bool,
    pub service_name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedSingBox {
    pub install_dir: PathBuf,
    pub executable_path: PathBuf,
    pub wrapper_path: PathBuf,
    pub binary_exists: bool,
    pub wrapper_exists: bool,
    pub running: bool,
    pub service_name: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistryInstallEntry {
    pub display_name: String,
    pub install_location: Option<PathBuf>,
    pub display_icon: Option<PathBuf>,
}

pub trait ProxyfierDetectionHost {
    fn env_var(&self, name: &str) -> Option<String>;

    fn path_exists(&self, path: &Path) -> bool;

    fn process_running(&self, process_name: &str) -> bool;

    fn service_running(&self, service_name: &str) -> bool;

    fn registry_install_entries(&self) -> Vec<RegistryInstallEntry>;
}

#[derive(Debug, Clone, Copy, Default)]
pub struct SystemProxyfierDetectionHost;

impl ProxyfierDetectionHost for SystemProxyfierDetectionHost {
    fn env_var(&self, name: &str) -> Option<String> {
        env::var(name).ok().filter(|value| !value.trim().is_empty())
    }

    fn path_exists(&self, path: &Path) -> bool {
        path.exists()
    }

    fn process_running(&self, process_name: &str) -> bool {
        let process_name = process_name.trim_end_matches(".exe");
        let script = format!(
            "if (Get-Process -Name '{}' -ErrorAction SilentlyContinue) {{ 'true' }} else {{ 'false' }}",
            escape_powershell_single(process_name)
        );

        powershell_bool(&script)
    }

    fn service_running(&self, service_name: &str) -> bool {
        let script = format!(
            "$s = Get-Service -Name '{}' -ErrorAction SilentlyContinue; if ($s -and $s.Status -eq 'Running') {{ 'true' }} else {{ 'false' }}",
            escape_powershell_single(service_name)
        );

        powershell_bool(&script)
    }

    fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
        read_registry_install_entries()
    }
}

pub fn detect_proxyfier_install() -> Option<DetectedProxyfier> {
    detect_proxyfier_install_with_host(&SystemProxyfierDetectionHost)
}

pub fn detect_proxyfier_install_with_host(
    host: &impl ProxyfierDetectionHost,
) -> Option<DetectedProxyfier> {
    let proxifyre_running = host.process_running("ProxiFyre.exe")
        || host.service_running("ProxiFyreService")
        || host.service_running("ProxiFyre");

    proxyfier_candidates(host)
        .into_iter()
        .filter_map(|candidate| candidate.into_detected(host, proxifyre_running))
        .next()
}

pub fn proxyfier_component_from_detection(detected: Option<&DetectedProxyfier>) -> ComponentStatus {
    match detected {
        Some(proxyfier) => detected_proxyfier_component(proxyfier),
        None => missing_proxyfier_component(),
    }
}

pub fn detect_singbox_install() -> Option<DetectedSingBox> {
    detect_singbox_install_with_host(&SystemProxyfierDetectionHost)
}

pub fn detect_singbox_install_with_host(
    host: &impl ProxyfierDetectionHost,
) -> Option<DetectedSingBox> {
    let running = host.process_running("sing-box.exe")
        || host.service_running(DEFAULT_LOCAL_SINGBOX_SERVICE_NAME);

    singbox_candidates(host)
        .into_iter()
        .filter_map(|install_dir| detected_singbox_from_dir(host, install_dir, running))
        .next()
}

pub fn singbox_component_from_detection(detected: Option<&DetectedSingBox>) -> ComponentStatus {
    match detected {
        Some(singbox) => detected_singbox_component(singbox),
        None => missing_singbox_component(),
    }
}

fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatus {
    let state = if proxyfier.running {
        ComponentState::Running
    } else {
        ComponentState::Installed
    };
    let actions = match proxyfier.engine {
        ProxyfierEngine::ProxiFyre => {
            if proxyfier.running {
                vec![
                    "Применить сгенерированный конфиг".to_string(),
                    "Открыть папку конфига".to_string(),
                    "Остановить".to_string(),
                ]
            } else {
                vec![
                    "Применить сгенерированный конфиг".to_string(),
                    "Открыть папку конфига".to_string(),
                    "Запустить".to_string(),
                ]
            }
        }
    };

    ComponentStatus {
        id: ComponentId::Proxyfier,
        name: "ProxiFyre".to_string(),
        state,
        installed: true,
        running: proxyfier.running,
        version: Some(match proxyfier.engine {
            ProxyfierEngine::ProxiFyre => "ProxiFyre найден".to_string(),
        }),
        path: Some(proxyfier.install_dir.display().to_string()),
        problems: Vec::new(),
        actions,
    }
}

fn missing_proxyfier_component() -> ComponentStatus {
    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()],
    }
}

fn detected_singbox_component(singbox: &DetectedSingBox) -> ComponentStatus {
    let state = if singbox.running {
        ComponentState::Running
    } else {
        ComponentState::Stopped
    };
    let actions = if singbox.running {
        vec![
            "Сгенерировать конфиг".to_string(),
            "Остановить".to_string(),
            "Открыть папку".to_string(),
        ]
    } else {
        vec![
            "Сгенерировать конфиг".to_string(),
            "Запустить".to_string(),
            "Открыть папку".to_string(),
        ]
    };
    let problems = if singbox.running {
        Vec::new()
    } else {
        vec!["Служба Local sing-box остановлена".to_string()]
    };

    ComponentStatus {
        id: ComponentId::Singbox,
        name: "Local sing-box".to_string(),
        state,
        installed: true,
        running: singbox.running,
        version: Some("sing-box найден".to_string()),
        path: Some(singbox.executable_path.display().to_string()),
        problems,
        actions,
    }
}

fn missing_singbox_component() -> ComponentStatus {
    ComponentStatus {
        id: ComponentId::Singbox,
        name: "Local sing-box".to_string(),
        state: ComponentState::Missing,
        installed: false,
        running: false,
        version: None,
        path: None,
        problems: Vec::new(),
        actions: vec!["Установить Local sing-box".to_string()],
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ProxyfierCandidate {
    engine: ProxyfierEngine,
    name: String,
    install_dir: PathBuf,
}

impl ProxyfierCandidate {
    fn into_detected(
        self,
        host: &impl ProxyfierDetectionHost,
        proxifyre_running: bool,
    ) -> Option<DetectedProxyfier> {
        let executable_path = self.install_dir.join(executable_name(&self.engine));
        let config_path = config_path(&self.engine, &self.install_dir);
        let exists = host.path_exists(&self.install_dir)
            || host.path_exists(&executable_path)
            || config_path
                .as_ref()
                .is_some_and(|path| host.path_exists(path));

        if !exists {
            return None;
        }

        Some(DetectedProxyfier {
            service_name: service_name(&self.engine).map(str::to_string),
            engine: self.engine,
            name: self.name,
            install_dir: self.install_dir,
            executable_path,
            config_path,
            running: proxifyre_running,
        })
    }
}

fn proxyfier_candidates(host: &impl ProxyfierDetectionHost) -> Vec<ProxyfierCandidate> {
    let mut candidates = Vec::new();

    push_env_candidate(
        &mut candidates,
        host,
        ProxyfierEngine::ProxiFyre,
        "ProxiFyre",
        "PROXYWARDEN_PROXIFYRE_ROOT",
    );
    for entry in host.registry_install_entries() {
        if let Some(engine) = engine_from_name(&entry.display_name) {
            let install_dir = entry
                .install_location
                .or_else(|| entry.display_icon.and_then(|path| executable_parent(&path)));
            if let Some(install_dir) = install_dir {
                push_candidate(
                    &mut candidates,
                    ProxyfierCandidate {
                        name: entry.display_name,
                        engine,
                        install_dir,
                    },
                );
            }
        }
    }

    for install_dir in common_install_dirs(host, "ProxiFyre") {
        push_candidate(
            &mut candidates,
            ProxyfierCandidate {
                engine: ProxyfierEngine::ProxiFyre,
                name: "ProxiFyre".to_string(),
                install_dir,
            },
        );
    }
    candidates
}

fn push_env_candidate(
    candidates: &mut Vec<ProxyfierCandidate>,
    host: &impl ProxyfierDetectionHost,
    engine: ProxyfierEngine,
    name: &str,
    env_name: &str,
) {
    if let Some(path) = host.env_var(env_name) {
        push_candidate(
            candidates,
            ProxyfierCandidate {
                engine,
                name: name.to_string(),
                install_dir: PathBuf::from(path),
            },
        );
    }
}

fn push_candidate(candidates: &mut Vec<ProxyfierCandidate>, candidate: ProxyfierCandidate) {
    if !candidates.iter().any(|existing| {
        existing.engine == candidate.engine
            && same_path(&existing.install_dir, &candidate.install_dir)
    }) {
        candidates.push(candidate);
    }
}

fn common_install_dirs(host: &impl ProxyfierDetectionHost, folder_name: &str) -> Vec<PathBuf> {
    let mut dirs = vec![PathBuf::from(format!(r"C:\Tools\{folder_name}"))];

    for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
        if let Some(root) = host.env_var(env_name) {
            dirs.push(PathBuf::from(root).join(folder_name));
        }
    }

    dirs
}

fn singbox_candidates(host: &impl ProxyfierDetectionHost) -> Vec<PathBuf> {
    let mut candidates = Vec::new();

    if let Some(path) = host.env_var("PROXYWARDEN_SINGBOX_ROOT") {
        push_path_candidate(&mut candidates, PathBuf::from(path));
    }
    push_path_candidate(
        &mut candidates,
        PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT),
    );
    push_path_candidate(
        &mut candidates,
        PathBuf::from(r"C:\Tools\ProxyWarden\sing-box"),
    );
    for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
        if let Some(root) = host.env_var(env_name) {
            push_path_candidate(
                &mut candidates,
                PathBuf::from(&root).join("ProxyWarden").join("sing-box"),
            );
            push_path_candidate(&mut candidates, PathBuf::from(root).join("sing-box"));
        }
    }

    candidates
}

fn push_path_candidate(candidates: &mut Vec<PathBuf>, candidate: PathBuf) {
    if !candidates
        .iter()
        .any(|existing| same_path(existing, &candidate))
    {
        candidates.push(candidate);
    }
}

fn detected_singbox_from_dir(
    host: &impl ProxyfierDetectionHost,
    install_dir: PathBuf,
    running: bool,
) -> Option<DetectedSingBox> {
    let executable_path = install_dir.join("sing-box.exe");
    let wrapper_path = install_dir.join("ProxyWardenSingBox.exe");
    let binary_exists = host.path_exists(&executable_path);
    let wrapper_exists = host.path_exists(&wrapper_path);
    let exists = host.path_exists(&install_dir) || binary_exists || wrapper_exists;

    if !exists {
        return None;
    }

    Some(DetectedSingBox {
        install_dir,
        executable_path,
        wrapper_path,
        binary_exists,
        wrapper_exists,
        running,
        service_name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(),
    })
}

fn executable_name(engine: &ProxyfierEngine) -> &'static str {
    match engine {
        ProxyfierEngine::ProxiFyre => "ProxiFyre.exe",
    }
}

fn config_path(engine: &ProxyfierEngine, install_dir: &Path) -> Option<PathBuf> {
    match engine {
        ProxyfierEngine::ProxiFyre => Some(install_dir.join("app-config.json")),
    }
}

fn service_name(engine: &ProxyfierEngine) -> Option<&'static str> {
    match engine {
        ProxyfierEngine::ProxiFyre => Some("ProxiFyreService"),
    }
}

fn engine_from_name(name: &str) -> Option<ProxyfierEngine> {
    let normalized = name.to_ascii_lowercase();
    if normalized.contains("proxifyre") {
        Some(ProxyfierEngine::ProxiFyre)
    } else {
        None
    }
}

fn executable_parent(path: &Path) -> Option<PathBuf> {
    path.parent().map(Path::to_path_buf)
}

fn same_path(left: &Path, right: &Path) -> bool {
    left.to_string_lossy()
        .eq_ignore_ascii_case(&right.to_string_lossy())
}

fn powershell_bool(script: &str) -> bool {
    command_no_window("powershell")
        .args(["-NoProfile", "-NonInteractive", "-Command", script])
        .output()
        .ok()
        .and_then(|output| String::from_utf8(output.stdout).ok())
        .is_some_and(|stdout| stdout.trim().eq_ignore_ascii_case("true"))
}

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

fn read_registry_install_entries() -> Vec<RegistryInstallEntry> {
    let script = r#"
$paths = @(
  'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$items = foreach ($path in $paths) {
  Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
}
$items |
  Where-Object { $_.DisplayName -match 'ProxiFyre' } |
  Select-Object DisplayName,InstallLocation,DisplayIcon |
  ConvertTo-Json -Compress
"#;

    let Ok(output) = command_no_window("powershell")
        .args(["-NoProfile", "-NonInteractive", "-Command", script])
        .output()
    else {
        return Vec::new();
    };

    if !output.status.success() {
        return Vec::new();
    }

    let Ok(stdout) = String::from_utf8(output.stdout) else {
        return Vec::new();
    };
    let stdout = stdout.trim();
    if stdout.is_empty() {
        return Vec::new();
    }

    parse_registry_json(stdout)
}

fn parse_registry_json(json: &str) -> Vec<RegistryInstallEntry> {
    let Ok(value) = serde_json::from_str::<serde_json::Value>(json) else {
        return Vec::new();
    };

    match value {
        serde_json::Value::Array(entries) => entries
            .into_iter()
            .filter_map(registry_entry_from_value)
            .collect(),
        entry => registry_entry_from_value(entry).into_iter().collect(),
    }
}

fn registry_entry_from_value(value: serde_json::Value) -> Option<RegistryInstallEntry> {
    let parsed = serde_json::from_value::<RegistryInstallJson>(value).ok()?;
    let display_name = parsed.display_name?;
    Some(RegistryInstallEntry {
        display_name,
        install_location: parsed
            .install_location
            .filter(|value| !value.trim().is_empty())
            .map(PathBuf::from),
        display_icon: parsed
            .display_icon
            .and_then(|value| display_icon_path(&value)),
    })
}

fn display_icon_path(value: &str) -> Option<PathBuf> {
    let trimmed = value.trim().trim_matches('"');
    if trimmed.is_empty() {
        return None;
    }

    let without_icon_index = trimmed
        .split_once(',')
        .map(|(path, _)| path)
        .unwrap_or(trimmed)
        .trim()
        .trim_matches('"');

    Some(PathBuf::from(without_icon_index))
}

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