package handler

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"sync"
	"time"

	"github.com/go-chi/chi/v5"
)

// ---------------------------------------------------------------------------
// In-memory ping store
// ---------------------------------------------------------------------------

// PingStatus represents the lifecycle of a runtime ping test.
type PingStatus string

const (
	PingPending   PingStatus = "pending"
	PingRunning   PingStatus = "running"
	PingCompleted PingStatus = "completed"
	PingFailed    PingStatus = "failed"
	PingTimeout   PingStatus = "timeout"
)

// PingRequest represents a pending or completed ping test.
type PingRequest struct {
	ID        string     `json:"id"`
	RuntimeID string     `json:"runtime_id"`
	Status    PingStatus `json:"status"`
	Output    string     `json:"output,omitempty"`
	Error     string     `json:"error,omitempty"`
	DurationMs int64    `json:"duration_ms,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt time.Time  `json:"updated_at"`
}

// PingStore is a thread-safe in-memory store for ping requests.
// Pings expire after 2 minutes.
type PingStore struct {
	mu    sync.Mutex
	pings map[string]*PingRequest // keyed by ping ID
}

func NewPingStore() *PingStore {
	return &PingStore{
		pings: make(map[string]*PingRequest),
	}
}

func (s *PingStore) Create(runtimeID string) *PingRequest {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Clean up old pings for this runtime
	for id, p := range s.pings {
		if time.Since(p.CreatedAt) > 2*time.Minute {
			delete(s.pings, id)
		}
	}

	ping := &PingRequest{
		ID:        randomID(),
		RuntimeID: runtimeID,
		Status:    PingPending,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
	}
	s.pings[ping.ID] = ping
	return ping
}

func (s *PingStore) Get(id string) *PingRequest {
	s.mu.Lock()
	defer s.mu.Unlock()

	p, ok := s.pings[id]
	if !ok {
		return nil
	}
	// Check for timeout
	if p.Status == PingPending && time.Since(p.CreatedAt) > 60*time.Second {
		p.Status = PingTimeout
		p.Error = "daemon did not respond within 60 seconds"
		p.UpdatedAt = time.Now()
	}
	return p
}

// PopPending returns and removes the oldest pending ping for a runtime.
func (s *PingStore) PopPending(runtimeID string) *PingRequest {
	s.mu.Lock()
	defer s.mu.Unlock()

	var oldest *PingRequest
	for _, p := range s.pings {
		if p.RuntimeID == runtimeID && p.Status == PingPending {
			if oldest == nil || p.CreatedAt.Before(oldest.CreatedAt) {
				oldest = p
			}
		}
	}
	if oldest != nil {
		oldest.Status = PingRunning
		oldest.UpdatedAt = time.Now()
	}
	return oldest
}

func (s *PingStore) Complete(id string, output string, durationMs int64) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if p, ok := s.pings[id]; ok {
		p.Status = PingCompleted
		p.Output = output
		p.DurationMs = durationMs
		p.UpdatedAt = time.Now()
	}
}

func (s *PingStore) Fail(id string, errMsg string, durationMs int64) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if p, ok := s.pings[id]; ok {
		p.Status = PingFailed
		p.Error = errMsg
		p.DurationMs = durationMs
		p.UpdatedAt = time.Now()
	}
}

func randomID() string {
	b := make([]byte, 16)
	rand.Read(b)
	return hex.EncodeToString(b)
}

// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------

// InitiatePing creates a new ping request (protected route, called by frontend).
func (h *Handler) InitiatePing(w http.ResponseWriter, r *http.Request) {
	runtimeID := chi.URLParam(r, "runtimeId")

	rt, err := h.Queries.GetAgentRuntime(r.Context(), parseUUID(runtimeID))
	if err != nil {
		writeError(w, http.StatusNotFound, "runtime not found")
		return
	}

	if _, ok := h.requireWorkspaceMember(w, r, uuidToString(rt.WorkspaceID), "runtime not found"); !ok {
		return
	}

	ping := h.PingStore.Create(runtimeID)
	writeJSON(w, http.StatusOK, ping)
}

// GetPing returns the status of a ping request (protected route, called by frontend).
func (h *Handler) GetPing(w http.ResponseWriter, r *http.Request) {
	pingID := chi.URLParam(r, "pingId")

	ping := h.PingStore.Get(pingID)
	if ping == nil {
		writeError(w, http.StatusNotFound, "ping not found")
		return
	}

	writeJSON(w, http.StatusOK, ping)
}

// ReportPingResult receives the ping result from the daemon.
func (h *Handler) ReportPingResult(w http.ResponseWriter, r *http.Request) {
	pingID := chi.URLParam(r, "pingId")

	var req struct {
		Status     string `json:"status"` // "completed" or "failed"
		Output     string `json:"output"`
		Error      string `json:"error"`
		DurationMs int64  `json:"duration_ms"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	if req.Status == "completed" {
		h.PingStore.Complete(pingID, req.Output, req.DurationMs)
	} else {
		h.PingStore.Fail(pingID, req.Error, req.DurationMs)
	}

	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
