package main

// Empyrion bridge proxy. Panel controller forwards calls to the per-agent
// `panel-empyrion-bridge` HTTP service so the dashboard's EAH tab can drive
// it without dealing with bridge URLs / tokens / instance↔EPM topology.
//
// Lookup chain:
//   1. instance ID → row in `instances` (gives agent_id + AssignedPorts["eah-api"])
//   2. agent ID    → row in `agents`    (gives LAN IP)
//   3. settings    → empyrion_bridge_url + empyrion_bridge_token
//   4. forward     → <bridge_url>/api/v1/<rest>?host=<lan_ip>&port=<eah-api>
//
// Auth: caller is already inside the panel's session middleware. Bearer
// token to the bridge is added server-side.

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// handleEmpyrionProxy is the catchall for ANY method on
// /api/instances/{id}/empyrion/{rest...}. The {rest...} portion is mapped
// directly to the bridge's /api/v1/{rest} surface.
func (h *httpServer) handleEmpyrionProxy(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	rest := r.PathValue("rest")
	if rest == "" {
		writeError(w, http.StatusBadRequest, "no_path", "empty empyrion subpath")
		return
	}

	target, err := h.resolveEmpyrionTarget(r.Context(), id)
	if err != nil {
		code := http.StatusInternalServerError
		if errors.Is(err, errBridgeNotConfigured) {
			code = http.StatusServiceUnavailable
		} else if errors.Is(err, errInstanceNotFound) {
			code = http.StatusNotFound
		}
		writeError(w, code, "resolve_failed", err.Error())
		return
	}

	q := url.Values{}
	q.Set("host", target.host)
	q.Set("port", fmt.Sprintf("%d", target.port))
	// Pass any additional caller query params through (e.g. ?prio=, ?days=).
	for k, vs := range r.URL.Query() {
		for _, v := range vs {
			q.Add(k, v)
		}
	}

	upstream := strings.TrimRight(target.bridgeURL, "/") + "/api/v1/" + strings.TrimLeft(rest, "/") + "?" + q.Encode()

	req, err := http.NewRequestWithContext(r.Context(), r.Method, upstream, r.Body)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "build_req", err.Error())
		return
	}
	if ct := r.Header.Get("Content-Type"); ct != "" {
		req.Header.Set("Content-Type", ct)
	}
	if target.bridgeToken != "" {
		req.Header.Set("Authorization", "Bearer "+target.bridgeToken)
	}

	// No client-side timeout: SSE streams /events for as long as the caller
	// stays connected. Cancellation rides r.Context() — when the caller's
	// request context cancels, the upstream request also unwinds.
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		writeError(w, http.StatusBadGateway, "upstream", err.Error())
		return
	}
	defer resp.Body.Close()

	for k, vs := range resp.Header {
		for _, v := range vs {
			w.Header().Add(k, v)
		}
	}
	w.WriteHeader(resp.StatusCode)
	// Stream-friendly copy: flush after each write so SSE chunks reach the
	// client without buffering. http.NewResponseController's Flush is a no-op
	// on responses that don't support it.
	flusher, _ := w.(http.Flusher)
	buf := make([]byte, 4096)
	for {
		n, rerr := resp.Body.Read(buf)
		if n > 0 {
			if _, werr := w.Write(buf[:n]); werr != nil {
				return
			}
			if flusher != nil {
				flusher.Flush()
			}
		}
		if rerr != nil {
			return
		}
	}
}

// --- target resolution ---

type empyrionTarget struct {
	bridgeURL   string
	bridgeToken string
	host        string
	port        int
}

var (
	errBridgeNotConfigured = errors.New("empyrion bridge not configured (set bridge URL via Settings)")
	errInstanceNotFound    = errors.New("instance not found")
	errNoEAHPort           = errors.New("instance has no EAH api port assigned (is this an empyrion instance with eah-api in module.yaml?)")
	errNoAgentLANIP        = errors.New("agent has no LAN IP set (configure via Settings → Agents)")
)

func (h *httpServer) resolveEmpyrionTarget(ctx context.Context, instanceID string) (*empyrionTarget, error) {
	settings, err := h.db.ListSettings(ctx)
	if err != nil {
		return nil, err
	}
	bridgeURL := strings.TrimRight(strings.TrimSpace(settings["empyrion_bridge_url"]), "/")
	if bridgeURL == "" {
		return nil, errBridgeNotConfigured
	}

	rows, err := h.db.ListInstances(ctx, "")
	if err != nil {
		return nil, err
	}
	var inst struct {
		AgentID    string
		EAHAPIPort int
	}
	found := false
	for _, row := range rows {
		if row.InstanceID != instanceID {
			continue
		}
		inst.AgentID = row.AgentID
		if p, ok := row.AssignedPorts["eah-api"]; ok && p > 0 {
			inst.EAHAPIPort = int(p)
		}
		found = true
		break
	}
	if !found {
		return nil, errInstanceNotFound
	}
	if inst.EAHAPIPort == 0 {
		return nil, errNoEAHPort
	}

	lanIP, err := h.db.GetAgentLANIP(ctx, inst.AgentID)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(lanIP) == "" {
		return nil, errNoAgentLANIP
	}

	return &empyrionTarget{
		bridgeURL:   bridgeURL,
		bridgeToken: strings.TrimSpace(settings["empyrion_bridge_token"]),
		host:        lanIP,
		port:        inst.EAHAPIPort,
	}, nil
}

// --- bridge admin settings ---

func (h *httpServer) empyrionBridgeGetSettings(w http.ResponseWriter, r *http.Request) {
	settings, err := h.db.ListSettings(r.Context())
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"url":       settings["empyrion_bridge_url"],
		"token_set": settings["empyrion_bridge_token"] != "",
	})
}

type empyrionBridgeSettingsReq struct {
	URL   *string `json:"url,omitempty"`
	Token *string `json:"token,omitempty"`
}

func (h *httpServer) empyrionBridgeSetSettings(w http.ResponseWriter, r *http.Request) {
	var req empyrionBridgeSettingsReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	ctx := r.Context()
	if req.URL != nil {
		u := strings.TrimRight(strings.TrimSpace(*req.URL), "/")
		if u != "" && !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
			writeError(w, http.StatusBadRequest, "bad_url", "url must start with http:// or https://")
			return
		}
		if err := h.db.SetSetting(ctx, "empyrion_bridge_url", u); err != nil {
			writeError(w, http.StatusInternalServerError, "db", err.Error())
			return
		}
	}
	if req.Token != nil {
		if err := h.db.SetSetting(ctx, "empyrion_bridge_token", strings.TrimSpace(*req.Token)); err != nil {
			writeError(w, http.StatusInternalServerError, "db", err.Error())
			return
		}
	}
	h.empyrionBridgeGetSettings(w, r)
}

// fireEmpyrionScheduledAction is called by the scheduler executor for
// Action.Type == "empyrion". Subop is one of:
//   "broadcast" — body is the message text (server-wide alert)
//   "save"      — no body (in-process save)
//   "console"   — body is the raw console line
// All actions are POST to the bridge with text/plain bodies, returning
// the bridge's response body as the schedule's detail string.
func fireEmpyrionScheduledAction(ctx context.Context, tgt *empyrionTarget, subop, body string) (string, error) {
	subop = strings.TrimSpace(subop)
	if subop == "" {
		return "", errors.New("empyrion subop is required (broadcast/save/console)")
	}
	switch subop {
	case "broadcast", "save", "console":
		// allowed
	default:
		return "", errors.New("unknown empyrion subop: " + subop)
	}

	q := url.Values{}
	q.Set("host", tgt.host)
	q.Set("port", fmt.Sprintf("%d", tgt.port))
	upstream := strings.TrimRight(tgt.bridgeURL, "/") + "/api/v1/" + subop + "?" + q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstream, strings.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "text/plain")
	if tgt.bridgeToken != "" {
		req.Header.Set("Authorization", "Bearer "+tgt.bridgeToken)
	}
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return "", fmt.Errorf("bridge returned %d: %s", resp.StatusCode, string(out))
	}
	if len(out) > 240 {
		return string(out[:240]) + "…", nil
	}
	return string(out), nil
}

// empyrionBridgeTest pings the bridge's /healthz with the configured creds
// to confirm reachability. Useful first-step in the Settings UI.
func (h *httpServer) empyrionBridgeTest(w http.ResponseWriter, r *http.Request) {
	settings, err := h.db.ListSettings(r.Context())
	if err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}
	bridgeURL := strings.TrimRight(strings.TrimSpace(settings["empyrion_bridge_url"]), "/")
	if bridgeURL == "" {
		writeError(w, http.StatusBadRequest, "not_configured", "set bridge URL first")
		return
	}
	req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, bridgeURL+"/healthz", nil)
	if t := strings.TrimSpace(settings["empyrion_bridge_token"]); t != "" {
		req.Header.Set("Authorization", "Bearer "+t)
	}
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		writeError(w, http.StatusBadGateway, "unreachable", err.Error())
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(body)
}
