package commerce

import (
	"context"
	"crypto/rand"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"nhooyr.io/websocket"

	redisstore "qycw/go-server/internal/redis"
)

const vncTicketTTL = 60 * time.Second

type vncTicketData struct {
	UserID     int64  `json:"user_id"`
	InstanceID int64  `json:"instance_id"`
	UpstreamID *int64 `json:"upstream_id,omitempty"`
	VNCURL     string `json:"vnc_url"`
	VNCPass    string `json:"vnc_pass"`
}

type VNCBridgeHandler struct {
	service *Service
	logger  *slog.Logger
}

func NewVNCBridgeHandler(service *Service, logger *slog.Logger) *VNCBridgeHandler {
	return &VNCBridgeHandler{service: service, logger: logger}
}

func (h *VNCBridgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ticket := strings.TrimSpace(chi.URLParam(r, "ticket"))
	if ticket == "" {
		http.Error(w, "missing ticket", http.StatusBadRequest)
		return
	}
	if h.service.redis == nil {
		http.Error(w, "vnc ticket store unavailable", http.StatusServiceUnavailable)
		return
	}

	data, err := h.service.consumeVNCTicket(r.Context(), ticket)
	if err != nil {
		if h.logger != nil {
			h.logger.Warn("consume vnc ticket failed", "error", err)
		}
		http.Error(w, "invalid or expired ticket", http.StatusUnauthorized)
		return
	}
	if data.VNCURL == "" {
		http.Error(w, "vnc url missing", http.StatusBadRequest)
		return
	}

	clientConn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
		Subprotocols:       []string{"binary"},
		InsecureSkipVerify: true,
	})
	if err != nil {
		return
	}
	defer clientConn.Close(websocket.StatusNormalClosure, "")

	dialCtx, cancelDial := context.WithTimeout(r.Context(), 8*time.Second)
	defer cancelDial()
	upstreamConn, _, err := websocket.Dial(dialCtx, data.VNCURL, &websocket.DialOptions{
		Subprotocols: []string{"binary"},
		HTTPClient: &http.Client{
			Timeout: 12 * time.Second,
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // upstream consoles often use self-signed certificates.
			},
		},
	})
	if err != nil {
		if h.logger != nil {
			h.logger.Warn("vnc upstream websocket connect failed", "instance_id", data.InstanceID, "error", err)
		}
		clientConn.Close(websocket.StatusInternalError, "upstream connect failed")
		return
	}
	defer upstreamConn.Close(websocket.StatusNormalClosure, "")

	if h.logger != nil {
		h.logger.Info("vnc bridge opened", "instance_id", data.InstanceID)
	}
	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	errCh := make(chan error, 2)
	go func() {
		errCh <- proxyWebSocket(ctx, clientConn, upstreamConn)
		cancel()
	}()
	go func() {
		errCh <- proxyWebSocket(ctx, upstreamConn, clientConn)
		cancel()
	}()
	<-errCh
	if h.logger != nil {
		h.logger.Info("vnc bridge closed", "instance_id", data.InstanceID)
	}
}

func proxyWebSocket(ctx context.Context, src *websocket.Conn, dst *websocket.Conn) error {
	for {
		messageType, data, err := src.Read(ctx)
		if err != nil {
			return err
		}
		if err := dst.Write(ctx, messageType, data); err != nil {
			return err
		}
	}
}

func (s *Service) issueVNCTicket(ctx context.Context, userID int64, instance Instance, payload map[string]any) (map[string]any, error) {
	vncURL := strings.TrimSpace(firstString(payload["vnc_url"], payload["url"], payload["ws_url"], payload["websocket_url"]))
	if vncURL == "" {
		return nil, HTTPError{Status: http.StatusBadGateway, Detail: "上游未返回 vnc_url"}
	}
	if strings.HasPrefix(vncURL, "http://") || strings.HasPrefix(vncURL, "https://") {
		return map[string]any{
			"ticket":          "",
			"expires_in":      0,
			"vnc_pass":        firstString(payload["vnc_pass"], payload["password"]),
			"password":        payload["password"],
			"hostname":        instance.Hostname,
			"instance_id":     instance.ID,
			"direct_url":      vncURL,
			"connection_type": "direct_url",
			"provider":        providerNameForInstancePayload(payload),
		}, nil
	}
	if s.redis == nil {
		return nil, ServiceUnavailable("VNC WebSocket 需要 Redis 保存一次性 ticket")
	}
	ticket, err := randomURLToken(24)
	if err != nil {
		return nil, err
	}
	vncPass := firstString(payload["vnc_pass"], payload["password"])
	raw, err := json.Marshal(vncTicketData{
		UserID:     userID,
		InstanceID: instance.ID,
		UpstreamID: instance.UpstreamID,
		VNCURL:     vncURL,
		VNCPass:    vncPass,
	})
	if err != nil {
		return nil, err
	}
	if err := s.redis.Set(ctx, vncTicketKey(ticket), raw, vncTicketTTL).Err(); err != nil {
		return nil, err
	}
	return map[string]any{
		"ticket":          ticket,
		"expires_in":      int(vncTicketTTL.Seconds()),
		"vnc_pass":        vncPass,
		"password":        payload["password"],
		"hostname":        instance.Hostname,
		"instance_id":     instance.ID,
		"provider":        providerNameForInstancePayload(payload),
		"connection_type": "websocket",
	}, nil
}

func (s *Service) consumeVNCTicket(ctx context.Context, ticket string) (vncTicketData, error) {
	raw, err := s.redis.GetDel(ctx, vncTicketKey(ticket)).Bytes()
	if err != nil {
		return vncTicketData{}, err
	}
	var data vncTicketData
	if err := json.Unmarshal(raw, &data); err != nil {
		return vncTicketData{}, err
	}
	if data.VNCURL == "" {
		return vncTicketData{}, errors.New("vnc_url missing")
	}
	return data, nil
}

func vncTicketKey(ticket string) string {
	return redisstore.Key("vnc_ticket", ticket)
}

func randomURLToken(bytesLen int) (string, error) {
	if bytesLen <= 0 {
		return "", fmt.Errorf("invalid token length")
	}
	raw := make([]byte, bytesLen)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

func providerNameForInstancePayload(payload map[string]any) string {
	provider := strings.TrimSpace(firstString(payload["provider"]))
	if provider == "" {
		return "dcim_cloud"
	}
	return provider
}
