package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net/netip"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	"adguard-shield/internal/appinfo"
	"adguard-shield/internal/config"
	"adguard-shield/internal/daemon"
	"adguard-shield/internal/installer"
	"adguard-shield/internal/report"
)

const statusBanLimit = 50

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "FEHLER:", err)
		os.Exit(1)
	}
}

func run() error {
	args := os.Args[1:]
	confPath := config.DefaultPath()
	if len(args) >= 2 && args[0] == "-config" {
		confPath = args[1]
		args = args[2:]
	}
	cmd := "run"
	if len(args) > 0 {
		cmd = args[0]
		args = args[1:]
	}
	switch cmd {
	case "version", "--version", "-v":
		fmt.Println(appinfo.Version)
		return nil
	case "install":
		return installCommand(args)
	case "update":
		return updateCommand(args)
	case "uninstall":
		return uninstallCommand(args)
	case "install-status":
		return installStatusCommand(args)
	}
	cfg, err := config.Load(confPath)
	if err != nil {
		return err
	}
	d, err := daemon.New(cfg)
	if err != nil {
		return err
	}
	defer d.Close()
	ctx, stop := commandContext(d, cmd == "run" || cmd == "start" || cmd == "dry-run")
	defer stop()
	switch cmd {
	case "run", "start", "dry-run":
		if cmd == "dry-run" {
			cfg.DryRun = true
		}
		if err := writePID(cfg.PIDFile); err != nil {
			return err
		}
		defer os.Remove(cfg.PIDFile)
		err := d.Run(ctx)
		if errors.Is(err, context.Canceled) {
			return nil
		}
		return err
	case "stop":
		return stopDaemon(cfg.PIDFile)
	case "test":
		items, err := d.FetchQueryLog(ctx)
		if err != nil {
			return err
		}
		fmt.Printf("Verbindung erfolgreich. %d Querylog-Einträge gefunden.\n", len(items))
	case "status":
		return status(d)
	case "ip-status":
		if len(args) < 1 {
			return fmt.Errorf("Nutzung: adguard-shield ip-status <IP>")
		}
		return ipStatus(d, args[0])
	case "live", "watch":
		return liveCommand(ctx, d, args)
	case "logs":
		return logsCommand(d, args)
	case "logs-follow":
		return logsFollowCommand(ctx, d, args)
	case "history":
		limit := 50
		if len(args) > 0 {
			if n, err := strconv.Atoi(args[0]); err == nil && n > 0 {
				limit = n
			}
		}
		lines, err := d.Store.RecentHistory(limit)
		if err != nil {
			return err
		}
		for _, l := range lines {
			fmt.Println(l)
		}
	case "flush":
		bans, err := d.Store.ActiveBans()
		if err != nil {
			return err
		}
		for _, b := range bans {
			_ = d.UnbanQuiet(ctx, b.IP, "manual-flush")
		}
		d.NotifyBulkUnban(ctx, "manual-flush", len(bans))
		fmt.Printf("%d Sperren aufgehoben\n", len(bans))
	case "unban":
		if len(args) < 1 {
			return fmt.Errorf("Nutzung: adguard-shield unban <IP>")
		}
		return d.Unban(ctx, args[0], "manual")
	case "ban":
		if len(args) < 1 {
			return fmt.Errorf("Nutzung: adguard-shield ban <IP>")
		}
		return d.Ban(ctx, args[0], "manual", 0, "-", "manual", "manual", "", true)
	case "reset-offenses":
		ip := ""
		if len(args) > 0 {
			ip = args[0]
		}
		return d.Store.ResetOffense(ip)
	case "offense-cleanup":
		n, err := d.Store.CleanupOffenses(cfg.ProgressiveBanResetAfter)
		if err != nil {
			return err
		}
		fmt.Printf("%d abgelaufene Offense-Zaehler entfernt\n", n)
	case "offense-status":
		total, err := d.Store.CountOffenses()
		if err != nil {
			return err
		}
		expired, err := d.Store.CountExpiredOffenses(cfg.ProgressiveBanResetAfter)
		if err != nil {
			return err
		}
		fmt.Println("Offense-Cleanup")
		fmt.Printf("Aktiv im Daemon: %v\n", cfg.ProgressiveBanEnabled)
		fmt.Printf("Reset nach: %ds\n", cfg.ProgressiveBanResetAfter)
		fmt.Printf("Offense-Zaehler gesamt: %d\n", total)
		fmt.Printf("Davon abgelaufen: %d\n", expired)
	case "geoip-lookup":
		if len(args) < 1 {
			return fmt.Errorf("Nutzung: adguard-shield geoip-lookup <IP>")
		}
		if err := d.Geo.Open(ctx); err != nil {
			return err
		}
		cc, err := d.Geo.Lookup(args[0])
		if err != nil {
			return err
		}
		fmt.Printf("IP: %s -> Land: %s\n", args[0], empty(cc, "unbekannt"))
	case "geoip-sync":
		if err := d.Geo.Open(ctx); err != nil {
			return err
		}
		items, err := d.FetchQueryLog(ctx)
		if err != nil {
			return err
		}
		events := d.ToEventsForCommand(items)
		seen := map[string]bool{}
		for _, ev := range events {
			if seen[ev] {
				continue
			}
			seen[ev] = true
			d.CheckGeoIPForCommand(ctx, ev)
		}
		fmt.Printf("GeoIP-Sync abgeschlossen: %d Clients geprüft\n", len(seen))
	case "geoip-status":
		return geoipStatus(d)
	case "geoip-flush":
		bans, err := d.Store.ActiveBans()
		if err != nil {
			return err
		}
		n := 0
		for _, b := range bans {
			if b.Reason == "geoip" || b.Source == "geoip" {
				_ = d.UnbanQuiet(ctx, b.IP, "geoip-flush")
				n++
			}
		}
		d.NotifyBulkUnban(ctx, "geoip-flush", n)
		fmt.Printf("%d GeoIP-Sperren aufgehoben\n", n)
	case "geoip-flush-cache":
		n, err := d.Store.ClearGeoIPCache()
		if err != nil {
			return err
		}
		fmt.Printf("%d GeoIP-Cache-Einträge entfernt\n", n)
	case "blocklist-sync":
		return d.SyncBlocklist(ctx)
	case "whitelist-sync":
		return d.SyncWhitelist(ctx)
	case "blocklist-status":
		return blocklistStatus(d)
	case "whitelist-status":
		return whitelistStatus(d)
	case "blocklist-flush":
		return flushSource(ctx, d, "external-blocklist")
	case "whitelist-flush":
		return d.Store.ReplaceWhitelist(nil, "external")
	case "report-status":
		fmt.Print(report.Status(cfg))
	case "report-generate":
		format := ""
		output := ""
		if len(args) > 0 {
			format = args[0]
		}
		if len(args) > 1 {
			output = args[1]
		}
		body, err := report.Generate(cfg, d.Store, format)
		if err != nil {
			return err
		}
		if output != "" {
			return os.WriteFile(output, []byte(body), 0644)
		}
		fmt.Print(body)
	case "report-send":
		return report.Send(ctx, cfg, d.Store)
	case "report-test":
		return report.SendTest(ctx, cfg)
	case "report-install":
		binary := "/opt/adguard-shield/adguard-shield"
		if _, err := os.Stat(installer.CLICommandPath); err == nil {
			binary = installer.CLICommandPath
		}
		return report.InstallCron(binary, cfg.Path, cfg)
	case "report-remove":
		return report.RemoveCron()
	case "firewall-create":
		return d.FW.Setup(ctx)
	case "firewall-remove":
		return d.FW.Remove(ctx)
	case "firewall-flush":
		return d.FW.Flush(ctx)
	case "firewall-status":
		return firewallStatus(ctx, d)
	case "firewall-save":
		return d.SaveFirewallRules(ctx)
	case "firewall-restore":
		return restoreFirewallRules(ctx, d)
	default:
		usage()
	}
	return nil
}

func ipStatus(d *daemon.Daemon, rawIP string) error {
	ip := strings.TrimSpace(rawIP)
	if _, err := netip.ParseAddr(ip); err != nil {
		return fmt.Errorf("ungültige IP %q", rawIP)
	}

	fmt.Println("IP Status")
	fmt.Printf("IP: %s\n", ip)

	printIPBanStatus(d, ip)
	printIPWhitelistStatus(d, ip)
	printIPOffenseStatus(d, ip)
	printIPGeoIPCacheStatus(d, ip)
	return printIPHistory(d, ip, 5)
}

func printIPBanStatus(d *daemon.Daemon, ip string) {
	b, ok, err := d.Store.BanByIP(ip)
	if err != nil {
		fmt.Printf("Sperre: Fehler (%v)\n", err)
		return
	}
	if !ok {
		fmt.Println("Sperre: nein")
		return
	}
	fmt.Println("Sperre: aktiv")
	fmt.Printf("  Quelle: %s\n", empty(b.Source, "unbekannt"))
	fmt.Printf("  Grund: %s\n", empty(b.Reason, "unbekannt"))
	if b.Domain != "" && b.Domain != "-" {
		fmt.Printf("  Domain: %s\n", b.Domain)
	}
	if b.Count > 0 {
		fmt.Printf("  Anzahl: %d\n", b.Count)
	}
	if b.Protocol != "" && b.Protocol != "-" {
		fmt.Printf("  Protokoll: %s\n", b.Protocol)
	}
	fmt.Printf("  Dauer: %s\n", ipStatusDuration(b.Permanent, b.Duration))
	if !b.Permanent && b.BanUntil > 0 {
		until := time.Unix(b.BanUntil, 0)
		state := until.Format("2006-01-02 15:04:05")
		if time.Now().Unix() >= b.BanUntil {
			state += " (abgelaufen, Cleanup ausstehend)"
		}
		fmt.Printf("  Ablauf: %s\n", state)
	}
	if b.OffenseLevel > 0 {
		fmt.Printf("  Offense-Stufe bei Sperre: %d\n", b.OffenseLevel)
	}
	if b.GeoIPCountry != "" {
		fmt.Printf("  GeoIP: %s (%s)\n", b.GeoIPCountry, empty(b.GeoIPMode, "unbekannt"))
	}
}

func printIPWhitelistStatus(d *daemon.Daemon, ip string) {
	var hits []string
	for _, entry := range d.Config.Whitelist {
		if strings.TrimSpace(entry) == ip {
			hits = append(hits, "statisch")
			break
		}
	}
	wl, ok, err := d.Store.WhitelistByIP(ip)
	if err != nil {
		fmt.Printf("Whitelist: Fehler (%v)\n", err)
		return
	}
	if ok {
		source := empty(wl.Source, "extern")
		if wl.ResolvedAt != "" {
			source += ", aufgeloest " + wl.ResolvedAt
		}
		hits = append(hits, source)
	}
	if len(hits) == 0 {
		fmt.Println("Whitelist: nein")
		return
	}
	fmt.Printf("Whitelist: ja (%s)\n", strings.Join(hits, "; "))
}

func printIPOffenseStatus(d *daemon.Daemon, ip string) {
	o, ok, err := d.Store.OffenseByIP(ip)
	if err != nil {
		fmt.Printf("Offense-Zaehler: Fehler (%v)\n", err)
		return
	}
	if !ok {
		fmt.Println("Offense-Zaehler: keiner")
		return
	}
	state := "aktiv"
	if o.LastEpoch > 0 && time.Now().Unix()-o.LastEpoch > d.Config.ProgressiveBanResetAfter {
		state = "abgelaufen"
	}
	fmt.Printf("Offense-Zaehler: Stufe %d (%s)\n", o.Level, state)
	if o.First != "" {
		fmt.Printf("  Erster Treffer: %s\n", o.First)
	}
	if o.Last != "" {
		fmt.Printf("  Letzter Treffer: %s\n", o.Last)
	}
	if d.Config.ProgressiveBanResetAfter > 0 {
		fmt.Printf("  Reset nach: %ds\n", d.Config.ProgressiveBanResetAfter)
	}
}

func printIPGeoIPCacheStatus(d *daemon.Daemon, ip string) {
	cache, ok, err := d.Store.GeoIPCacheByIP(ip)
	if err != nil {
		fmt.Printf("GeoIP-Cache: Fehler (%v)\n", err)
		return
	}
	if !ok {
		fmt.Println("GeoIP-Cache: kein Eintrag")
		return
	}
	fmt.Printf("GeoIP-Cache: %s\n", empty(cache.CountryCode, "unbekannt"))
	if cache.LookedUpAtEpoch > 0 {
		fmt.Printf("  Nachgeschlagen: %s\n", time.Unix(cache.LookedUpAtEpoch, 0).Format("2006-01-02 15:04:05"))
	}
}

func printIPHistory(d *daemon.Daemon, ip string, limit int) error {
	lines, err := d.Store.RecentHistoryByIP(ip, limit)
	if err != nil {
		return err
	}
	if len(lines) == 0 {
		fmt.Println("History: keine Eintraege")
		return nil
	}
	fmt.Printf("History: letzte %d Eintraege\n", len(lines))
	for _, l := range lines {
		fmt.Printf("  %s\n", l)
	}
	return nil
}

func status(d *daemon.Daemon) error {
	bans, err := d.Store.ActiveBans()
	if err != nil {
		return err
	}
	fmt.Println("AdGuard Shield Daemon Status")
	fmt.Printf("Config: %s\n", d.Config.Path)
	fmt.Printf("Firewall: %s/%s (Chain: %s)\n", d.Config.FirewallBackend, d.Config.FirewallMode, d.Config.Chain)
	fmt.Printf("GeoIP: %v (%s %v)\n", d.Config.GeoIPEnabled, d.Config.GeoIPMode, d.Config.GeoIPCountries)
	fmt.Printf("Externe Blocklist: %v (%d URLs)\n", d.Config.ExternalBlocklistEnabled, len(d.Config.ExternalBlocklistURLs))
	fmt.Printf("Externe Whitelist: %v (%d URLs)\n", d.Config.ExternalWhitelistEnabled, len(d.Config.ExternalWhitelistURLs))
	fmt.Printf("Aktive Sperren: %d\n", len(bans))
	limit := min(len(bans), statusBanLimit)
	for _, b := range bans[:limit] {
		until := "permanent"
		if !b.Permanent && b.BanUntil > 0 {
			until = time.Unix(b.BanUntil, 0).Format("2006-01-02 15:04:05")
		}
		fmt.Printf("  %s | %s | %s | %s\n", b.IP, b.Source, b.Reason, until)
	}
	if len(bans) > limit {
		fmt.Printf("  ... %d weitere Sperren. Details mit: adguard-shield ip-status <IP> oder history.\n", len(bans)-limit)
	}
	return nil
}

func ipStatusDuration(permanent bool, seconds int64) string {
	if permanent || seconds == 0 {
		return "permanent"
	}
	return strconv.FormatInt(seconds, 10) + "s"
}

func blocklistStatus(d *daemon.Daemon) error {
	count, err := d.Store.CountBySource("external-blocklist")
	if err != nil {
		return err
	}
	fmt.Println("Externe Blocklist")
	fmt.Printf("Aktiv: %v\n", d.Config.ExternalBlocklistEnabled)
	fmt.Printf("Intervall: %ds\n", d.Config.ExternalBlocklistInterval)
	fmt.Printf("Auto-Unban: %v\n", d.Config.ExternalBlocklistAutoUnban)
	fmt.Printf("Cache: %s\n", d.Config.ExternalBlocklistCacheDir)
	fmt.Printf("Aktive Sperren: %d\n", count)
	for i, u := range d.Config.ExternalBlocklistURLs {
		fmt.Printf("  [%d] %s\n", i, u)
	}
	return nil
}

func whitelistStatus(d *daemon.Daemon) error {
	wl, err := d.Store.AllWhitelist()
	if err != nil {
		return err
	}
	fmt.Println("Externe Whitelist")
	fmt.Printf("Aktiv: %v\n", d.Config.ExternalWhitelistEnabled)
	fmt.Printf("Intervall: %ds\n", d.Config.ExternalWhitelistInterval)
	fmt.Printf("Cache: %s\n", d.Config.ExternalWhitelistCacheDir)
	fmt.Printf("Aufgelöste IPs: %d\n", len(wl))
	for i, u := range d.Config.ExternalWhitelistURLs {
		fmt.Printf("  [%d] %s\n", i, u)
	}
	return nil
}

func geoipStatus(d *daemon.Daemon) error {
	fmt.Println("GeoIP Status")
	fmt.Printf("Aktiv: %v\n", d.Config.GeoIPEnabled)
	fmt.Printf("Modus: %s\n", d.Config.GeoIPMode)
	fmt.Printf("Länder: %v\n", d.Config.GeoIPCountries)
	fmt.Printf("Cache TTL: %ds\n", d.Config.GeoIPCacheTTL)
	fmt.Printf("MMDB: %s\n", empty(d.Config.GeoIPMMDBPath, "<auto/falls License-Key gesetzt>"))
	bans, err := d.Store.BansByReason("geoip")
	if err != nil {
		return err
	}
	fmt.Printf("Aktive GeoIP-Sperren: %d\n", len(bans))
	return nil
}

func flushSource(ctx context.Context, d *daemon.Daemon, source string) error {
	bans, err := d.Store.BansBySource(source)
	if err != nil {
		return err
	}
	for _, b := range bans {
		_ = d.UnbanQuiet(ctx, b.IP, source+"-flush")
	}
	d.NotifyBulkUnban(ctx, source+"-flush", len(bans))
	fmt.Printf("%d Sperren aufgehoben\n", len(bans))
	return nil
}

func writePID(path string) error {
	if path == "" {
		return nil
	}
	return os.WriteFile(path, []byte(strconv.Itoa(os.Getpid())+"\n"), 0644)
}

func commandContext(d *daemon.Daemon, notifyServiceStop bool) (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithCancel(context.Background())
	signals := make(chan os.Signal, 1)
	done := make(chan struct{})
	signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
	go func() {
		select {
		case <-signals:
			if notifyServiceStop {
				d.NotifyServiceStop(context.Background())
			}
			cancel()
		case <-done:
		}
	}()
	return ctx, func() {
		close(done)
		signal.Stop(signals)
		cancel()
	}
}

func stopDaemon(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("Daemon läuft nicht oder PID-Datei fehlt: %w", err)
	}
	pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
	if err != nil {
		return err
	}
	proc, err := os.FindProcess(pid)
	if err != nil {
		return err
	}
	if err := proc.Signal(syscall.SIGTERM); err != nil {
		return err
	}
	fmt.Printf("Daemon gestoppt (PID %d)\n", pid)
	return nil
}

func firewallStatus(ctx context.Context, d *daemon.Daemon) error {
	fmt.Printf("Firewall Backend: %s\n", d.Config.FirewallBackend)
	fmt.Printf("Firewall Modus: %s\n", d.Config.FirewallMode)
	fmt.Printf("Chain: %s\n", d.Config.Chain)
	for _, cmd := range [][]string{
		{"ipset", "list", "adguard_shield_v4"},
		{"ipset", "list", "adguard_shield_v6"},
		{"iptables", "-n", "-L", d.Config.Chain, "--line-numbers", "-v"},
		{"ip6tables", "-n", "-L", d.Config.Chain, "--line-numbers", "-v"},
	} {
		out, err := exec.CommandContext(ctx, cmd[0], cmd[1:]...).CombinedOutput()
		fmt.Printf("\n--- %s ---\n", cmd[0])
		if err != nil {
			fmt.Printf("%v\n", err)
		}
		fmt.Print(string(out))
	}
	return nil
}

func restoreFirewallRules(ctx context.Context, d *daemon.Daemon) error {
	files := []struct {
		path string
		cmd  string
	}{
		{filepath.Join(d.Config.StateDir, "iptables-rules.v4"), "iptables-restore"},
		{filepath.Join(d.Config.StateDir, "iptables-rules.v6"), "ip6tables-restore"},
	}
	for _, f := range files {
		data, err := os.ReadFile(f.path)
		if err != nil {
			continue
		}
		c := exec.CommandContext(ctx, f.cmd)
		stdin, err := c.StdinPipe()
		if err != nil {
			return err
		}
		if err := c.Start(); err != nil {
			return err
		}
		_, _ = stdin.Write(data)
		_ = stdin.Close()
		if err := c.Wait(); err != nil {
			return err
		}
	}
	return nil
}

func empty(s, fallback string) string {
	if s == "" {
		return fallback
	}
	return s
}

func usage() {
	fmt.Println(`AdGuard Shield Daemon

Nutzung:
  adguard-shield version
  adguard-shield install [--config-source PATH] [--skip-deps] [--no-register]
  adguard-shield update [--config-source PATH] [--skip-deps] [--no-register]
  adguard-shield uninstall [--keep-config]
  adguard-shield install-status
  adguard-shield [-config PATH] run|start|stop|dry-run
  adguard-shield status|ip-status IP|history [N]|test|flush|ban IP|unban IP|reset-offenses [IP]
  adguard-shield live [--interval N] [--top N] [--recent N] [--logs LEVEL] [--once]
  adguard-shield logs [--level LEVEL] [--limit N]|logs-follow [--level LEVEL]
  adguard-shield offense-status|offense-cleanup
  adguard-shield geoip-status|geoip-sync|geoip-flush|geoip-flush-cache|geoip-lookup IP
  adguard-shield blocklist-status|blocklist-sync|blocklist-flush
  adguard-shield whitelist-status|whitelist-sync|whitelist-flush
  adguard-shield report-status|report-generate [html|txt] [OUTPUT]|report-send|report-test|report-install|report-remove
  adguard-shield firewall-create|firewall-remove|firewall-flush|firewall-status|firewall-save|firewall-restore`)
}

func liveCommand(ctx context.Context, d *daemon.Daemon, args []string) error {
	fs := flag.NewFlagSet("live", flag.ContinueOnError)
	interval := fs.Int("interval", d.Config.CheckInterval, "Aktualisierungsintervall in Sekunden")
	top := fs.Int("top", 10, "Anzahl Top-Einträge")
	recent := fs.Int("recent", 12, "Anzahl letzter Queries und Logs")
	logLevel := fs.String("logs", "INFO", "Systemlogs ab Level anzeigen: DEBUG, INFO, WARN, ERROR oder off")
	once := fs.Bool("once", false, "Nur einen Snapshot anzeigen")
	if err := fs.Parse(args); err != nil {
		return err
	}
	err := d.Live(ctx, os.Stdout, daemon.LiveOptions{
		Interval: time.Duration(*interval) * time.Second,
		Top:      *top,
		Recent:   *recent,
		LogLevel: *logLevel,
		Once:     *once,
	})
	if errors.Is(err, context.Canceled) {
		return nil
	}
	return err
}

func logsCommand(d *daemon.Daemon, args []string) error {
	level, limit, err := parseLogArgs("logs", args, "INFO", 80)
	if err != nil {
		return err
	}
	lines := daemon.RecentLogLines(d.Config.LogFile, level, limit)
	if len(lines) == 0 {
		fmt.Printf("Keine Logeinträge ab Level %s in %s gefunden.\n", strings.ToUpper(level), d.Config.LogFile)
		return nil
	}
	for _, line := range lines {
		fmt.Println(line)
	}
	return nil
}

func logsFollowCommand(ctx context.Context, d *daemon.Daemon, args []string) error {
	level, limit, err := parseLogArgs("logs-follow", args, "INFO", 40)
	if err != nil {
		return err
	}
	t := time.NewTicker(2 * time.Second)
	defer t.Stop()
	for {
		fmt.Print("\033[H\033[2J")
		fmt.Printf("AdGuard Shield Logs | %s | %s ab %s | Strg+C beendet\n", time.Now().Format("2006-01-02 15:04:05"), d.Config.LogFile, strings.ToUpper(level))
		fmt.Println(strings.Repeat("=", 92))
		lines := daemon.RecentLogLines(d.Config.LogFile, level, limit)
		if len(lines) == 0 {
			fmt.Println("Keine passenden Logeinträge.")
		} else {
			for _, line := range lines {
				fmt.Println(line)
			}
		}
		select {
		case <-ctx.Done():
			if errors.Is(ctx.Err(), context.Canceled) {
				return nil
			}
			return ctx.Err()
		case <-t.C:
		}
	}
}

func parseLogArgs(name string, args []string, defaultLevel string, defaultLimit int) (string, int, error) {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	level := fs.String("level", defaultLevel, "Mindestlevel: DEBUG, INFO, WARN, ERROR")
	limit := fs.Int("limit", defaultLimit, "Anzahl der letzten Logzeilen")
	if err := fs.Parse(args); err != nil {
		return "", 0, err
	}
	rest := fs.Args()
	if len(rest) > 0 {
		if isLogLevel(rest[0]) {
			*level = rest[0]
		} else if n, err := strconv.Atoi(rest[0]); err == nil && n > 0 {
			*limit = n
		}
	}
	if len(rest) > 1 {
		if n, err := strconv.Atoi(rest[1]); err == nil && n > 0 {
			*limit = n
		}
	}
	if !isLogLevel(*level) {
		return "", 0, fmt.Errorf("ungültiges Log-Level %q (erlaubt: DEBUG, INFO, WARN, ERROR)", *level)
	}
	if *limit <= 0 {
		*limit = defaultLimit
	}
	return strings.ToUpper(*level), *limit, nil
}

func isLogLevel(s string) bool {
	switch strings.ToUpper(strings.TrimSpace(s)) {
	case "DEBUG", "INFO", "WARN", "WARNING", "ERROR", "ERR":
		return true
	default:
		return false
	}
}

func installCommand(args []string) error {
	opts, err := parseInstallFlags("install", args)
	if err != nil {
		return err
	}
	err = installer.Install(opts)
	if le, ok := installer.IsLegacyError(err); ok {
		return fmt.Errorf("%s", installer.FormatLegacyMessage(le, opts.InstallDir))
	}
	if err != nil {
		return err
	}
	fmt.Println("AdGuard Shield Go-Installation abgeschlossen.")
	fmt.Println(installer.PrintStatus(installer.GetStatus(opts.InstallDir)))
	return nil
}

func updateCommand(args []string) error {
	opts, err := parseInstallFlags("update", args)
	if err != nil {
		return err
	}
	err = installer.Update(opts)
	if le, ok := installer.IsLegacyError(err); ok {
		return fmt.Errorf("%s", installer.FormatLegacyMessage(le, opts.InstallDir))
	}
	if err != nil {
		return err
	}
	fmt.Println("AdGuard Shield Go-Update abgeschlossen.")
	fmt.Println(installer.PrintStatus(installer.GetStatus(opts.InstallDir)))
	return nil
}

func uninstallCommand(args []string) error {
	fs := flag.NewFlagSet("uninstall", flag.ContinueOnError)
	opts := installer.DefaultOptions()
	fs.StringVar(&opts.InstallDir, "install-dir", opts.InstallDir, "Installationsverzeichnis")
	fs.BoolVar(&opts.KeepConfig, "keep-config", false, "Konfiguration behalten")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if err := installer.Uninstall(opts); err != nil {
		return err
	}
	fmt.Println("AdGuard Shield wurde deinstalliert.")
	return nil
}

func installStatusCommand(args []string) error {
	fs := flag.NewFlagSet("install-status", flag.ContinueOnError)
	installDir := installer.DefaultInstallDir
	fs.StringVar(&installDir, "install-dir", installDir, "Installationsverzeichnis")
	if err := fs.Parse(args); err != nil {
		return err
	}
	fmt.Print(installer.PrintStatus(installer.GetStatus(installDir)))
	return nil
}

func parseInstallFlags(name string, args []string) (installer.Options, error) {
	fs := flag.NewFlagSet(name, flag.ContinueOnError)
	opts := installer.DefaultOptions()
	fs.StringVar(&opts.InstallDir, "install-dir", opts.InstallDir, "Installationsverzeichnis")
	fs.StringVar(&opts.ConfigSource, "config-source", "", "Konfiguration fuer Neuinstallation uebernehmen")
	fs.BoolVar(&opts.SkipDeps, "skip-deps", false, "Paketpruefung ueberspringen")
	noEnable := fs.Bool("no-enable", false, "systemd Autostart nicht aktivieren")
	noRegister := fs.Bool("no-register", false, "CLI-Befehl nicht in /usr/local/bin registrieren")
	if err := fs.Parse(args); err != nil {
		return opts, err
	}
	opts.Enable = !*noEnable
	opts.RegisterCLI = !*noRegister
	return opts, nil
}
