// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package http3

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"sync"

	"golang.org/x/net/quic"
)

// A transport is an HTTP/3 transport.
//
// It does not manage a pool of connections,
// and therefore does not implement net/http.RoundTripper.
//
// TODO: Provide a way to register an HTTP/3 transport with a net/http.transport's
// connection pool.
type transport struct {
	// config is the QUIC configuration used for client connections.
	config *quic.Config

	listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)

	mu sync.Mutex // Guards fields below.
	// endpoint is the QUIC endpoint used by connections created by the
	// transport. If CloseIdleConnections is called when activeConns is empty,
	// endpoint will be unset. If unset, endpoint will be initialized by any
	// call to dial.
	endpoint      *quic.Endpoint
	activeConns   map[*clientConn]struct{}
	inFlightDials int
}

// netHTTPTransport implements the net/http.dialClientConner interface,
// allowing our HTTP/3 transport to integrate with net/http.
type netHTTPTransport struct {
	*transport
}

// RoundTrip is defined since Transport.RegisterProtocol takes in a
// RoundTripper. However, this method will never be used as net/http's
// dialClientConner interface does not have a RoundTrip method and will only
// use DialClientConn to create a new RoundTripper.
func (t netHTTPTransport) RoundTrip(*http.Request) (*http.Response, error) {
	panic("netHTTPTransport.RoundTrip should never be called")
}

func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, _ func()) (http.RoundTripper, error) {
	return t.transport.dial(ctx, addr)
}

type TransportOpts struct {
	// ListenQUIC determines how the transport will open a QUIC endpoint.
	// By default, quic.Listen("udp", addr, config) is used.
	// ListenQUIC might be called multiple times.
	ListenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)

	// QUICConfig is the QUIC configuration used by the transport.
	// QUICConfig may be nil and should not be modified after calling
	// RegisterTransport.
	// If QUICConfig.TLSConfig is nil, the TLSConfig of the net/http Transport
	// given to RegisterTransport will be used.
	QUICConfig *quic.Config
}

// RegisterTransport configures a net/http HTTP/1 Transport to use HTTP/3.
func RegisterTransport(tr *http.Transport, opts TransportOpts) {
	if opts.QUICConfig == nil {
		opts.QUICConfig = &quic.Config{}
	}
	if opts.QUICConfig.TLSConfig == nil {
		opts.QUICConfig.TLSConfig = tr.TLSClientConfig
	}
	if opts.ListenQUIC == nil {
		opts.ListenQUIC = func(addr string, config *quic.Config) (*quic.Endpoint, error) {
			return quic.Listen("udp", addr, config)
		}
	}
	tr3 := &transport{
		// initConfig will clone the tr.TLSClientConfig.
		config:      initConfig(opts.QUICConfig),
		listenQUIC:  opts.ListenQUIC,
		activeConns: make(map[*clientConn]struct{}),
	}
	tr.RegisterProtocol("http/3", netHTTPTransport{tr3})
}

func (tr *transport) incInFlightDials() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.inFlightDials++
}

func (tr *transport) decInFlightDials() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.inFlightDials--
}

func (tr *transport) initEndpoint() (err error) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	if tr.endpoint == nil {
		tr.endpoint, err = tr.listenQUIC(":0", tr.config)
	}
	return err
}

// dial creates a new HTTP/3 client connection.
func (tr *transport) dial(ctx context.Context, target string) (*clientConn, error) {
	tr.incInFlightDials()
	defer tr.decInFlightDials()

	if err := tr.initEndpoint(); err != nil {
		return nil, err
	}
	qconn, err := tr.endpoint.Dial(ctx, "udp", target, tr.config)
	if err != nil {
		return nil, err
	}
	return tr.newClientConn(ctx, qconn)
}

// CloseIdleConnections is called by net/http.Transport.CloseIdleConnections
// after all existing idle connections are closed using http3.clientConn.Close.
//
// When the transport has no active connections anymore, calling this method
// will make the transport clean up any shared resources that are no longer
// required, such as its QUIC endpoint.
func (tr *transport) CloseIdleConnections() {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	if tr.endpoint == nil || len(tr.activeConns) > 0 || tr.inFlightDials > 0 {
		return
	}
	tr.endpoint.Close(canceledCtx)
	tr.endpoint = nil
}

// A clientConn is a client HTTP/3 connection.
//
// Multiple goroutines may invoke methods on a clientConn simultaneously.
type clientConn struct {
	qconn *quic.Conn
	genericConn

	enc qpackEncoder
	dec qpackDecoder
}

func (tr *transport) registerConn(cc *clientConn) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	tr.activeConns[cc] = struct{}{}
}

func (tr *transport) unregisterConn(cc *clientConn) {
	tr.mu.Lock()
	defer tr.mu.Unlock()
	delete(tr.activeConns, cc)
}

func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn) (*clientConn, error) {
	cc := &clientConn{
		qconn: qconn,
	}
	tr.registerConn(cc)
	cc.enc.init()

	// Create control stream and send SETTINGS frame.
	controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl)
	if err != nil {
		tr.unregisterConn(cc)
		return nil, fmt.Errorf("http3: cannot create control stream: %v", err)
	}
	controlStream.writeSettings()
	controlStream.Flush()

	go func() {
		cc.acceptStreams(qconn, cc)
		tr.unregisterConn(cc)
	}()
	return cc, nil
}

// TODO: implement the rest of net/http.ClientConn methods beyond Close.
func (cc *clientConn) Close() error {
	// We need to use Close rather than Abort on the QUIC connection.
	// Otherwise, when a net/http.Transport.CloseIdleConnections is called, it
	// might call the http3.transport.CloseIdleConnections prior to all idle
	// connections being fully closed; this would make it unable to close its
	// QUIC endpoint, making http3.transport.CloseIdleConnections a no-op
	// unintentionally.
	return cc.qconn.Close()
}

func (cc *clientConn) Err() error {
	return nil
}

func (cc *clientConn) Reserve() error {
	return nil
}

func (cc *clientConn) Release() {
}

func (cc *clientConn) Available() int {
	return 0
}

func (cc *clientConn) InFlight() int {
	return 0
}

func (cc *clientConn) handleControlStream(st *stream) error {
	// "A SETTINGS frame MUST be sent as the first frame of each control stream [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2
	if err := st.readSettings(func(settingsType, settingsValue int64) error {
		switch settingsType {
		case settingsMaxFieldSectionSize:
			_ = settingsValue // TODO
		case settingsQPACKMaxTableCapacity:
			_ = settingsValue // TODO
		case settingsQPACKBlockedStreams:
			_ = settingsValue // TODO
		default:
			// Unknown settings types are ignored.
		}
		return nil
	}); err != nil {
		return err
	}

	for {
		ftype, err := st.readFrameHeader()
		if err != nil {
			return err
		}
		switch ftype {
		case frameTypeCancelPush:
			// "If a CANCEL_PUSH frame is received that references a push ID
			// greater than currently allowed on the connection,
			// this MUST be treated as a connection error of type H3_ID_ERROR."
			// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7
			return &connectionError{
				code:    errH3IDError,
				message: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent",
			}
		case frameTypeGoaway:
			// TODO: Wait for requests to complete before closing connection.
			return errH3NoError
		default:
			// Unknown frames are ignored.
			if err := st.discardUnknownFrame(ftype); err != nil {
				return err
			}
		}
	}
}

func (cc *clientConn) handleEncoderStream(*stream) error {
	// TODO
	return nil
}

func (cc *clientConn) handleDecoderStream(*stream) error {
	// TODO
	return nil
}

func (cc *clientConn) handlePushStream(*stream) error {
	// "A client MUST treat receipt of a push stream as a connection error
	// of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3
	return &connectionError{
		code:    errH3IDError,
		message: "push stream created when no MAX_PUSH_ID has been sent",
	}
}

func (cc *clientConn) handleRequestStream(st *stream) error {
	// "Clients MUST treat receipt of a server-initiated bidirectional
	// stream as a connection error of type H3_STREAM_CREATION_ERROR [...]"
	// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3
	return &connectionError{
		code:    errH3StreamCreationError,
		message: "server created bidirectional stream",
	}
}

// abort closes the connection with an error.
func (cc *clientConn) abort(err error) {
	if e, ok := err.(*connectionError); ok {
		cc.qconn.Abort(&quic.ApplicationError{
			Code:   uint64(e.code),
			Reason: e.message,
		})
	} else {
		cc.qconn.Abort(err)
	}
}
