package api

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"
)

// ── Security Audit ──────────────────────────────────────────────────────────

// nftDportRe matches "dport 443" as well as the set form nft uses for
// multiple ports on one rule, e.g. "dport { 80, 443 }".
var nftDportRe = regexp.MustCompile(`dport\s+(\{[^}]*\}|\d+)`)

// nftHasPort reports whether nft's "list ruleset" output opens the given
// port, either as a standalone "dport <port>" rule or as a member of a
// "dport { ... }" set. A plain strings.Contains(nftStr, "dport 443") misses
// the set form because "{ 80, " sits between "dport" and "443", producing a
// false "no HTTPS" warning even when port 443 is correctly whitelisted.
func nftHasPort(nftStr, port string) bool {
	for _, m := range nftDportRe.FindAllStringSubmatch(nftStr, -1) {
		group := m[1]
		if group == port {
			return true
		}
		if strings.HasPrefix(group, "{") {
			for _, p := range strings.Split(strings.Trim(group, "{} "), ",") {
				if strings.TrimSpace(p) == port {
					return true
				}
			}
		}
	}
	return false
}

type securityCheck struct {
	Name    string `json:"name"`
	Status  string `json:"status"` // "ok" | "warning" | "error"
	Message string `json:"message"`
}

func (s *Server) handleSecurityAudit(w http.ResponseWriter, r *http.Request) {
	var checks []securityCheck

	// 1. Firewall (nftables) aktiv?
	nftOut, err := exec.CommandContext(r.Context(), "nft", "list", "ruleset").Output()
	nftStr := string(nftOut)
	firewallActive := err == nil

	if !firewallActive {
		checks = append(checks, securityCheck{
			Name:    "Firewall (nftables)",
			Status:  "error",
			Message: "nft konnte nicht ausgeführt werden — Firewall möglicherweise inaktiv",
		})
	} else if strings.Contains(nftStr, "policy drop") {
		checks = append(checks, securityCheck{
			Name:    "Firewall (nftables)",
			Status:  "ok",
			Message: "Aktiv — Input-Chain policy: drop (Whitelist-Modus)",
		})
	} else {
		checks = append(checks, securityCheck{
			Name:    "Firewall (nftables)",
			Status:  "warning",
			Message: "nftables aktiv, aber Input-Chain policy ist nicht 'drop'",
		})
	}

	// 2. Port 3000 (Next.js) extern erreichbar?
	if !firewallActive {
		checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"})
	} else if nftHasPort(nftStr, "3000") {
		checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "warning", Message: "Port 3000 explizit in Firewall-Regeln — prüfen ob gewollt"})
	} else {
		checks = append(checks, securityCheck{Name: "Port 3000 (Next.js)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"})
	}

	// 3. Port 8080 (Go Backend) extern erreichbar?
	if !firewallActive {
		checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "error", Message: "Keine Firewall aktiv — Port möglicherweise extern erreichbar"})
	} else if nftHasPort(nftStr, "8080") {
		checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "warning", Message: "Port 8080 explizit in Firewall-Regeln — prüfen ob gewollt"})
	} else {
		checks = append(checks, securityCheck{Name: "Port 8080 (Go Backend)", Status: "ok", Message: "Blockiert (nicht in Whitelist)"})
	}

	// 4. HTTPS aktiv?
	if firewallActive && nftHasPort(nftStr, "443") {
		checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "ok", Message: "Port 443 in Firewall freigegeben"})
	} else {
		checks = append(checks, securityCheck{Name: "HTTPS (TLS)", Status: "warning", Message: "Kein HTTPS — Verbindungen unverschlüsselt (certbot empfohlen)"})
	}

	// 5. SSH PermitRootLogin + PasswordAuthentication
	sshConf, err := os.ReadFile("/etc/ssh/sshd_config")
	if err == nil {
		lines := strings.Split(string(sshConf), "\n")
		rootLogin := ""
		passAuth := ""
		for _, l := range lines {
			tl := strings.ToLower(strings.TrimSpace(l))
			if strings.HasPrefix(tl, "permitrootlogin") && !strings.HasPrefix(tl, "#") {
				rootLogin = tl
			}
			if strings.HasPrefix(tl, "passwordauthentication") && !strings.HasPrefix(tl, "#") {
				passAuth = tl
			}
		}
		if strings.Contains(rootLogin, "no") || strings.Contains(rootLogin, "prohibit-password") {
			checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "ok", Message: rootLogin})
		} else if rootLogin == "" {
			checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "warning", Message: "Nicht explizit gesetzt (Standard: prohibit-password)"})
		} else {
			checks = append(checks, securityCheck{Name: "SSH Root-Login", Status: "warning", Message: rootLogin + " — Passwort-Login für root möglich"})
		}
		if strings.Contains(passAuth, "no") {
			checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "ok", Message: "Nur Key-basierte Authentifizierung"})
		} else if passAuth == "" {
			checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "warning", Message: "Nicht explizit deaktiviert — SSH-Keys empfohlen"})
		} else {
			checks = append(checks, securityCheck{Name: "SSH Passwort-Auth", Status: "warning", Message: passAuth + " — Brute-Force-Risiko"})
		}
	} else {
		checks = append(checks, securityCheck{Name: "SSH Konfiguration", Status: "warning", Message: "/etc/ssh/sshd_config nicht lesbar"})
	}

	// 6. Fail2ban
	f2bOut, err := exec.CommandContext(r.Context(), "systemctl", "is-active", "fail2ban").Output()
	if err == nil && strings.TrimSpace(string(f2bOut)) == "active" {
		checks = append(checks, securityCheck{Name: "Fail2ban", Status: "ok", Message: "Aktiv"})
	} else {
		checks = append(checks, securityCheck{Name: "Fail2ban", Status: "warning", Message: "Nicht aktiv — kein Brute-Force-Schutz (apt install fail2ban)"})
	}

	writeJSON(w, http.StatusOK, map[string]interface{}{
		"checks": checks,
		"run_at": time.Now().UTC().Format(time.RFC3339),
	})
}

// ── Security Fix ────────────────────────────────────────────────────────────

// allowedFixActions is a strict whitelist — only these actions may be executed.
var allowedFixActions = map[string]bool{
	"install_fail2ban":      true,
	"enable_firewall":       true,
	"fix_ssh_password_auth": true,
	"fix_ssh_root_login":    true,
}

func (s *Server) handleSecurityFix(w http.ResponseWriter, r *http.Request) {
	var body struct {
		Action string `json:"action"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}
	if !allowedFixActions[body.Action] {
		writeError(w, http.StatusBadRequest, "unknown action: "+body.Action)
		return
	}

	ctx := r.Context()
	var msg string
	var fixErr error

	switch body.Action {

	case "install_fail2ban":
		// Install fail2ban if not present
		if _, err := exec.LookPath("fail2ban-client"); err != nil {
			out, err := exec.CommandContext(ctx, "apt-get", "install", "-y", "fail2ban").CombinedOutput()
			if err != nil {
				writeError(w, http.StatusInternalServerError, "apt-get install fail2ban: "+string(out))
				return
			}
		}
		// Write a minimal jail.local if not already present
		jailPath := "/etc/fail2ban/jail.local"
		if _, err := os.Stat(jailPath); os.IsNotExist(err) {
			jailConf := "[sshd]\nenabled = true\nmaxretry = 5\nbantime = 3600\nfindtime = 600\n"
			if err := os.WriteFile(jailPath, []byte(jailConf), 0644); err != nil {
				s.logger.Error("could not write jail.local", "err", err)
				writeError(w, http.StatusInternalServerError, "security config update failed")
				return
			}
		}
		// Enable and start
		exec.CommandContext(ctx, "systemctl", "enable", "fail2ban").Run()
		out, err := exec.CommandContext(ctx, "systemctl", "restart", "fail2ban").CombinedOutput()
		if err != nil {
			fixErr = fmt.Errorf("systemctl restart fail2ban: %s", string(out))
		} else {
			msg = "Fail2ban installiert, SSH-Jail aktiviert und Dienst gestartet."
		}

	case "enable_firewall":
		// Reload rules from /etc/nftables.conf and enable service
		out, err := exec.CommandContext(ctx, "nft", "-f", "/etc/nftables.conf").CombinedOutput()
		if err != nil {
			writeError(w, http.StatusInternalServerError, "nft -f /etc/nftables.conf: "+string(out))
			return
		}
		exec.CommandContext(ctx, "systemctl", "enable", "nftables").Run()
		msg = "nftables-Regeln neu geladen und Dienst aktiviert."

	case "fix_ssh_password_auth":
		fixErr = sshConfigSet("PasswordAuthentication", "no")
		if fixErr == nil {
			out, err := exec.CommandContext(ctx, "systemctl", "restart", "ssh").CombinedOutput()
			if err != nil {
				fixErr = fmt.Errorf("systemctl restart ssh: %s", string(out))
			} else {
				msg = "PasswordAuthentication auf 'no' gesetzt, SSH neu gestartet."
			}
		}

	case "fix_ssh_root_login":
		fixErr = sshConfigSet("PermitRootLogin", "prohibit-password")
		if fixErr == nil {
			out, err := exec.CommandContext(ctx, "systemctl", "restart", "ssh").CombinedOutput()
			if err != nil {
				fixErr = fmt.Errorf("systemctl restart ssh: %s", string(out))
			} else {
				msg = "PermitRootLogin auf 'prohibit-password' gesetzt, SSH neu gestartet."
			}
		}
	}

	if fixErr != nil {
		writeError(w, http.StatusInternalServerError, fixErr.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"message": msg})
}

// sshConfigSet sets or replaces a directive in /etc/ssh/sshd_config.
// Commented-out lines are left untouched; the active directive is updated or appended.
func sshConfigSet(key, value string) error {
	const path = "/etc/ssh/sshd_config"
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read sshd_config: %w", err)
	}
	lines := strings.Split(string(data), "\n")
	keyLower := strings.ToLower(key)
	found := false
	for i, l := range lines {
		tl := strings.ToLower(strings.TrimSpace(l))
		if strings.HasPrefix(tl, keyLower) && !strings.HasPrefix(strings.TrimSpace(l), "#") {
			lines[i] = key + " " + value
			found = true
		}
	}
	if !found {
		lines = append(lines, key+" "+value)
	}
	return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
}
