package httpapi

import (
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"time"

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

	"github.com/wangjia/pangolin/server/internal/apierr"
	"github.com/wangjia/pangolin/server/internal/auth"
	"github.com/wangjia/pangolin/server/internal/nodes"
	agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
)

const (
	// paidCredentialTTL is the default connect credential lifetime for paid users.
	paidCredentialTTL = 24 * time.Hour
	// freeCredentialTTL is the per-minute TTL for free users (per remaining minutes).
	freeMinuteTTL = time.Minute
)

// NodeAPI serves the /v1/nodes endpoints.
type NodeAPI struct {
	store     nodes.NodeStore
	hub       *nodes.Hub
	deriveKey string
	// rulesBaseURL 是控制面对外公网基址(PANGOLIN_PUBLIC_URL),供国内分流的
	// rule_set .srs 下载用;空则分流不生效。
	rulesBaseURL string
}

// NewNodeAPI creates a NodeAPI.
func NewNodeAPI(store nodes.NodeStore, hub *nodes.Hub, deriveKey, rulesBaseURL string) *NodeAPI {
	return &NodeAPI{store: store, hub: hub, deriveKey: deriveKey, rulesBaseURL: rulesBaseURL}
}

// ─── GET /v1/nodes ───────────────────────────────────────────────────────────

type nodeResponse struct {
	ID     string `json:"id"`     // UUID (used as path param for connect)
	Region string `json:"region"` // HK / JP / SG / US
	NameZH string `json:"name_zh"`
	NameEN string `json:"name_en"`
	Tier   string `json:"tier"`   // "free" | "pro"
	Status string `json:"status"` // always "up" in this endpoint
	// host/port 暴露节点入口,供客户端实测真实延迟(per-client ping 服务端无法代知)。
	Host string `json:"host"`
	Port int    `json:"port"`
}

// ListNodes handles GET /v1/nodes.
func (a *NodeAPI) ListNodes(w http.ResponseWriter, r *http.Request) {
	nodeRows, err := a.store.ListUp(r.Context())
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	resp := make([]nodeResponse, 0, len(nodeRows))
	for _, n := range nodeRows {
		host, portStr := splitHostPort(n.Endpoint)
		if host == "" {
			host = n.Endpoint
		}
		port, _ := strconv.Atoi(portStr)
		resp = append(resp, nodeResponse{
			ID:     n.UUID,
			Region: n.Region,
			NameZH: n.NameZH,
			NameEN: n.NameEN,
			Tier:   n.Tier,
			Status: n.Status,
			Host:   host,
			Port:   port,
		})
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]any{"nodes": resp})
}

// ─── POST /v1/nodes/{id}/connect ─────────────────────────────────────────────

type connectRequest struct {
	DeviceID string `json:"device_id"`
}

// ConnectNode handles POST /v1/nodes/{id}/connect.
func (a *NodeAPI) ConnectNode(w http.ResponseWriter, r *http.Request) {
	uid, ok := auth.UserIDFromContext(r.Context())
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}

	nodeUUID := chi.URLParam(r, "id")
	if nodeUUID == "" {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	var req connectRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8*1024)).Decode(&req); err != nil || strings.TrimSpace(req.DeviceID) == "" {
		apierr.WriteJSON(w, http.StatusBadRequest, &apierr.Error{
			Code:      "BAD_REQUEST",
			MessageZH: "缺少 device_id",
			MessageEn: "Missing device_id",
		})
		return
	}

	// 1. Load user entitlement (dp_uuid + plan).
	ent, err := a.store.EntitlementForUser(r.Context(), uid)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if ent == nil {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}

	// 2. Determine TTL from plan.
	var ttl time.Duration
	if ent.AdGate {
		// Free plan: flat 10-minute session for MVP (full ad-gate in a later pass).
		dm := int64(10)
		if ent.DailyMinutes.Valid {
			dm = ent.DailyMinutes.Int64
		}
		if dm <= 0 {
			apierr.WriteJSON(w, http.StatusForbidden, apierr.ErrQuotaExhausted)
			return
		}
		ttl = time.Duration(dm) * freeMinuteTTL
	} else {
		ttl = paidCredentialTTL
	}
	expiresAt := time.Now().UTC().Add(ttl)

	// 3. Resolve node.
	node, err := a.store.NodeByUUID(r.Context(), nodeUUID)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if node == nil || node.Status != "up" {
		apierr.WriteJSON(w, http.StatusNotFound, apierr.ErrNotFound)
		return
	}

	// 4. Build the agentv1.Credential.
	cred := &agentv1.Credential{
		DpUUID:        ent.DpUUID,
		Protocol:      agentv1.ProtocolBoth,
		Flow:          "xtls-rprx-vision",
		ExpiresAtUnix: expiresAt.Unix(),
	}

	// 5. Push to the node agent via Hub (real gRPC channel).
	pushErr := a.hub.Push(r.Context(), node.UUID, &agentv1.Command{
		Type:   agentv1.CommandTypeUpsert,
		Upsert: &agentv1.UpsertPayload{Credential: cred},
	})
	if pushErr != nil {
		// Non-fatal: command is queued in Redis and will be replayed on reconnect.
		// Log but don't abort — return the config so the client can attempt the tunnel.
		_ = pushErr
	}

	// 6. Persist credential for agent resync.
	if persistErr := a.store.PersistCredential(r.Context(), node.ID, cred, expiresAt); persistErr != nil {
		// Non-fatal: tunnel still works via Hub push; resync will miss it on agent restart.
		_ = persistErr
	}

	// 7. Render and return the full sing-box CLIENT config JSON.
	//    split_cn=1/true → 国内 IP/域名直连(#5);客户端按 smartRoute 偏好传。
	splitCN := r.URL.Query().Get("split_cn") == "1" || r.URL.Query().Get("split_cn") == "true"
	cfgJSON, renderErr := BuildClientConfig(node, ent.DpUUID, a.deriveKey,
		ClientConfigOpts{SplitCN: splitCN, RulesBaseURL: a.rulesBaseURL})
	if renderErr != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(cfgJSON)
}

// ─── POST /v1/nodes/{id}/disconnect ──────────────────────────────────────────

// DisconnectNode handles POST /v1/nodes/{id}/disconnect.
func (a *NodeAPI) DisconnectNode(w http.ResponseWriter, r *http.Request) {
	uid, ok := auth.UserIDFromContext(r.Context())
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}

	nodeUUID := chi.URLParam(r, "id")
	if nodeUUID == "" {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	// Load dp_uuid.
	ent, err := a.store.EntitlementForUser(r.Context(), uid)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if ent == nil {
		w.WriteHeader(http.StatusNoContent) // nothing to revoke
		return
	}

	node, err := a.store.NodeByUUID(r.Context(), nodeUUID)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if node == nil {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	// Push revoke command.
	_ = a.hub.Push(r.Context(), node.UUID, &agentv1.Command{
		Type:   agentv1.CommandTypeRevoke,
		Revoke: &agentv1.RevokePayload{DpUUID: ent.DpUUID},
	})

	// Delete persisted credential.
	_ = a.store.DeleteCredential(r.Context(), node.ID, ent.DpUUID)

	w.WriteHeader(http.StatusNoContent)
}
