package testenv

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync/atomic"
	"time"

	gatewayauthn "galaxy/gateway/authn"
	gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1"
	"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect"

	"connectrpc.com/connect"
	"github.com/google/uuid"
	"golang.org/x/net/http2"
)

// SignedGatewayClient drives the authenticated edge surface of the
// gateway from tests. It signs ExecuteCommand envelopes with the
// session's Ed25519 private key, verifies response signatures with
// the gateway's response-signer public key, and exposes a
// SubscribeEvents helper. The client speaks Connect over HTTP/2
// cleartext (h2c) — the gateway listener supports that natively
// alongside gRPC and gRPC-Web on the same port.
type SignedGatewayClient struct {
	httpClient *http.Client
	edge       gatewayv1connect.EdgeGatewayClient
	deviceSID  string
	privateKey ed25519.PrivateKey
	respPub    ed25519.PublicKey

	requestSeq uint64
}

// NewSession is the device-session shape returned by registration.
type NewSession struct {
	DeviceSessionID string
	PrivateKey      ed25519.PrivateKey
	PublicKey       ed25519.PublicKey
}

// GenerateSessionKeyPair returns a fresh Ed25519 keypair for use in
// `confirm-email-code`.
func GenerateSessionKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
	return ed25519.GenerateKey(rand.Reader)
}

// EncodePublicKey base64-encodes the raw 32-byte Ed25519 public key
// for the `client_public_key` field.
func EncodePublicKey(pub ed25519.PublicKey) string {
	return base64.StdEncoding.EncodeToString(pub)
}

// DialGateway opens a Connect (HTTP/2 cleartext) client against the
// gateway's authenticated edge listener at addr ("host:port") and
// prepares a signing client bound to deviceSID.
func DialGateway(_ context.Context, addr string, deviceSID string, privateKey ed25519.PrivateKey, respPub ed25519.PublicKey) (*SignedGatewayClient, error) {
	if addr == "" {
		return nil, fmt.Errorf("dial gateway: empty addr")
	}

	httpClient := &http.Client{
		Transport: &http2.Transport{
			AllowHTTP: true,
			DialTLSContext: func(ctx context.Context, network, target string, _ *tls.Config) (net.Conn, error) {
				return (&net.Dialer{}).DialContext(ctx, network, target)
			},
		},
	}
	edge := gatewayv1connect.NewEdgeGatewayClient(httpClient, "http://"+addr)

	return &SignedGatewayClient{
		httpClient: httpClient,
		edge:       edge,
		deviceSID:  deviceSID,
		privateKey: privateKey,
		respPub:    respPub,
	}, nil
}

// Close releases idle HTTP/2 connections held by the underlying transport.
// The Connect client itself is stateless, so this is best-effort.
func (c *SignedGatewayClient) Close() error {
	if c.httpClient != nil {
		if transport, ok := c.httpClient.Transport.(*http2.Transport); ok {
			transport.CloseIdleConnections()
		}
	}
	return nil
}

// ExecuteOptions tunes one ExecuteCommand call. The zero value
// produces a fresh `request_id` and the current timestamp; tests that
// need a fixed request_id (anti-replay) or a stale timestamp
// (freshness window) override the relevant fields.
type ExecuteOptions struct {
	RequestID               string
	TimestampMS             int64
	OverrideSignature       []byte
	OverridePayloadHash     []byte
	OverrideSessionID       string
	OverrideProtocolVersion string
}

// ExecuteResult is the verified response of a successful
// ExecuteCommand. PayloadBytes is the authenticated FlatBuffers
// blob; tests decode it via galaxy/transcoder.
type ExecuteResult struct {
	ResultCode   string
	PayloadBytes []byte
	RequestID    string
	TimestampMS  int64
}

// Execute signs the supplied payload, calls ExecuteCommand, verifies
// the response signature against the gateway response signer, and
// returns the decoded result.
func (c *SignedGatewayClient) Execute(ctx context.Context, messageType string, payload []byte, opts ExecuteOptions) (*ExecuteResult, error) {
	if len(payload) == 0 {
		return nil, errors.New("ExecuteCommand requires non-empty payload")
	}

	requestID := opts.RequestID
	if requestID == "" {
		requestID = uuid.NewString()
	}
	timestampMS := opts.TimestampMS
	if timestampMS == 0 {
		timestampMS = time.Now().UnixMilli()
	}
	protocolVersion := opts.OverrideProtocolVersion
	if protocolVersion == "" {
		protocolVersion = "v1"
	}
	deviceSID := opts.OverrideSessionID
	if deviceSID == "" {
		deviceSID = c.deviceSID
	}

	hash := opts.OverridePayloadHash
	if hash == nil {
		sum := sha256.Sum256(payload)
		hash = sum[:]
	}

	signature := opts.OverrideSignature
	if signature == nil {
		input := gatewayauthn.BuildRequestSigningInput(gatewayauthn.RequestSigningFields{
			ProtocolVersion: protocolVersion,
			DeviceSessionID: deviceSID,
			MessageType:     messageType,
			TimestampMS:     timestampMS,
			RequestID:       requestID,
			PayloadHash:     hash,
		})
		signature = ed25519.Sign(c.privateKey, input)
	}

	req := &gatewayv1.ExecuteCommandRequest{
		ProtocolVersion: protocolVersion,
		DeviceSessionId: deviceSID,
		MessageType:     messageType,
		TimestampMs:     timestampMS,
		RequestId:       requestID,
		PayloadBytes:    payload,
		PayloadHash:     hash,
		Signature:       signature,
	}
	atomic.AddUint64(&c.requestSeq, 1)

	respWrap, err := c.edge.ExecuteCommand(ctx, connect.NewRequest(req))
	if err != nil {
		return nil, err
	}
	resp := respWrap.Msg

	respHash := sha256.Sum256(resp.GetPayloadBytes())
	if string(respHash[:]) != string(resp.GetPayloadHash()) {
		return nil, fmt.Errorf("response payload_hash mismatch")
	}
	if err := gatewayauthn.VerifyResponseSignature(c.respPub, resp.GetSignature(), gatewayauthn.ResponseSigningFields{
		ProtocolVersion: resp.GetProtocolVersion(),
		RequestID:       resp.GetRequestId(),
		TimestampMS:     resp.GetTimestampMs(),
		ResultCode:      resp.GetResultCode(),
		PayloadHash:     resp.GetPayloadHash(),
	}); err != nil {
		return nil, fmt.Errorf("response signature verification failed: %w", err)
	}

	return &ExecuteResult{
		ResultCode:   resp.GetResultCode(),
		PayloadBytes: resp.GetPayloadBytes(),
		RequestID:    resp.GetRequestId(),
		TimestampMS:  resp.GetTimestampMs(),
	}, nil
}

// SubscribeEvents opens the authenticated server-streaming
// SubscribeEvents RPC. The returned channel receives every
// authenticated event the gateway delivers; the channel closes when
// the stream ends or when ctx is done. Errors land on the err
// channel.
func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType string) (<-chan *gatewayv1.GatewayEvent, <-chan error, error) {
	requestID := uuid.NewString()
	timestampMS := time.Now().UnixMilli()
	protocolVersion := "v1"

	emptyHash := sha256.Sum256(nil)
	signature := ed25519.Sign(c.privateKey, gatewayauthn.BuildRequestSigningInput(gatewayauthn.RequestSigningFields{
		ProtocolVersion: protocolVersion,
		DeviceSessionID: c.deviceSID,
		MessageType:     messageType,
		TimestampMS:     timestampMS,
		RequestID:       requestID,
		PayloadHash:     emptyHash[:],
	}))

	stream, err := c.edge.SubscribeEvents(ctx, connect.NewRequest(&gatewayv1.SubscribeEventsRequest{
		ProtocolVersion: protocolVersion,
		DeviceSessionId: c.deviceSID,
		MessageType:     messageType,
		TimestampMs:     timestampMS,
		RequestId:       requestID,
		PayloadHash:     emptyHash[:],
		Signature:       signature,
	}))
	if err != nil {
		return nil, nil, fmt.Errorf("open subscribe events: %w", err)
	}

	events := make(chan *gatewayv1.GatewayEvent, 16)
	errs := make(chan error, 1)
	go func() {
		defer close(events)
		defer func() { _ = stream.Close() }()
		for stream.Receive() {
			events <- stream.Msg()
		}
		errs <- stream.Err()
	}()
	return events, errs, nil
}

// IsUnauthenticated reports whether err carries Connect's
// CodeUnauthenticated, useful for negative-path edge tests.
func IsUnauthenticated(err error) bool {
	return connect.CodeOf(err) == connect.CodeUnauthenticated
}

// IsInvalidArgument reports whether err carries Connect's
// CodeInvalidArgument (used for malformed envelopes and unsupported
// protocol_version).
func IsInvalidArgument(err error) bool {
	return connect.CodeOf(err) == connect.CodeInvalidArgument
}

// IsResourceExhausted reports whether err carries Connect's
// CodeResourceExhausted (used for replay rejection or rate-limit
// rejections).
func IsResourceExhausted(err error) bool {
	return connect.CodeOf(err) == connect.CodeResourceExhausted
}

// IsFailedPrecondition reports whether err carries Connect's
// CodeFailedPrecondition. The gateway uses this code for replay
// rejections (the canonical envelope was authentic but the
// `request_id` was already consumed).
func IsFailedPrecondition(err error) bool {
	return connect.CodeOf(err) == connect.CodeFailedPrecondition
}
