package nostr

import (
	"bytes"
	"context"
	"crypto/tls"
	"errors"
	"io"
	"net/http"
	"sync/atomic"
	"time"

	ws "github.com/coder/websocket"
)

// Connection represents a websocket connection to a Nostr relay.
type Connection struct {
	conn         *ws.Conn
	writeQueue   chan writeRequest
	closed       *atomic.Bool
	closedNotify chan struct{}
}

type writeRequest struct {
	msg    []byte
	answer chan error
}

// NewConnection creates a new websocket connection to a Nostr relay.
func NewConnection(
	ctx context.Context,
	url string,
	handleMessage func(string),
	requestHeader http.Header,
	tlsConfig *tls.Config,
) (*Connection, error) {
	c, _, err := ws.Dial(ctx, url, getConnectionOptions(requestHeader, tlsConfig))
	if err != nil {
		return nil, err
	}
	c.SetReadLimit(2 << 24) // 33MB

	// this will tell if the connection is closed

	// ping every 29 seconds
	ticker := time.NewTicker(29 * time.Second)

	// main websocket loop
	writeQueue := make(chan writeRequest)
	readQueue := make(chan string)

	conn := &Connection{
		conn:         c,
		writeQueue:   writeQueue,
		closed:       &atomic.Bool{},
		closedNotify: make(chan struct{}),
	}

	go func() {
		for {
			select {
			case <-ctx.Done():
				conn.doClose(ws.StatusNormalClosure, "")
				return
			case <-ticker.C:
				ctx, cancel := context.WithTimeoutCause(ctx, time.Millisecond*800, errors.New("ping took too long"))
				err := c.Ping(ctx)
				cancel()
				if err != nil {
					conn.doClose(ws.StatusAbnormalClosure, "ping took too long")
					return
				}
			case wr := <-writeQueue:
				debugLogf("{%s} sending %v\n", url, string(wr.msg))
				ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("write took too long"))
				err := c.Write(ctx, ws.MessageText, wr.msg)
				cancel()
				if err != nil {
					conn.doClose(ws.StatusAbnormalClosure, "write took too long")
					if wr.answer != nil {
						wr.answer <- err
					}
					return
				}
				if wr.answer != nil {
					close(wr.answer)
				}
			case msg := <-readQueue:
				debugLogf("{%s} received %v\n", url, msg)
				handleMessage(msg)
			}
		}
	}()

	// read loop -- loops back to the main loop
	go func() {
		buf := new(bytes.Buffer)

		for {
			buf.Reset()

			_, reader, err := c.Reader(ctx)
			if err != nil {
				conn.doClose(ws.StatusAbnormalClosure, "failed to get reader")
				return
			}
			if _, err := io.Copy(buf, reader); err != nil {
				conn.doClose(ws.StatusAbnormalClosure, "failed to read")
				return
			}

			readQueue <- string(buf.Bytes())
		}
	}()

	return conn, nil
}

func (c *Connection) doClose(code ws.StatusCode, reason string) {
	wasClosed := c.closed.Swap(true)
	if !wasClosed {
		c.conn.Close(code, reason)
		close(c.closedNotify)
		close(c.writeQueue)
	}
}
