package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// Region Medic controller endpoints. The agent does the .7rg validation +
// surgical heal; the controller orchestrates the lifecycle (stop → heal →
// start, reusing the graceful-save stop) and relays scan results to the UI.
//
//   GET  /api/instances/{id}/region-health
//   POST /api/instances/{id}/region-health/{region}/heal
//   POST /api/instances/{id}/region-health/mode   {"mode":"alert"|"auto"}

// regionHealMode stores the per-instance alert/auto-heal preference. MVP: held
// in memory and surfaced in the UI; the auto-heal watcher that consumes it is a
// follow-up. instanceID → "alert" | "auto".
var regionHealMode sync.Map

// resolveInstanceConn maps an instance id to its DB row + connected agent conn.
func (h *httpServer) resolveInstanceConn(ctx context.Context, id string) (*panelv1.ControllerEnvelope, *agentConn, error) {
	rows, err := h.db.ListInstances(ctx, "")
	if err != nil {
		return nil, nil, fmt.Errorf("db: %w", err)
	}
	for i := range rows {
		if rows[i].InstanceID == id {
			conn := h.registry.get(rows[i].AgentID)
			if conn == nil {
				return nil, nil, fmt.Errorf("agent %q not connected", rows[i].AgentID)
			}
			return nil, conn, nil
		}
	}
	return nil, nil, fmt.Errorf("instance %q not found", id)
}

func (h *httpServer) sendRegionScan(ctx context.Context, conn *agentConn, instanceID string) (*panelv1.RegionScanResult, error) {
	corrID := newCorrelationID("region-scan")
	env := &panelv1.ControllerEnvelope{
		CorrelationId: corrID, SentAt: timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_RegionScan{RegionScan: &panelv1.RegionScanRequest{InstanceId: instanceID}},
	}
	if err := conn.Send(env); err != nil {
		return nil, err
	}
	reply, err := h.pending.Await(ctx, corrID, 60*time.Second)
	if err != nil {
		return nil, err
	}
	res := reply.GetRegionScanResult()
	if res == nil {
		return nil, fmt.Errorf("agent reply missing region_scan_result")
	}
	if res.Error != nil && res.Error.Message != "" {
		return nil, fmt.Errorf("%s: %s", res.Error.Code, res.Error.Message)
	}
	return res, nil
}

func (h *httpServer) sendRegionHeal(ctx context.Context, conn *agentConn, instanceID, region string) (*panelv1.RegionHealResult, error) {
	corrID := newCorrelationID("region-heal")
	env := &panelv1.ControllerEnvelope{
		CorrelationId: corrID, SentAt: timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_RegionHeal{RegionHeal: &panelv1.RegionHealRequest{InstanceId: instanceID, Region: region}},
	}
	if err := conn.Send(env); err != nil {
		return nil, err
	}
	reply, err := h.pending.Await(ctx, corrID, 120*time.Second)
	if err != nil {
		return nil, err
	}
	res := reply.GetRegionHealResult()
	if res == nil {
		return nil, fmt.Errorf("agent reply missing region_heal_result")
	}
	if res.Error != nil && res.Error.Message != "" {
		return nil, fmt.Errorf("%s: %s", res.Error.Code, res.Error.Message)
	}
	return res, nil
}

// GET /api/instances/{id}/region-health
func (h *httpServer) getRegionHealth(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	_, conn, err := h.resolveInstanceConn(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusNotFound, "resolve", err.Error())
		return
	}
	scan, err := h.sendRegionScan(r.Context(), conn, id)
	if err != nil {
		writeError(w, http.StatusBadGateway, "scan_failed", err.Error())
		return
	}
	mode := "alert"
	if v, ok := regionHealMode.Load(id); ok {
		mode = v.(string)
	}
	regions := make([]map[string]any, 0, len(scan.Affected))
	for _, a := range scan.Affected {
		issue := ""
		if a.FileCorrupt {
			issue = fmt.Sprintf("region file corrupt (%d bad chunks)", a.BadChunks)
		} else if a.ErrorBackups > 0 {
			issue = fmt.Sprintf("%d salvage file(s) — chunks failed to load", a.ErrorBackups)
		}
		regions = append(regions, map[string]any{
			"name":          a.Region,
			"error_backups": a.ErrorBackups,
			"file_present":  a.FilePresent,
			"file_corrupt":  a.FileCorrupt,
			"bad_chunks":    a.BadChunks,
			"issue":         issue,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"mode":          mode,
		"error_backups": scan.ErrorBackups,
		"region_dir":    scan.RegionDir,
		"regions":       regions,
	})
}

// POST /api/instances/{id}/region-health/{region}/heal
func (h *httpServer) postRegionHeal(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	region := r.PathValue("region")
	if region == "" {
		writeError(w, http.StatusBadRequest, "missing_region", "region required")
		return
	}
	_, conn, err := h.resolveInstanceConn(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusNotFound, "resolve", err.Error())
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 200*time.Second)
	defer cancel()

	// 1. Stop (graceful save runs inside the agent stop path).
	if err := h.awaitAgentCall(ctx, conn, "stop", &panelv1.ControllerEnvelope_InstanceStop{
		InstanceStop: &panelv1.InstanceStop{InstanceId: id, GraceSeconds: 30},
	}); err != nil {
		writeError(w, http.StatusBadGateway, "stop_failed", err.Error())
		return
	}
	// 2. Heal (file swap on the stopped instance).
	res, healErr := h.sendRegionHeal(ctx, conn, id, region)
	// 3. Always restart, even if the heal failed, so we never leave the server down.
	startErr := h.awaitAgentCall(ctx, conn, "start", &panelv1.ControllerEnvelope_InstanceStart{
		InstanceStart: &panelv1.InstanceStart{InstanceId: id},
	})
	if healErr != nil {
		writeError(w, http.StatusBadGateway, "heal_failed", healErr.Error())
		return
	}
	if startErr != nil {
		writeError(w, http.StatusBadGateway, "restart_failed", "healed but restart failed: "+startErr.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"region":        res.Region,
		"source_backup": res.SourceBackup,
		"bytes_written": res.BytesWritten,
		"snapshot_path": res.SnapshotPath,
		"healed":        res.Healed,
	})
}

// POST /api/instances/{id}/region-health/mode  {"mode":"alert"|"auto"}
func (h *httpServer) postRegionHealMode(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	var body struct {
		Mode string `json:"mode"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		writeError(w, http.StatusBadRequest, "bad_body", err.Error())
		return
	}
	if body.Mode != "alert" && body.Mode != "auto" {
		writeError(w, http.StatusBadRequest, "bad_mode", "mode must be 'alert' or 'auto'")
		return
	}
	regionHealMode.Store(id, body.Mode)
	writeJSON(w, http.StatusOK, map[string]any{"mode": body.Mode})
}
