//! protection-core — geteilte DoH-Schutz-Logik für ReBreak Magic Windows.
//!
//! Wird von der Tauri-App (Setup via elevated One-Shot) UND vom
//! Tamper-Protection-Service (SYSTEM, re-apply/teardown) benutzt.
//!
//! Architektur: Windows-DoH zeigt auf ReBreaks eigenen DoH-Server
//! (dns.rebreak.org, AdGuard mit per-Device-Token im Pfad) — KEIN NextDNS.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process::Command;

/// DoH-Host des ReBreak-DNS-Servers (AdGuard, kuratierte Gambling-Blocklist
/// + User-Custom-Domains). Token wird als Pfad-Segment angehängt.
pub const DOH_HOST: &str = "dns.rebreak.org";

/// Bootstrap-Fallback falls Resolve von DOH_HOST fehlschlägt
/// (z.B. weil bereits ein kaputter DNS gesetzt ist). Hetzner-Box.
pub const DOH_FALLBACK_IP: &str = "178.105.101.137";

/// Windows-Service-Name des Tamper-Protection-Monitors.
pub const SERVICE_NAME: &str = "RebreakProtection";

/// Persistenter Zustand unter %ProgramData%\ReBreak\protection.json.
/// Wird vom elevated Setup geschrieben, vom SYSTEM-Service gelesen.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProtectionState {
    /// Per-Device DNS-Token (64-char hex) aus POST /api/magic/register.
    pub dns_token: String,
    /// Aufgelöste IPv4 des DoH-Servers zum Setup-Zeitpunkt.
    pub server_ip: String,
    /// Vollständiges DoH-Template inkl. Token-Pfad.
    pub doh_template: String,
    /// Backend-Base-URL für Status-Polling (z.B. https://staging.rebreak.org).
    pub backend_url: String,
    /// Stabile Device-ID (win-<MachineGuid>).
    pub device_id: String,
}

impl ProtectionState {
    pub fn doh_template_for(token: &str) -> String {
        format!("https://{DOH_HOST}/dns-query/{token}")
    }
}

/// %ProgramData%\ReBreak — auf non-Windows (Dev) /tmp/rebreak-protection.
pub fn state_dir() -> PathBuf {
    #[cfg(windows)]
    {
        let base = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".into());
        PathBuf::from(base).join("ReBreak")
    }
    #[cfg(not(windows))]
    {
        PathBuf::from("/tmp/rebreak-protection")
    }
}

pub fn state_path() -> PathBuf {
    state_dir().join("protection.json")
}

pub fn load_state() -> Option<ProtectionState> {
    let raw = std::fs::read_to_string(state_path()).ok()?;
    serde_json::from_str(&raw).ok()
}

// ---------------------------------------------------------------------------
// PowerShell-Runner
// ---------------------------------------------------------------------------

/// Führt ein PowerShell-Script aus (nicht-elevated, hidden window).
/// Setup/Teardown brauchen Admin — dafür generiert die App ein One-Shot-Script
/// und startet es via `Start-Process -Verb RunAs` (ein einziger UAC-Prompt).
/// Der SYSTEM-Service ruft `ps_run` direkt auf (läuft bereits elevated).
pub fn ps_run(script: &str) -> Result<String, String> {
    let mut cmd = Command::new("powershell.exe");
    cmd.args([
        "-NonInteractive",
        "-NoProfile",
        "-ExecutionPolicy",
        "Bypass",
        "-Command",
        script,
    ]);

    // Kein Konsolen-Fenster aufpoppen lassen (CREATE_NO_WINDOW)
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        cmd.creation_flags(0x0800_0000);
    }

    let out = cmd
        .output()
        .map_err(|e| format!("powershell.exe nicht startbar: {e}"))?;

    let stdout = String::from_utf8_lossy(&out.stdout).to_string();
    if out.status.success() {
        Ok(stdout)
    } else {
        let stderr = String::from_utf8_lossy(&out.stderr);
        Err(format!("PowerShell exit {:?}: {stderr}", out.status.code()))
    }
}

// ---------------------------------------------------------------------------
// Script-Generierung (pure Strings — cross-platform test- und reviewbar)
// ---------------------------------------------------------------------------

/// DoH-Setup-Script (braucht Admin). Adaptiert aus dem verifizierten
/// Windows-11-Flow: DoH-Server registrieren → Interface-DNS setzen →
/// EnableAutoDoh → per-Interface DohFlags → flushdns.
pub fn apply_script(server_ip: &str, doh_template: &str) -> String {
    format!(
        r#"$ErrorActionPreference = "Stop"
$ip = "{server_ip}"
$template = "{doh_template}"

# Schritt 1: ReBreak-DoH-Server als bekannten DoH-Endpoint registrieren
Remove-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction Ignore
Add-DnsClientDohServerAddress `
  -ServerAddress $ip `
  -DohTemplate $template `
  -AllowFallbackToUdp $False `
  -AutoUpgrade $True

# Schritt 2: DNS aller aktiven (non-loopback) Adapter auf ReBreak setzen
$adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" -and -not $_.Virtual }}
if (-not $adapters) {{ $adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" }} }}
foreach ($adapter in $adapters) {{
  Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ServerAddresses $ip

  # Schritt 4: Interface-spezifische DoH-Flags (DoH erzwingen, kein Klartext-Fallback)
  $guid = $adapter.InterfaceGuid
  $dohPath = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters\$guid\DohInterfaceSettings\Doh\$ip"
  New-Item -Path $dohPath -Force | Out-Null
  New-ItemProperty -Path $dohPath -Name "DohFlags" -Value 1 -PropertyType QWORD -Force | Out-Null
}}

# Schritt 3: DoH systemweit erzwingen
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters"
Set-ItemProperty -Path $regPath -Name "EnableAutoDoh" -Value 2 -Type DWord

# Schritt 5: Browser-Policies — Chromium (Edge/Chrome/Brave) umgehen sonst die
# System-DNS via eigenem built-in Resolver UND eigenem DoH. Beide MÜSSEN aus,
# sonst greift die DNS-Sperre im Browser nicht (empirisch verifiziert 2026-06-07).
# Test-Path-Guard statt New-Item -Force, damit bestehende Org-Policies bleiben.
function Set-ChromiumDnsPolicy($base) {{
  if (-not (Test-Path $base)) {{ New-Item -Path $base -Force | Out-Null }}
  Set-ItemProperty -Path $base -Name "DnsOverHttpsMode" -Value "off" -Type String
  Set-ItemProperty -Path $base -Name "BuiltInDnsClientEnabled" -Value 0 -Type DWord
}}
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\Microsoft\Edge"
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\Google\Chrome"
Set-ChromiumDnsPolicy "HKLM:\SOFTWARE\Policies\BraveSoftware\Brave"
# Firefox: eigenes DoH (TRR) aus + sperren (kein built-in-Resolver-Bypass)
$ff = "HKLM:\SOFTWARE\Policies\Mozilla\Firefox\DNSOverHTTPS"
if (-not (Test-Path $ff)) {{ New-Item -Path $ff -Force | Out-Null }}
Set-ItemProperty -Path $ff -Name "Enabled" -Value 0 -Type DWord
Set-ItemProperty -Path $ff -Name "Locked" -Value 1 -Type DWord

Clear-DnsClientCache
"#
    )
}

/// DoH-Teardown-Script (braucht Admin / SYSTEM). Wird NUR ausgeführt wenn
/// das Backend den Token nach abgelaufenem 24h-Cooldown revoked hat.
pub fn teardown_script(server_ip: &str) -> String {
    format!(
        r#"$ErrorActionPreference = "Continue"
$ip = "{server_ip}"

Remove-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction Ignore

$adapters = Get-NetAdapter | Where-Object {{ $_.Status -eq "Up" }}
foreach ($adapter in $adapters) {{
  Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -ResetServerAddresses
  $guid = $adapter.InterfaceGuid
  $dohPath = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters\$guid\DohInterfaceSettings"
  Remove-Item -Path $dohPath -Recurse -ErrorAction Ignore
}}

$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters"
Remove-ItemProperty -Path $regPath -Name "EnableAutoDoh" -ErrorAction Ignore

# Browser-Policies wieder entfernen
foreach ($base in @("HKLM:\SOFTWARE\Policies\Microsoft\Edge","HKLM:\SOFTWARE\Policies\Google\Chrome","HKLM:\SOFTWARE\Policies\BraveSoftware\Brave")) {{
  Remove-ItemProperty -Path $base -Name "DnsOverHttpsMode" -ErrorAction Ignore
  Remove-ItemProperty -Path $base -Name "BuiltInDnsClientEnabled" -ErrorAction Ignore
}}
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Mozilla\Firefox\DNSOverHTTPS" -Recurse -ErrorAction Ignore

Clear-DnsClientCache
"#
    )
}

/// Status-Check-Script (läuft unelevated): prüft DoH-Registrierung UND
/// dass mindestens ein aktiver Adapter ReBreak als DNS gesetzt hat.
/// Output: "APPLIED" | "MISSING"
pub fn check_script(server_ip: &str) -> String {
    format!(
        r#"$ip = "{server_ip}"
$doh = Get-DnsClientDohServerAddress -ErrorAction Ignore | Where-Object {{ $_.ServerAddress -eq $ip }}
$dns = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Ignore | Where-Object {{ $_.ServerAddresses -contains $ip }}
if ($doh -and $dns) {{ Write-Output "APPLIED" }} else {{ Write-Output "MISSING" }}
"#
    )
}

// ---------------------------------------------------------------------------
// High-Level-API (nutzt ps_run — App-Check unelevated, Service elevated)
// ---------------------------------------------------------------------------

/// Prüft ob der DoH-Schutz aktuell greift (unelevated lesbar).
pub fn doh_is_applied(server_ip: &str) -> bool {
    matches!(ps_run(&check_script(server_ip)), Ok(out) if out.trim() == "APPLIED")
}

/// Wendet den DoH-Schutz an. Braucht Admin/SYSTEM-Kontext (Service).
pub fn apply_doh(server_ip: &str, doh_template: &str) -> Result<(), String> {
    ps_run(&apply_script(server_ip, doh_template)).map(|_| ())
}

/// Entfernt den DoH-Schutz. Braucht Admin/SYSTEM-Kontext (Service).
pub fn teardown_doh(server_ip: &str) -> Result<(), String> {
    ps_run(&teardown_script(server_ip)).map(|_| ())
}

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

    #[test]
    fn doh_template_embeds_token() {
        let t = ProtectionState::doh_template_for("abc123");
        assert_eq!(t, "https://dns.rebreak.org/dns-query/abc123");
    }

    #[test]
    fn scripts_contain_ip_and_template() {
        let s = apply_script("1.2.3.4", "https://dns.rebreak.org/dns-query/tok");
        assert!(s.contains("1.2.3.4"));
        assert!(s.contains("/dns-query/tok"));
        assert!(s.contains("EnableAutoDoh"));
        let t = teardown_script("1.2.3.4");
        assert!(t.contains("ResetServerAddresses"));
        let c = check_script("1.2.3.4");
        assert!(c.contains("APPLIED"));
    }

    #[test]
    fn state_roundtrip() {
        let st = ProtectionState {
            dns_token: "tok".into(),
            server_ip: "1.2.3.4".into(),
            doh_template: "https://dns.rebreak.org/dns-query/tok".into(),
            backend_url: "https://staging.rebreak.org".into(),
            device_id: "win-x".into(),
        };
        let json = serde_json::to_string(&st).unwrap();
        let back: ProtectionState = serde_json::from_str(&json).unwrap();
        assert_eq!(back.dns_token, "tok");
        // camelCase für Lesbarkeit im JSON-File
        assert!(json.contains("dnsToken"));
    }
}
