/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   https://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 tracer

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"github.com/rs/zerolog"

	"github.com/apache/plc4x/plc4go/spi/options"
	"github.com/apache/plc4x/plc4go/spi/utils"
)

type TraceEntry struct {
	Timestamp     time.Time
	ConnectionId  string
	TransactionId string
	Operation     string
	Message       string
}

type Provider interface {
	EnableTracer()
	GetTracer() *Tracer
}

type Tracer interface {
	GetConnectionId() string
	SetConnectionId(connectionId string)
	ResetTraces()
	GetTraces() []TraceEntry
	AddTrace(operation string, message string)
	AddTransactionalStartTrace(operation string, message string) string
	AddTransactionalTrace(transactionId string, operation string, message string)
	FilterTraces(traces []TraceEntry, connectionIdFilter string, transactionIdFilter string, operationFilter string, messageFilter string) []TraceEntry
}

func NewTracer(connectionId string, _options ...options.WithOption) Tracer {
	customLogger := options.ExtractCustomLoggerOrDefaultToGlobal(_options...)
	t := tracer{
		traceEntries: []TraceEntry{},
		log:          customLogger,
	}
	t.connectionId.Store(connectionId)
	return &t
}

type tracer struct {
	connectionId atomic.Value
	traceEntries []TraceEntry
	m            sync.Mutex

	log zerolog.Logger
}

func (t *tracer) GetConnectionId() string {
	return t.connectionId.Load().(string)
}

func (t *tracer) SetConnectionId(connectionId string) {
	t.connectionId.Store(connectionId)
}

func (t *tracer) ResetTraces() {
	t.m.Lock()
	defer t.m.Unlock()
	t.traceEntries = []TraceEntry{}
}

func (t *tracer) GetTraces() []TraceEntry {
	t.m.Lock()
	defer t.m.Unlock()
	return t.traceEntries
}

func (t *tracer) AddTrace(operation string, message string) {
	t.m.Lock()
	defer t.m.Unlock()
	t.traceEntries = append(t.traceEntries, TraceEntry{
		Timestamp:     time.Now(),
		ConnectionId:  t.connectionId.Load().(string),
		TransactionId: "",
		Operation:     operation,
		Message:       message,
	})
}

func (t *tracer) AddTransactionalStartTrace(operation string, message string) string {
	t.m.Lock()
	defer t.m.Unlock()
	transactionId := utils.GenerateId(t.log, 4)
	t.traceEntries = append(t.traceEntries, TraceEntry{
		Timestamp:     time.Now(),
		ConnectionId:  t.connectionId.Load().(string),
		TransactionId: transactionId,
		Operation:     operation,
		Message:       message,
	})
	return transactionId
}

func (t *tracer) AddTransactionalTrace(transactionId string, operation string, message string) {
	t.m.Lock()
	defer t.m.Unlock()
	t.traceEntries = append(t.traceEntries, TraceEntry{
		Timestamp:     time.Now(),
		ConnectionId:  t.connectionId.Load().(string),
		TransactionId: transactionId,
		Operation:     operation,
		Message:       message,
	})
}

func (*tracer) FilterTraces(traces []TraceEntry, connectionIdFilter string, transactionIdFilter string, operationFilter string, messageFilter string) []TraceEntry {
	var result []TraceEntry
traceFiltering:
	for _, trace := range traces {
		if connectionIdFilter != "" && trace.ConnectionId != connectionIdFilter {
			continue traceFiltering
		}
		if transactionIdFilter != "" && trace.TransactionId != transactionIdFilter {
			continue traceFiltering
		}
		if operationFilter != "" && trace.Operation != operationFilter {
			continue traceFiltering
		}
		if messageFilter != "" && trace.Message != messageFilter {
			continue traceFiltering
		}
		result = append(result, trace)
	}
	return result
}

func (t *tracer) String() string {
	return fmt.Sprintf("Tracer for %s", t.connectionId.Load())
}
