// Package transport provides the low-level QUIC+TLS connection to a quicproquo server
// and bootstraps the Cap'n Proto RPC client over a single bidirectional QUIC stream.
package transport

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"
	"time"

	capnp "capnproto.org/go/capnp/v3"
	"capnproto.org/go/capnp/v3/rpc"
	"github.com/quic-go/quic-go"

	"quicproquo.dev/sdk/go/proto/node"
)

// ConnectOptions configures the connection to a quicproquo server.
type ConnectOptions struct {
	// Addr is the host:port of the server (e.g. "127.0.0.1:5001").
	Addr string

	// InsecureSkipVerify disables TLS certificate verification (dev mode only).
	InsecureSkipVerify bool

	// CACertPath is the path to a PEM-encoded CA certificate for production use.
	// When set, the server's certificate is verified against this CA.
	CACertPath string

	// ConnectTimeout is the maximum time to wait for the QUIC handshake.
	// Defaults to 10 seconds if zero.
	ConnectTimeout time.Duration
}

// Connection holds the QUIC connection, Cap'n Proto RPC state, and the
// bootstrapped NodeService client.
type Connection struct {
	quicConn *quic.Conn
	stream   *quic.Stream
	rpcConn  *rpc.Conn
	client   node.NodeService
}

// Connect establishes a QUIC+TLS 1.3 connection to the server, opens a single
// bidirectional stream, and bootstraps the Cap'n Proto RPC NodeService client.
func Connect(ctx context.Context, opts ConnectOptions) (*Connection, error) {
	tlsCfg, err := buildTLSConfig(opts)
	if err != nil {
		return nil, fmt.Errorf("transport: build TLS config: %w", err)
	}

	quicCfg := &quic.Config{
		MaxIdleTimeout:  300 * time.Second,
		KeepAlivePeriod: 30 * time.Second,
	}

	timeout := opts.ConnectTimeout
	if timeout == 0 {
		timeout = 10 * time.Second
	}
	dialCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	qConn, err := quic.DialAddr(dialCtx, opts.Addr, tlsCfg, quicCfg)
	if err != nil {
		return nil, fmt.Errorf("transport: QUIC dial %s: %w", opts.Addr, err)
	}

	stream, err := qConn.OpenStream()
	if err != nil {
		qConn.CloseWithError(1, "failed to open stream")
		return nil, fmt.Errorf("transport: open bidi stream: %w", err)
	}

	capnpTransport := rpc.NewStreamTransport(stream)
	rpcConn := rpc.NewConn(capnpTransport, nil)

	client := node.NodeService(rpcConn.Bootstrap(ctx))

	return &Connection{
		quicConn: qConn,
		stream:   stream,
		rpcConn:  rpcConn,
		client:   client,
	}, nil
}

// Client returns the bootstrapped NodeService RPC client.
func (c *Connection) Client() node.NodeService {
	return c.client
}

// Close gracefully shuts down the RPC connection and underlying QUIC transport.
func (c *Connection) Close() error {
	capnp.Client(c.client).Release()
	c.rpcConn.Close()
	return c.quicConn.CloseWithError(0, "client closed")
}

func buildTLSConfig(opts ConnectOptions) (*tls.Config, error) {
	cfg := &tls.Config{
		NextProtos: []string{"capnp"},
		MinVersion: tls.VersionTLS13,
	}

	if opts.InsecureSkipVerify {
		cfg.InsecureSkipVerify = true
		return cfg, nil
	}

	if opts.CACertPath != "" {
		caPEM, err := os.ReadFile(opts.CACertPath)
		if err != nil {
			return nil, fmt.Errorf("read CA cert %s: %w", opts.CACertPath, err)
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(caPEM) {
			return nil, fmt.Errorf("CA cert %s: no valid PEM certificates found", opts.CACertPath)
		}
		cfg.RootCAs = pool
	}

	return cfg, nil
}
