// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logging

import (
	"context"
	"sync"

	"github.com/rs/zerolog/log"

	"go.woodpecker-ci.org/woodpecker/v3/server/model"
)

// TODO: (bradrydzewski) writing to subscribers is currently a blocking
// operation and does not protect against slow clients from locking
// the stream. This should be resolved.

//nolint:godot
// TODO: (bradrydzewski) implement a mux.Info to fetch information and
// statistics for the multiplexer. Streams, subscribers, etc
// mux.Info()

//nolint:godot
// TODO: (bradrydzewski) refactor code to place publisher and subscriber
// operations in separate files with more encapsulated logic.
// sub.push()
// sub.join()
// sub.start()... event loop

type subscriber struct {
	receiver LogChan
}

type stream struct {
	sync.Mutex

	stepID int64
	list   []*model.LogEntry
	subs   map[*subscriber]struct{}
	done   chan struct{}
}

type logger struct {
	sync.Mutex

	streams map[int64]*stream
}

// New returns a new logger.
func New() Log {
	return &logger{
		streams: map[int64]*stream{},
	}
}

func (l *logger) Open(_ context.Context, stepID int64) error {
	l.Lock()
	l.open(stepID)
	l.Unlock()
	return nil
}

func (l *logger) open(stepID int64) {
	_, ok := l.streams[stepID]
	if !ok {
		l.streams[stepID] = &stream{
			stepID: stepID,
			subs:   make(map[*subscriber]struct{}),
			done:   make(chan struct{}),
		}
	}
}

func (l *logger) Write(ctx context.Context, stepID int64, entries []*model.LogEntry) error {
	l.Lock()
	s, ok := l.streams[stepID]
	if !ok {
		// Auto-open the stream while still holding the logger lock so that a
		// concurrent Write for the same step cannot race on l.streams.
		l.open(stepID)
		s = l.streams[stepID]
	}
	l.Unlock()

	s.Lock()
	s.list = append(s.list, entries...)
	for sub := range s.subs {
		select {
		case sub.receiver <- entries:
		default:
			log.Info().Msgf("subscriber channel is full -- dropping logs for step %d", stepID)
		}
	}
	s.Unlock()

	return nil
}

func (l *logger) Tail(c context.Context, stepID int64, receiver LogChan) error {
	l.Lock()
	s, ok := l.streams[stepID]
	l.Unlock()
	if !ok {
		return ErrNotFound
	}

	sub := &subscriber{
		receiver: receiver,
	}
	s.Lock()
	if len(s.list) != 0 {
		sub.receiver <- s.list
	}
	s.subs[sub] = struct{}{}
	s.Unlock()

	select {
	case <-c.Done():
	case <-s.done:
	}

	s.Lock()
	delete(s.subs, sub)
	s.Unlock()
	return nil
}

func (l *logger) Close(_ context.Context, stepID int64) error {
	l.Lock()
	s, ok := l.streams[stepID]
	l.Unlock()
	if !ok {
		return ErrNotFound
	}

	s.Lock()
	close(s.done)
	s.Unlock()

	l.Lock()
	delete(l.streams, stepID)
	l.Unlock()
	return nil
}
