package syslog

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"net"
	"sync"
	"time"

	"github.com/leodido/go-syslog/v4/rfc5424"
)

type message struct {
	*rfc5424.SyslogMessage
}

type transport struct {
	conn   net.Conn
	family string
	addr   string
	tlsCfg *tls.Config
	framed bool

	msgs chan message
	done chan struct{}
	seen chan struct{}
	mu   sync.RWMutex
}

func dial(family, addr string, bufSize int, tlsCfg *tls.Config) (*transport, error) {
	t := &transport{}
	t.family = family
	t.addr = addr
	t.tlsCfg = tlsCfg
	t.msgs = make(chan message, bufSize)
	t.done = make(chan struct{})
	t.seen = make(chan struct{})

	if err := t.connect(); err != nil {
		return nil, err
	}

	switch family {
	case "tcp+tls", "tcp", "tcp4", "tcp6", "unix", "unixpacket":
		t.framed = true
	}

	go t.worker()

	return t, nil
}

func (t *transport) connect() error {
	var (
		conn net.Conn
		err  error
	)

	dialer := net.Dialer{Timeout: 5 * time.Second}
	if t.family == "tcp+tls" {
		conn, err = tls.DialWithDialer(&dialer, "tcp", t.addr, t.tlsCfg)
	} else {
		conn, err = dialer.Dial(t.family, t.addr)
	}

	if err != nil {
		return fmt.Errorf("dial: %w", err)
	}

	t.conn = conn
	return nil
}

func (t *transport) send(msg *rfc5424.SyslogMessage) {
	if !msg.Valid() {
		// msg.String() will fail during serialization, so don't bother.
		return
	}

	t.mu.RLock()
	msgs := t.msgs
	t.mu.RUnlock()

	if msgs == nil {
		return
	}

	select {
	case msgs <- message{msg}:
	default:
		// Drop the message if the channel buffer is full.
	}
}

func (t *transport) flush(timeout time.Duration) {
	t.mu.RLock()
	if t.msgs == nil {
		t.mu.RUnlock()
		return
	}
	seen := t.seen
	t.mu.RUnlock()

	<-seen

	t.mu.Lock()
	if t.msgs == nil {
		t.mu.Unlock()
		return
	}
	close(t.msgs)
	done := t.done
	t.msgs = make(chan message, cap(t.msgs))
	t.done = make(chan struct{})
	t.seen = make(chan struct{})
	t.mu.Unlock()

	select {
	case <-done:
	case <-time.After(timeout):
	}
}

func (t *transport) close() {
	t.mu.Lock()
	if t.msgs != nil {
		close(t.msgs)
		t.msgs = nil
	}
	t.mu.Unlock()
}

func (t *transport) worker() {
	outbuf := bytes.Buffer{}
	msgbuf := make([]message, 0, cap(t.msgs))
	framed := t.framed

	if !framed {
		// the transport does not support framing;
		// only send one message at a time
		msgbuf = msgbuf[0:0:1]
	}

	for {
		t.mu.RLock()
		if t.msgs == nil {
			t.mu.RUnlock()
			break
		}
		msgs := t.msgs
		done := t.done
		close(t.seen)
		t.mu.RUnlock()

		for {
			// trim the message buffer
			msgbuf = msgbuf[0:0:cap(msgbuf)]

			msg, ok := <-msgs
			if !ok {
				// channel is closed
				break
			}
			msgbuf = append(msgbuf, msg)

		bufloop:
			// read out the whole channel buffer
			for len(msgbuf) < cap(msgbuf) {
				select {
				case msg, ok = <-msgs:
					if !ok {
						// channel is closed; we still need to process what we read
						break bufloop
					}
				default:
					// buffer is empty; we still need to process what we read
					break bufloop
				}
				msgbuf = append(msgbuf, msg)
			}

			outbuf.Reset()
			for _, msg := range msgbuf {
				serialized, err := msg.String()
				if err != nil {
					continue
				}
				if framed {
					fmt.Fprintf(&outbuf, "%d ", len(serialized))
				}
				outbuf.WriteString(serialized)
			}

			t.write(outbuf.Bytes())
		}

		close(done)
	}

	t.conn.Close()
}

func (t *transport) write(buf []byte) error {
	timeout := 1 * time.Second

	t.conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err := t.conn.Write(buf)
	if err != nil {
		// attempt to reconnect and send again
		t.conn.Close()
		for {
			t.mu.RLock()
			closed := t.msgs == nil
			t.mu.RUnlock()

			if closed {
				return fmt.Errorf("transport closed")
			}

			if err := t.connect(); err == nil {
				break
			}
			time.Sleep(5 * time.Second)
		}

		t.conn.SetWriteDeadline(time.Now().Add(timeout))
		_, err = t.conn.Write(buf)
	}

	return err
}
