package main

import (
	"archive/zip"
	"bytes"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
)

type Config struct {
	BackendURL string `yaml:"backend_url"`
	APIKey     string `yaml:"api_key"`
	AgentName  string `yaml:"agent_name"`
	Insecure   bool   `yaml:"insecure"`
}

// Plattform-spezifische Pfade und Befehle
var (
	agentBinaryPath   string
	updaterBinaryPath string
	configPath        string
	agentIDPath       string
	platformName      string

	// Dateinamen im ZIP-Bundle
	agentZipName   string
	updaterZipName string
)

func init() {
	switch runtime.GOOS {
	case "freebsd":
		agentBinaryPath = "/usr/local/rmm/rmm-agent"
		updaterBinaryPath = "/usr/local/rmm/rmm-updater"
		configPath = "/usr/local/rmm/config.yaml"
		agentIDPath = "/usr/local/rmm/agent_id"
		platformName = "freebsd"
		agentZipName = "rmm-agent"
		updaterZipName = "rmm-updater-freebsd"
	default: // linux
		agentBinaryPath = "/usr/local/bin/rmm-agent"
		updaterBinaryPath = "/usr/local/bin/rmm-updater"
		configPath = "/etc/rmm/config.yaml"
		agentIDPath = "/etc/rmm/agent_id"
		platformName = "linux"
		agentZipName = "rmm-agent-linux"
		updaterZipName = "rmm-updater-linux"
	}
}

func stopAgent() error {
	switch runtime.GOOS {
	case "freebsd":
		return exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run()
	default:
		return exec.Command("/usr/bin/systemctl", "stop", "rmm-agent").Run()
	}
}

func startAgent() error {
	switch runtime.GOOS {
	case "freebsd":
		// Versuche zuerst via rc.d service
		err := exec.Command("/usr/sbin/service", "rmm_agent", "start").Run()
		if err != nil {
			// Fallback: direkt via daemon starten
			log.Printf("service start fehlgeschlagen (%v), versuche daemon-Fallback", err)
			err = exec.Command("/usr/sbin/daemon",
				"-p", "/var/run/rmm-agent.pid",
				"-r",
				agentBinaryPath,
				"--config", configPath,
				"--insecure",
			).Start()
		}
		return err
	default:
		return exec.Command("/usr/bin/systemctl", "start", "rmm-agent").Run()
	}
}

func restartUpdater() {
	switch runtime.GOOS {
	case "freebsd":
		exec.Command("/usr/sbin/service", "rmm_updater", "restart").Run()
	default:
		exec.Command("/usr/bin/systemctl", "restart", "rmm-updater").Run()
	}
}

const (
	checkInterval = 60 * time.Second
	logPrefix     = "[rmm-updater] "
)

func main() {
	log.SetPrefix(logPrefix)
	log.SetFlags(log.LstdFlags)

	log.Printf("RMM Updater gestartet (platform=%s, agent=%s, updater=%s)", platformName, agentBinaryPath, updaterBinaryPath)

	cfg, err := loadConfig()
	if err != nil {
		log.Fatalf("Config laden fehlgeschlagen: %v", err)
	}

	agentID, err := loadAgentID()
	if err != nil {
		log.Fatalf("Agent-ID laden fehlgeschlagen: %v", err)
	}

	log.Printf("Backend: %s, Agent: %s (%s)", cfg.BackendURL, cfg.AgentName, agentID)

	client := &http.Client{
		Timeout: 120 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure},
		},
	}

	for {
		checkAndUpdate(client, cfg, agentID)
		time.Sleep(checkInterval)
	}
}

func loadConfig() (*Config, error) {
	data, err := os.ReadFile(configPath)
	if err != nil {
		return nil, err
	}
	var cfg Config
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, err
	}
	cfg.Insecure = true
	return &cfg, nil
}

func loadAgentID() (string, error) {
	data, err := os.ReadFile(agentIDPath)
	if err != nil {
		return "", fmt.Errorf("agent_id nicht gefunden: %v", err)
	}
	id := strings.TrimSpace(string(data))
	if id == "" {
		return "", fmt.Errorf("agent_id leer")
	}
	return id, nil
}

func checkAndUpdate(client *http.Client, cfg *Config, agentID string) {
	// Firmware-Info vom Backend holen (plattformspezifisch)
	url := fmt.Sprintf("%s/api/v1/firmware?platform=%s", cfg.BackendURL, platformName)
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("X-API-Key", cfg.APIKey)

	resp, err := client.Do(req)
	if err != nil {
		log.Printf("Firmware-Info Abfrage fehlgeschlagen: %v", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return
	}

	var info struct {
		Available bool   `json:"available"`
		Version   string `json:"version"`
		Hash      string `json:"hash"`
		Size      int    `json:"size"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil || !info.Available {
		return
	}

	// Pruefen ob lokaler Agent schon den gleichen Hash hat
	if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash {
		return // Schon aktuell
	}

	// Update angefordert?
	agentURL := fmt.Sprintf("%s/api/v1/agents/%s", cfg.BackendURL, agentID)
	agentReq, _ := http.NewRequest("GET", agentURL, nil)
	agentReq.Header.Set("X-API-Key", cfg.APIKey)

	agentResp, err := client.Do(agentReq)
	if err != nil {
		return
	}
	defer agentResp.Body.Close()

	var agentInfo struct {
		Agent struct {
			UpdateRequested bool `json:"update_requested"`
		} `json:"agent"`
	}
	if err := json.NewDecoder(agentResp.Body).Decode(&agentInfo); err != nil {
		return
	}
	if !agentInfo.Agent.UpdateRequested {
		return
	}

	log.Printf("Update verfuegbar: v%s (%d bytes, platform=%s)", info.Version, info.Size, platformName)

	if err := downloadAndApply(client, cfg, agentID, info.Hash); err != nil {
		log.Printf("Update fehlgeschlagen: %v", err)
		return
	}

	log.Printf("Update auf v%s erfolgreich", info.Version)
}

func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash string) error {
	url := fmt.Sprintf("%s/api/v1/firmware/download?platform=%s", cfg.BackendURL, platformName)
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("X-API-Key", cfg.APIKey)

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("download fehlgeschlagen: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("download HTTP %d", resp.StatusCode)
	}

	bundle, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("download lesen fehlgeschlagen: %v", err)
	}

	// Hash des gesamten Bundles verifizieren
	hash := sha256.Sum256(bundle)
	hashStr := hex.EncodeToString(hash[:])
	if hashStr != expectedHash {
		return fmt.Errorf("hash mismatch: erwartet %s, bekommen %s", expectedHash[:16], hashStr[:16])
	}

	log.Printf("Bundle heruntergeladen (%d bytes, Hash OK)", len(bundle))

	// Versuche als ZIP zu entpacken
	agentBinary, updaterBinary, err := extractZipBundle(bundle)
	if err != nil {
		// Fallback: Kein ZIP — einzelne Agent-Binary (Abwaertskompatibilitaet)
		log.Printf("Kein ZIP-Bundle, verwende als einzelne Agent-Binary")
		agentBinary = bundle
		updaterBinary = nil
	}

	// Agent stoppen
	log.Println("Agent wird gestoppt...")
	stopAgent()
	time.Sleep(2 * time.Second)

	// Agent-Binary ersetzen
	if err := replaceBinary(agentBinaryPath, agentBinary); err != nil {
		return fmt.Errorf("agent binary ersetzen fehlgeschlagen: %v", err)
	}
	log.Println("Agent-Binary aktualisiert")

	// Agent starten
	log.Println("Agent wird gestartet...")
	if err := startAgent(); err != nil {
		log.Printf("WARNUNG: Agent-Start fehlgeschlagen: %v — versuche Rollback", err)
		rollbackBinary(agentBinaryPath)
		startAgent()
		return fmt.Errorf("agent start fehlgeschlagen")
	}

	// Update-Flag loeschen
	clearURL := fmt.Sprintf("%s/api/v1/agents/%s/request-update", cfg.BackendURL, agentID)
	clearReq, _ := http.NewRequest("DELETE", clearURL, nil)
	clearReq.Header.Set("X-API-Key", cfg.APIKey)
	client.Do(clearReq)

	// Updater-Binary ersetzen (wenn im Bundle enthalten)
	if updaterBinary != nil {
		log.Println("Updater-Binary wird aktualisiert...")
		if err := replaceBinary(updaterBinaryPath, updaterBinary); err != nil {
			log.Printf("WARNUNG: Updater-Binary ersetzen fehlgeschlagen: %v", err)
		} else {
			log.Println("Updater-Binary aktualisiert, starte neu...")
			// Updater neustart — dieser Prozess wird beendet und neu gestartet
			go func() {
				time.Sleep(1 * time.Second)
				restartUpdater()
			}()
		}
	}

	return nil
}

func extractZipBundle(data []byte) (agentBin []byte, updaterBin []byte, err error) {
	reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		return nil, nil, fmt.Errorf("kein gueltiges ZIP: %v", err)
	}

	for _, f := range reader.File {
		rc, err := f.Open()
		if err != nil {
			continue
		}
		content, err := io.ReadAll(rc)
		rc.Close()
		if err != nil {
			continue
		}

		switch f.Name {
		case agentZipName:
			agentBin = content
			log.Printf("ZIP: Agent-Binary gefunden (%s, %d bytes)", f.Name, len(content))
		case updaterZipName:
			updaterBin = content
			log.Printf("ZIP: Updater-Binary gefunden (%s, %d bytes)", f.Name, len(content))
		default:
			log.Printf("ZIP: Unbekannte Datei ignoriert: %s", f.Name)
		}
	}

	if agentBin == nil {
		return nil, nil, fmt.Errorf("agent binary nicht im ZIP gefunden (erwartet: %s)", agentZipName)
	}

	return agentBin, updaterBin, nil
}

func replaceBinary(path string, newContent []byte) error {
	backupPath := path + ".bak"

	// Backup erstellen
	if _, err := os.Stat(path); err == nil {
		os.Rename(path, backupPath)
	}

	// Neue Binary schreiben
	if err := os.WriteFile(path, newContent, 0755); err != nil {
		// Rollback
		os.Rename(backupPath, path)
		return err
	}

	// Backup loeschen
	os.Remove(backupPath)
	return nil
}

func rollbackBinary(path string) {
	backupPath := path + ".bak"
	if _, err := os.Stat(backupPath); err == nil {
		os.Rename(backupPath, path)
	}
}

func hashFile(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	hash := sha256.Sum256(data)
	return hex.EncodeToString(hash[:]), nil
}
