package api

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/cynfo/rmm-backend/db"
	"github.com/cynfo/rmm-backend/models"
	"github.com/cynfo/rmm-backend/ws"
)

type TaskScheduler struct {
	db  *db.Database
	hub *ws.Hub
}

func NewTaskScheduler(database *db.Database, hub *ws.Hub) *TaskScheduler {
	return &TaskScheduler{db: database, hub: hub}
}

func (s *TaskScheduler) Run() {
	log.Println("Task-Scheduler gestartet (30s Intervall)")
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	// Run once immediately
	s.processDueTasks()

	for range ticker.C {
		s.processDueTasks()
	}
}

func (s *TaskScheduler) processDueTasks() {
	tasks, err := s.db.GetDueTasks()
	if err != nil {
		log.Printf("Scheduler: Fehler beim Laden faelliger Tasks: %v", err)
		return
	}

	for _, task := range tasks {
		go s.executeTask(task)
	}
}

func (s *TaskScheduler) executeTask(task models.Task) {
	log.Printf("Scheduler: Starte Task #%d (%s) fuer Agent %s", task.ID, task.Action, task.AgentID)

	// Set status to running
	s.db.UpdateTaskStatus(task.ID, "running", nil)

	var result interface{}
	var taskErr error

	switch task.Action {
	case "backup":
		result, taskErr = s.executeBackup(task.AgentID)
	case "update":
		result, taskErr = s.executeUpdateWithHealthCheck(task.AgentID, task.Reboot, task.HealthCheck, task.HealthCheckTimeout, task.SecurityOnly)
	case "zpoolscrub":
		result, taskErr = s.executeSimpleCommand(task.AgentID, "zpoolscrub", map[string]interface{}{})
	case "script":
		timeout := task.ScriptTimeout
		if timeout <= 0 {
			timeout = 300
		}
		result, taskErr = s.executeSimpleCommand(task.AgentID, "exec", map[string]interface{}{
			"command": task.Script,
			"timeout": timeout,
		})
	default:
		taskErr = fmt.Errorf("unbekannte Aktion: %s", task.Action)
	}

	success := taskErr == nil
	healthCheckWarning := false
	if !success {
		// Check if it's a health check warning (update ran OK but agent didn't come back)
		if resultMap, ok := result.(map[string]interface{}); ok {
			if hc, ok := resultMap["health_check"].(map[string]interface{}); ok {
				if online, ok := hc["agent_online"].(bool); ok && !online {
					healthCheckWarning = true
				}
			}
		}
		if !healthCheckWarning {
			result = map[string]string{"error": taskErr.Error()}
		}
		log.Printf("Scheduler: Task #%d fehlgeschlagen: %v", task.ID, taskErr)
	} else {
		log.Printf("Scheduler: Task #%d erfolgreich abgeschlossen", task.ID)
	}

	// Use CompleteRecurringTask for proper recurring handling
	// Health check warnings count as success for recurring schedule, but status = "completed_with_warning"
	scheduleSuccess := success || healthCheckWarning
	s.db.CompleteRecurringTask(task.ID, task.ScheduleType, task.ScheduleTime,
		task.ScheduleWeekday, task.ScheduleMonthday, scheduleSuccess, result)

	// Override status for health check warnings
	if healthCheckWarning {
		s.db.UpdateTaskStatusDirect(task.ID, "completed_with_warning")
	}

	// Re-read task for webhook payload
	fullTask, _ := s.db.GetTask(task.ID)

	// Send webhook if configured
	if task.WebhookURL != "" && fullTask != nil {
		s.sendWebhook(task.ID, task.WebhookURL, fullTask)
	}
}

// executeSimpleCommand schickt einen beliebigen Command an den Agenten und wartet auf Antwort
func (s *TaskScheduler) executeSimpleCommand(agentID, command string, params map[string]interface{}) (interface{}, error) {
	if !s.hub.IsAgentConnected(agentID) {
		return nil, fmt.Errorf("Agent nicht verbunden")
	}
	cmdID := generateID()
	cmd := map[string]interface{}{
		"type":    "command",
		"id":      cmdID,
		"command": command,
		"params":  params,
	}
	responseCh := s.hub.RegisterResponseWaiter(cmdID)
	defer s.hub.UnregisterResponseWaiter(cmdID)
	cmdJSON, _ := json.Marshal(cmd)
	if err := s.hub.SendToAgent(agentID, cmdJSON); err != nil {
		return nil, fmt.Errorf("Senden fehlgeschlagen: %w", err)
	}
	select {
	case resp := <-responseCh:
		if status, _ := resp["status"].(string); status == "error" {
			errMsg, _ := resp["error"].(string)
			return resp["data"], fmt.Errorf("%s", errMsg)
		}
		return resp["data"], nil
	case <-time.After(120 * time.Second):
		return nil, fmt.Errorf("Timeout nach 120s")
	}
}

func (s *TaskScheduler) executeBackup(agentID string) (interface{}, error) {
	if !s.hub.IsAgentConnected(agentID) {
		return nil, fmt.Errorf("Agent nicht verbunden")
	}

	cmdID := generateID()
	cmd := map[string]interface{}{
		"type":    "command",
		"id":      cmdID,
		"command": "backup",
		"params":  map[string]interface{}{},
	}

	responseCh := s.hub.RegisterResponseWaiter(cmdID)
	defer s.hub.UnregisterResponseWaiter(cmdID)

	cmdJSON, _ := json.Marshal(cmd)
	if err := s.hub.SendToAgent(agentID, cmdJSON); err != nil {
		return nil, fmt.Errorf("Command senden fehlgeschlagen: %w", err)
	}

	select {
	case resp := <-responseCh:
		respStatus, _ := resp["status"].(string)
		if respStatus != "ok" {
			errMsg, _ := resp["error"].(string)
			return resp, fmt.Errorf("Backup fehlgeschlagen: %s", errMsg)
		}

		data, ok := resp["data"].(map[string]interface{})
		if !ok {
			return resp, fmt.Errorf("ungueltiges Response-Format")
		}

		configB64, _ := data["config"].(string)
		hash, _ := data["hash"].(string)
		sizeFloat, _ := data["size"].(float64)
		size := int(sizeFloat)

		configXML, err := base64.StdEncoding.DecodeString(configB64)
		if err != nil {
			return nil, fmt.Errorf("Base64-Dekodierung fehlgeschlagen")
		}

		backupID, isNew, err := s.db.SaveConfigBackup(agentID, hash, size, string(configXML))
		if err != nil {
			return nil, fmt.Errorf("Backup speichern fehlgeschlagen: %w", err)
		}

		msg := "Backup gespeichert"
		if !isNew {
			msg = "Config unveraendert (Hash identisch)"
		}

		return map[string]interface{}{
			"message":   msg,
			"backup_id": backupID,
			"sha256":    hash,
			"size":      size,
			"new":       isNew,
		}, nil

	case <-time.After(60 * time.Second):
		return nil, fmt.Errorf("Timeout: Agent hat nicht rechtzeitig geantwortet")
	}
}

func (s *TaskScheduler) executeUpdate(agentID string, reboot bool) (interface{}, error) {
	return s.executeUpdateWithHealthCheck(agentID, reboot, false, 600, false)
}

func (s *TaskScheduler) executeUpdateWithHealthCheck(agentID string, reboot bool, healthCheck bool, healthCheckTimeout int, securityOnly bool) (interface{}, error) {
	if !s.hub.IsAgentConnected(agentID) {
		return nil, fmt.Errorf("Agent nicht verbunden")
	}

	// First: update_check
	checkCmdID := generateID()
	checkCmd := map[string]interface{}{
		"type":    "command",
		"id":      checkCmdID,
		"command": "update_check",
	}

	checkCh := s.hub.RegisterResponseWaiter(checkCmdID)
	defer s.hub.UnregisterResponseWaiter(checkCmdID)

	checkJSON, _ := json.Marshal(checkCmd)
	if err := s.hub.SendToAgent(agentID, checkJSON); err != nil {
		return nil, fmt.Errorf("Update-Check senden fehlgeschlagen: %w", err)
	}

	var checkResult map[string]interface{}
	select {
	case resp := <-checkCh:
		checkResult = resp
	case <-time.After(120 * time.Second):
		return nil, fmt.Errorf("Timeout beim Update-Check")
	}

	// Then: update
	updateCmdID := generateID()
	updateCmd := map[string]interface{}{
		"type":    "command",
		"id":      updateCmdID,
		"command": "update",
		"params":  map[string]interface{}{"reboot": reboot, "security_only": securityOnly},
	}

	updateCh := s.hub.RegisterResponseWaiter(updateCmdID)
	defer s.hub.UnregisterResponseWaiter(updateCmdID)

	updateJSON, _ := json.Marshal(updateCmd)
	if err := s.hub.SendToAgent(agentID, updateJSON); err != nil {
		return nil, fmt.Errorf("Update-Command senden fehlgeschlagen: %w", err)
	}

	var updateResult map[string]interface{}
	select {
	case resp := <-updateCh:
		respStatus, _ := resp["status"].(string)
		if respStatus != "ok" {
			errMsg, _ := resp["error"].(string)
			return resp, fmt.Errorf("Update fehlgeschlagen: %s", errMsg)
		}
		updateResult = resp
	case <-time.After(900 * time.Second):
		return nil, fmt.Errorf("Timeout beim Update")
	}

	result := map[string]interface{}{
		"message":       "Update ausgefuehrt",
		"check_result":  checkResult,
		"update_result": updateResult,
		"reboot":        reboot,
	}

	// Health Check: wait for agent to come back online
	if healthCheck {
		timeout := time.Duration(healthCheckTimeout) * time.Second
		if timeout <= 0 {
			timeout = 600 * time.Second
		}
		log.Printf("Scheduler: Health Check fuer Agent %s (Timeout: %v)", agentID, timeout)

		// If no reboot, agent should still be connected — give it a moment for post-update restart
		if !reboot {
			time.Sleep(15 * time.Second)
		}

		checkedAt := time.Now().UTC().Format(time.RFC3339)
		agentOnline := s.hub.WaitForAgentOnline(agentID, timeout)
		result["health_check"] = map[string]interface{}{
			"agent_online": agentOnline,
			"checked_at":   checkedAt,
		}

		if !agentOnline {
			result["message"] = "Update ausgefuehrt, aber Agent nicht wieder online (Health Check fehlgeschlagen)"
			log.Printf("Scheduler: Health Check fehlgeschlagen — Agent %s nach %v nicht online", agentID, timeout)
			// Return result but signal warning via error
			return result, fmt.Errorf("Health Check: Agent nach %v nicht online", timeout)
		}
		log.Printf("Scheduler: Health Check OK — Agent %s wieder online", agentID)
	}

	return result, nil
}

// WebhookPayload ist der strukturierte Payload fuer ERP-Serviceberichte
type WebhookPayload struct {
	CustomerID      string `json:"customer_id"`
	Hostname        string `json:"hostname"`
	OPNsenseVersion string `json:"opnsense_version"`
	Timestamp       string `json:"timestamp"`
	Trigger         string `json:"trigger"`
	Reachable       bool   `json:"reachable"`
	UpdateStatus    string `json:"update_status"`
}

func (s *TaskScheduler) sendWebhook(taskID int, url string, task *models.Task) {
	// Agent-Daten aus DB holen (hostname, opnsense_version)
	agent, _, err := s.db.GetAgent(task.AgentID)
	if err != nil || agent == nil {
		log.Printf("Scheduler: Webhook Task #%d — Agent %s nicht gefunden: %v", taskID, task.AgentID, err)
		s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Agent nicht gefunden: %v", err))
		return
	}

	// Erreichbarkeit aus Health-Check-Result extrahieren
	reachable := true
	if result, ok := task.Result.(map[string]interface{}); ok {
		if hc, ok := result["health_check"].(map[string]interface{}); ok {
			if online, ok := hc["agent_online"].(bool); ok {
				reachable = online
			}
		}
	}

	// Trigger aus Action ableiten
	trigger := task.Action + "_completed"

	// Timestamp: FinishedAt oder jetzt
	timestamp := time.Now().UTC().Format(time.RFC3339)
	if task.FinishedAt != nil && *task.FinishedAt != "" {
		timestamp = *task.FinishedAt
	}

	payload := WebhookPayload{
		CustomerID:      task.CustomerNumber,
		Hostname:        agent.Hostname,
		OPNsenseVersion: agent.OPNsenseVersion,
		Timestamp:       timestamp,
		Trigger:         trigger,
		Reachable:       reachable,
		UpdateStatus:    task.Status,
	}

	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		log.Printf("Scheduler: Webhook-Payload fuer Task #%d serialisieren fehlgeschlagen: %v", taskID, err)
		s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("JSON-Fehler: %v", err))
		return
	}

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Post(url, "application/json", bytes.NewReader(payloadJSON))
	if err != nil {
		log.Printf("Scheduler: Webhook fuer Task #%d fehlgeschlagen: %v", taskID, err)
		s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Fehler: %v", err))
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
	response := fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body))

	sent := resp.StatusCode >= 200 && resp.StatusCode < 300
	s.db.SetTaskWebhookResult(taskID, sent, response)
	log.Printf("Scheduler: Webhook fuer Task #%d gesendet (Status: %d)", taskID, resp.StatusCode)
}
