//go:build windows

package tls

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"io"
	"net"
	"os"
	"sync"
	"sync/atomic"
	"time"

	"github.com/sagernet/sing-box/common/schannel"
	"github.com/sagernet/sing-box/option"
	"github.com/sagernet/sing/common/buf"
	"github.com/sagernet/sing/common/bufio"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
	N "github.com/sagernet/sing/common/network"
)

const (
	windowsTLSEngineName        = "Windows TLS engine"
	handshakeReadChunkSize      = 8192
	readScratchSize             = 16 * 1024
	readWaitCiphertextChunkSize = 4096
)

type windowsClientConfig struct {
	systemTLSConfig
	userRoots *x509.CertPool
}

func (c *windowsClientConfig) Clone() Config {
	return &windowsClientConfig{
		systemTLSConfig: c.systemTLSConfig.clone(),
		userRoots:       c.userRoots,
	}
}

func newWindowsClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
	err := schannel.CheckPlatform()
	if err != nil {
		return nil, err
	}
	base, validated, err := newSystemTLSConfig(ctx, serverAddress, options, allowEmptyServerName, windowsTLSEngineName)
	if err != nil {
		return nil, err
	}
	var userRoots *x509.CertPool
	if len(validated.UserPEM) > 0 {
		userRoots = x509.NewCertPool()
		if !userRoots.AppendCertsFromPEM(validated.UserPEM) {
			return nil, E.New("parse certificate PEM")
		}
	}
	return &windowsClientConfig{
		systemTLSConfig: base,
		userRoots:       userRoots,
	}, nil
}

func (c *windowsClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
	deadline, hasDeadline := ctx.Deadline()
	if hasDeadline {
		deadlineErr := conn.SetDeadline(deadline)
		if deadlineErr != nil {
			return nil, E.Cause(deadlineErr, "set handshake deadline")
		}
		defer conn.SetDeadline(time.Time{})
	}

	client, err := schannel.NewClientContext(c.minVersion, c.maxVersion, c.serverName, c.nextProtos)
	if err != nil {
		return nil, err
	}

	handshakeOK := false
	defer func() {
		if !handshakeOK {
			client.Close()
		}
	}()

	stopCancel := installHandshakeCancel(ctx, conn)
	defer stopCancel()

	scratch := make([]byte, handshakeReadChunkSize)
	leftover, err := driveHandshake(ctx, conn, client, scratch)
	if err != nil {
		return nil, err
	}
	state, rawCerts, err := buildConnectionState(c.serverName, client)
	if err != nil {
		return nil, err
	}
	err = c.verifyPeerCertificates(state.PeerCertificates)
	if err != nil {
		return nil, err
	}
	if len(c.certificatePublicKeySHA256) > 0 {
		err = VerifyPublicKeySHA256(c.certificatePublicKeySHA256, rawCerts)
		if err != nil {
			return nil, err
		}
	}
	header, trailer, maxMessage, err := client.StreamSizes()
	if err != nil {
		return nil, err
	}

	handshakeOK = true
	tlsConn := &windowsTLSConn{
		rawConn:    conn,
		client:     client,
		state:      state,
		header:     header,
		trailer:    trailer,
		maxMessage: maxMessage,
		cipher:     leftover,
	}
	return tlsConn, nil
}

func driveHandshake(ctx context.Context, conn net.Conn, client *schannel.ClientContext, scratch []byte) ([]byte, error) {
	readMore := func() ([]byte, error) {
		more, err := readTLSRaw(conn, scratch, true)
		if err != nil {
			return nil, handshakeIOError(ctx, err, "read handshake")
		}
		return more, nil
	}
	writeOut := func(data []byte) error {
		_, err := conn.Write(data)
		if err != nil {
			return handshakeIOError(ctx, err, "write handshake")
		}
		return nil
	}
	leftover, err := driveSteps(nil, client.Step, readMore, writeOut)
	if err != nil {
		return nil, E.Cause(err, "tls handshake")
	}
	return leftover, nil
}

func driveSteps(
	initial []byte,
	step func([]byte) (schannel.StepResult, error),
	readMore func() ([]byte, error),
	writeOut func([]byte) error,
) ([]byte, error) {
	buffer := initial
	for {
		result, stepErr := step(buffer)
		if stepErr != nil {
			return nil, stepErr
		}
		if len(result.Output) > 0 {
			writeErr := writeOut(result.Output)
			if writeErr != nil {
				return nil, writeErr
			}
		}
		if result.Incomplete {
			// readMore reuses scratch storage, so keep the buffered handshake
			// bytes in stable memory before the next read overwrites them.
			buffer = append([]byte(nil), buffer...)
			more, readErr := readMore()
			if readErr != nil {
				return nil, readErr
			}
			buffer = append(buffer, more...)
			continue
		}
		if result.Consumed > len(buffer) {
			return nil, E.New("schannel: Consumed > input length")
		}
		buffer = buffer[result.Consumed:]
		if result.Done {
			return buffer, nil
		}
		if len(buffer) == 0 {
			more, readErr := readMore()
			if readErr != nil {
				return nil, readErr
			}
			buffer = append(buffer, more...)
		}
	}
}

// installHandshakeCancel unblocks an in-flight read/write by forcing an
// immediate deadline on conn when ctx is cancelled. The returned cleanup
// waits for a racing cancel to finish and clears the forced deadline.
func installHandshakeCancel(ctx context.Context, conn net.Conn) func() {
	var fired atomic.Bool
	done := make(chan struct{})
	stop := context.AfterFunc(ctx, func() {
		defer close(done)
		fired.Store(true)
		_ = conn.SetDeadline(time.Now())
	})
	return func() {
		if stop() {
			return
		}
		<-done
		if fired.Load() {
			_ = conn.SetDeadline(time.Time{})
		}
	}
}

func handshakeIOError(ctx context.Context, err error, message string) error {
	ctxErr := ctx.Err()
	if ctxErr != nil && isTimeoutError(err) {
		return ctxErr
	}
	return E.Cause(err, message)
}

func readTLSRaw(conn net.Conn, scratch []byte, requireMore bool) ([]byte, error) {
	n, err := conn.Read(scratch)
	if n > 0 {
		return scratch[:n], nil
	}
	if err != nil {
		if requireMore && errors.Is(err, io.EOF) {
			return nil, io.ErrUnexpectedEOF
		}
		return nil, err
	}
	return nil, io.ErrUnexpectedEOF
}

func isTimeoutError(err error) bool {
	if errors.Is(err, os.ErrDeadlineExceeded) {
		return true
	}
	var netErr net.Error
	return errors.As(err, &netErr) && netErr.Timeout()
}

func buildConnectionState(serverName string, client *schannel.ClientContext) (tls.ConnectionState, [][]byte, error) {
	version, cipherSuite, err := client.ConnectionInfo()
	if err != nil {
		return tls.ConnectionState{}, nil, err
	}
	alpn, err := client.ApplicationProtocol()
	if err != nil {
		return tls.ConnectionState{}, nil, err
	}
	rawCerts, err := client.RemoteCertificateChain()
	if err != nil {
		return tls.ConnectionState{}, nil, err
	}
	peerCertificates := make([]*x509.Certificate, 0, len(rawCerts))
	for index, der := range rawCerts {
		cert, parseErr := x509.ParseCertificate(der)
		if parseErr != nil {
			return tls.ConnectionState{}, nil, E.Cause(parseErr, "parse peer certificate ", index)
		}
		peerCertificates = append(peerCertificates, cert)
	}
	return tls.ConnectionState{
		Version:            version,
		HandshakeComplete:  true,
		CipherSuite:        cipherSuite,
		NegotiatedProtocol: alpn,
		ServerName:         serverName,
		PeerCertificates:   peerCertificates,
	}, rawCerts, nil
}

func (c *windowsClientConfig) verifyPeerCertificates(peerCertificates []*x509.Certificate) error {
	if c.insecure {
		return nil
	}
	var roots *x509.CertPool
	switch {
	case c.userRoots != nil:
		roots = c.userRoots
	case c.store != nil:
		roots = c.store.Pool()
	}
	return verifySystemTLSPeer(roots, c.serverName, c.timeFunc, peerCertificates)
}

type windowsTLSConn struct {
	rawConn    net.Conn
	client     *schannel.ClientContext
	state      tls.ConnectionState
	header     uint32
	trailer    uint32
	maxMessage uint32

	readAccess    sync.Mutex
	writeAccess   sync.Mutex
	contextAccess sync.RWMutex

	writeState     sync.Mutex
	writeStateOnce sync.Once
	writeReady     *sync.Cond
	postHandshake  bool
	writeActive    bool

	cipher       []byte
	plain        []byte
	readScratch  []byte
	writeScratch []byte
	readEOF      bool

	deadlineAccess sync.Mutex
	readDeadline   time.Time
	writeDeadline  time.Time
	closed         atomic.Bool
}

var (
	_ N.ExtendedConn    = (*windowsTLSConn)(nil)
	_ N.ReadWaitCreator = (*windowsTLSConn)(nil)
)

type windowsTLSAppendCipherFunc func(requireMore bool) error
type windowsTLSReadRawFunc func(requireMore bool) ([]byte, error)

func (c *windowsTLSConn) Read(p []byte) (int, error) {
	c.readAccess.Lock()
	defer c.readAccess.Unlock()
	if len(p) == 0 {
		return 0, nil
	}
	if c.isClosed() {
		return 0, net.ErrClosed
	}
	return c.readIntoLocked(p, c.appendRaw, c.readRaw)
}

func (c *windowsTLSConn) ReadBuffer(buffer *buf.Buffer) error {
	c.readAccess.Lock()
	defer c.readAccess.Unlock()
	if buffer.IsFull() {
		return io.ErrShortBuffer
	}
	if c.isClosed() {
		return net.ErrClosed
	}
	startLen := buffer.Len()
	n, err := c.readIntoLocked(buffer.FreeBytes(), c.appendRaw, c.readRaw)
	buffer.Truncate(startLen + n)
	return err
}

func (c *windowsTLSConn) readIntoLocked(p []byte, appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) (int, error) {
	plaintext, err := c.readPlaintextLocked(appendCipher, readRaw)
	if err != nil {
		return 0, err
	}
	n := copy(p, plaintext)
	if n < len(plaintext) {
		c.plain = append([]byte(nil), plaintext[n:]...)
	}
	return n, nil
}

func (c *windowsTLSConn) readPlaintextLocked(appendCipher windowsTLSAppendCipherFunc, readRaw windowsTLSReadRawFunc) ([]byte, error) {
	if len(c.plain) > 0 {
		plaintext := c.plain
		c.plain = nil
		return plaintext, nil
	}
	if c.readEOF {
		return nil, io.EOF
	}

	cleanup, err := c.applyReadDeadline()
	if err != nil {
		return nil, err
	}
	defer cleanup()

	for {
		if len(c.cipher) > 0 {
			result, decryptErr := c.decrypt(c.cipher)
			if decryptErr != nil {
				return nil, decryptErr
			}
			if result.Expired {
				c.readEOF = true
				return nil, io.EOF
			}
			if !result.Incomplete {
				plaintext := result.Plaintext
				if result.Renegotiate && len(plaintext) > 0 {
					plaintext = append([]byte(nil), plaintext...)
				}
				nextCipher := c.cipher[result.ConsumedTotal:]
				if len(result.RenegotiateToken) > 0 {
					nextCipher = result.RenegotiateToken
				}
				c.cipher = nextCipher
				if len(c.cipher) == 0 {
					c.cipher = nil
				}
				if result.Renegotiate {
					postErr := c.drivePostHandshake(readRaw)
					if postErr != nil {
						return nil, postErr
					}
				}
				if len(plaintext) > 0 {
					return plaintext, nil
				}
				continue
			}
		}
		err = appendCipher(len(c.cipher) > 0)
		if err != nil {
			return nil, err
		}
	}
}

func (c *windowsTLSConn) drivePostHandshake(readRaw windowsTLSReadRawFunc) error {
	initial := c.cipher
	c.cipher = nil
	err := c.beginPostHandshakeWrite()
	if err != nil {
		return err
	}
	defer c.finishPostHandshakeWrite()
	c.contextAccess.Lock()
	if c.client == nil {
		c.contextAccess.Unlock()
		return net.ErrClosed
	}
	writeFailed := false
	readMore := func() ([]byte, error) {
		more, err := readRaw(true)
		if err != nil {
			return nil, E.Cause(err, "tls post-handshake read")
		}
		return more, nil
	}
	writeOut := func(data []byte) error {
		err := c.writePostHandshakeReplyLocked(data)
		if err != nil {
			writeFailed = true
			return E.Cause(err, "tls post-handshake write")
		}
		return nil
	}
	leftover, err := driveSteps(initial, c.client.PostHandshake, readMore, writeOut)
	c.contextAccess.Unlock()
	if err != nil {
		if writeFailed {
			_ = c.Close()
		}
		return E.Cause(err, "tls post-handshake")
	}
	if len(leftover) > 0 {
		c.cipher = leftover
	}
	return nil
}

func (c *windowsTLSConn) writePostHandshakeReplyLocked(data []byte) error {
	c.deadlineAccess.Lock()
	deadline := c.readDeadline
	c.deadlineAccess.Unlock()
	cleanup, err := c.applyDeadline(deadline, c.rawConn.SetWriteDeadline)
	if err != nil {
		return err
	}
	defer cleanup()
	_, err = c.rawConn.Write(data)
	return err
}

func (c *windowsTLSConn) decrypt(input []byte) (schannel.DecryptResult, error) {
	c.contextAccess.RLock()
	defer c.contextAccess.RUnlock()
	if c.client == nil {
		return schannel.DecryptResult{}, net.ErrClosed
	}
	return c.client.Decrypt(input)
}

func (c *windowsTLSConn) encrypt(plaintext []byte) ([]byte, error) {
	c.contextAccess.RLock()
	defer c.contextAccess.RUnlock()
	if c.client == nil {
		return nil, net.ErrClosed
	}
	if c.writeScratch == nil {
		c.writeScratch = make([]byte, int(c.header)+int(c.maxMessage)+int(c.trailer))
	}
	return c.client.Encrypt(c.header, c.trailer, plaintext, c.writeScratch)
}

func (c *windowsTLSConn) readRaw(requireMore bool) ([]byte, error) {
	if c.readScratch == nil {
		c.readScratch = make([]byte, readScratchSize)
	}
	return readTLSRaw(c.rawConn, c.readScratch, requireMore)
}

func (c *windowsTLSConn) appendRaw(requireMore bool) error {
	more, err := c.readRaw(requireMore)
	if err != nil {
		return err
	}
	c.cipher = append(c.cipher, more...)
	return nil
}

func (c *windowsTLSConn) Write(p []byte) (int, error) {
	err := c.beginWrite()
	if err != nil {
		return 0, err
	}
	defer c.finishWrite()
	if len(p) == 0 {
		return 0, nil
	}
	if c.isClosed() {
		return 0, net.ErrClosed
	}

	cleanup, err := c.applyWriteDeadline()
	if err != nil {
		return 0, err
	}
	defer cleanup()

	total := 0
	chunkSize := int(c.maxMessage)
	for len(p) > 0 {
		chunk := p
		if len(chunk) > chunkSize {
			chunk = chunk[:chunkSize]
		}
		encrypted, encryptErr := c.encrypt(chunk)
		if encryptErr != nil {
			if errors.Is(encryptErr, net.ErrClosed) {
				return total, net.ErrClosed
			}
			return total, E.Cause(encryptErr, "tls encrypt")
		}
		_, writeErr := c.rawConn.Write(encrypted)
		if writeErr != nil {
			_ = c.Close()
			return total, E.Cause(writeErr, "tls write")
		}
		total += len(chunk)
		p = p[len(chunk):]
	}
	return total, nil
}

func (c *windowsTLSConn) WriteBuffer(buffer *buf.Buffer) error {
	defer buffer.Release()
	_, err := c.Write(buffer.Bytes())
	return err
}

func (c *windowsTLSConn) CreateReadWaiter() (N.ReadWaiter, bool) {
	rawWaiter, ok := bufio.CreateReadWaiter(c.rawConn)
	if !ok {
		return nil, false
	}
	return &windowsTLSReadWaiter{
		conn:      c,
		rawWaiter: rawWaiter,
	}, true
}

func (c *windowsTLSConn) Close() error {
	if !c.closed.CompareAndSwap(false, true) {
		return nil
	}
	ready := c.writeCondition()
	c.writeState.Lock()
	ready.Broadcast()
	c.writeState.Unlock()
	closeErr := c.rawConn.Close()
	c.contextAccess.Lock()
	if c.client != nil {
		c.client.Close()
		c.client = nil
	}
	c.contextAccess.Unlock()
	return closeErr
}

func (c *windowsTLSConn) LocalAddr() net.Addr {
	return c.rawConn.LocalAddr()
}

func (c *windowsTLSConn) RemoteAddr() net.Addr {
	return c.rawConn.RemoteAddr()
}

func (c *windowsTLSConn) SetDeadline(t time.Time) error {
	c.deadlineAccess.Lock()
	defer c.deadlineAccess.Unlock()
	err := c.rawConn.SetDeadline(t)
	if err != nil {
		return err
	}
	c.readDeadline = t
	c.writeDeadline = t
	return nil
}

func (c *windowsTLSConn) SetReadDeadline(t time.Time) error {
	c.deadlineAccess.Lock()
	defer c.deadlineAccess.Unlock()
	err := c.rawConn.SetReadDeadline(t)
	if err != nil {
		return err
	}
	c.readDeadline = t
	return nil
}

func (c *windowsTLSConn) SetWriteDeadline(t time.Time) error {
	c.deadlineAccess.Lock()
	defer c.deadlineAccess.Unlock()
	err := c.rawConn.SetWriteDeadline(t)
	if err != nil {
		return err
	}
	c.writeDeadline = t
	return nil
}

func (c *windowsTLSConn) NetConn() net.Conn {
	return c.rawConn
}

func (c *windowsTLSConn) HandshakeContext(ctx context.Context) error {
	return nil
}

func (c *windowsTLSConn) ConnectionState() ConnectionState {
	return c.state
}

func (c *windowsTLSConn) applyReadDeadline() (func(), error) {
	c.deadlineAccess.Lock()
	deadline := c.readDeadline
	c.deadlineAccess.Unlock()
	return c.applyDeadline(deadline, c.rawConn.SetReadDeadline)
}

func (c *windowsTLSConn) applyWriteDeadline() (func(), error) {
	c.deadlineAccess.Lock()
	deadline := c.writeDeadline
	c.deadlineAccess.Unlock()
	return c.applyDeadline(deadline, c.rawConn.SetWriteDeadline)
}

func (c *windowsTLSConn) applyDeadline(deadline time.Time, set func(time.Time) error) (func(), error) {
	if deadline.IsZero() {
		return func() {}, nil
	}
	if !deadline.After(time.Now()) {
		return nil, os.ErrDeadlineExceeded
	}
	err := set(deadline)
	if err != nil {
		return nil, err
	}
	return func() { _ = set(time.Time{}) }, nil
}

func (c *windowsTLSConn) beginWrite() error {
	ready := c.writeCondition()
	c.writeState.Lock()
	for c.postHandshake || c.writeActive {
		if c.closed.Load() {
			c.writeState.Unlock()
			return net.ErrClosed
		}
		ready.Wait()
	}
	c.writeActive = true
	c.writeState.Unlock()

	c.writeAccess.Lock()
	if c.closed.Load() {
		c.writeAccess.Unlock()
		c.writeState.Lock()
		c.writeActive = false
		ready.Broadcast()
		c.writeState.Unlock()
		return net.ErrClosed
	}
	return nil
}

func (c *windowsTLSConn) finishWrite() {
	c.writeAccess.Unlock()
	ready := c.writeCondition()
	c.writeState.Lock()
	c.writeActive = false
	ready.Broadcast()
	c.writeState.Unlock()
}

func (c *windowsTLSConn) beginPostHandshakeWrite() error {
	ready := c.writeCondition()
	c.writeState.Lock()
	c.postHandshake = true
	for c.writeActive {
		if c.closed.Load() {
			c.postHandshake = false
			ready.Broadcast()
			c.writeState.Unlock()
			return net.ErrClosed
		}
		ready.Wait()
	}
	c.writeActive = true
	c.writeState.Unlock()

	c.writeAccess.Lock()
	if c.closed.Load() {
		c.writeAccess.Unlock()
		c.writeState.Lock()
		c.writeActive = false
		c.postHandshake = false
		ready.Broadcast()
		c.writeState.Unlock()
		return net.ErrClosed
	}
	return nil
}

func (c *windowsTLSConn) finishPostHandshakeWrite() {
	c.writeAccess.Unlock()
	ready := c.writeCondition()
	c.writeState.Lock()
	c.writeActive = false
	c.postHandshake = false
	ready.Broadcast()
	c.writeState.Unlock()
}

func (c *windowsTLSConn) writeCondition() *sync.Cond {
	c.writeStateOnce.Do(func() {
		c.writeReady = sync.NewCond(&c.writeState)
	})
	return c.writeReady
}

func (c *windowsTLSConn) isClosed() bool {
	return c.closed.Load()
}

type windowsTLSReadWaiter struct {
	conn      *windowsTLSConn
	rawWaiter N.ReadWaiter
	options   N.ReadWaitOptions
}

var _ N.ReadWaiter = (*windowsTLSReadWaiter)(nil)

func (w *windowsTLSReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
	w.options = options
	w.rawWaiter.InitializeReadWaiter(N.ReadWaitOptions{
		MTU: readWaitCiphertextChunkSize,
	})
	return false
}

func (w *windowsTLSReadWaiter) WaitReadBuffer() (*buf.Buffer, error) {
	c := w.conn
	c.readAccess.Lock()
	defer c.readAccess.Unlock()
	if c.isClosed() {
		return nil, net.ErrClosed
	}
	plaintext, err := c.readPlaintextLocked(w.appendRaw, w.readRaw)
	if err != nil {
		return nil, err
	}
	buffer := w.options.NewBuffer()
	n, writeErr := buffer.Write(plaintext)
	if writeErr != nil {
		buffer.Release()
		return nil, writeErr
	}
	if n == 0 {
		buffer.Release()
		return nil, io.ErrShortBuffer
	}
	if n < len(plaintext) {
		c.plain = append([]byte(nil), plaintext[n:]...)
	}
	w.options.PostReturn(buffer)
	return buffer, nil
}

func (w *windowsTLSReadWaiter) appendRaw(requireMore bool) error {
	rawBuffer, err := w.readRawBuffer(requireMore)
	if err != nil {
		return err
	}
	w.conn.cipher = append(w.conn.cipher, rawBuffer.Bytes()...)
	rawBuffer.Release()
	return nil
}

func (w *windowsTLSReadWaiter) readRaw(requireMore bool) ([]byte, error) {
	rawBuffer, err := w.readRawBuffer(requireMore)
	if err != nil {
		return nil, err
	}
	data := append([]byte(nil), rawBuffer.Bytes()...)
	rawBuffer.Release()
	return data, nil
}

func (w *windowsTLSReadWaiter) readRawBuffer(requireMore bool) (*buf.Buffer, error) {
	rawBuffer, err := w.rawWaiter.WaitReadBuffer()
	if err != nil {
		if requireMore && errors.Is(err, io.EOF) {
			return nil, io.ErrUnexpectedEOF
		}
		return nil, err
	}
	if rawBuffer == nil || rawBuffer.Len() == 0 {
		if rawBuffer != nil {
			rawBuffer.Release()
		}
		return nil, io.ErrUnexpectedEOF
	}
	return rawBuffer, nil
}
