package diagnostics

import (
	"errors"
	"fmt"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

const (
	MaxWarningGroups    = 128
	MaxNonWarningGroups = 256
)

// Aggregator merges origin-enriched diagnostics in caller-supplied canonical
// order. Its zero value is ready for use.
type Aggregator struct {
	groups                   []contracts.DiagnosticGroup
	indices                  map[groupKey]int
	warningGroups            int
	nonWarningGroups         int
	warningOccurrences       int
	nonWarningOccurrences    int
	unrepresentedOccurrences int
}

// Add validates and incorporates one final diagnostic group. Actionable
// warnings cannot overflow; later non-warning groups are represented by exact
// unrepresented-occurrence metadata once their fixed bound is reached.
func (aggregator *Aggregator) Add(group contracts.DiagnosticGroup) error {
	if err := group.Validate(); err != nil {
		return fmt.Errorf("diagnostic group: %w", err)
	}
	if aggregator.indices == nil {
		aggregator.indices = make(map[groupKey]int)
	}
	key := groupKeyFromGroup(group)
	if index, exists := aggregator.indices[key]; exists {
		if err := aggregator.checkOccurrenceTotal(group.Disposition, group.OccurrenceCount); err != nil {
			return err
		}
		if err := aggregator.merge(index, group); err != nil {
			return err
		}
		aggregator.addOccurrenceTotal(group.Disposition, group.OccurrenceCount)
		return nil
	}
	if group.Disposition == contracts.DiagnosticDispositionWarning {
		if aggregator.warningGroups >= MaxWarningGroups {
			return errors.New("diagnostic warning groups exceed maximum count")
		}
	} else if aggregator.nonWarningGroups >= MaxNonWarningGroups {
		if err := aggregator.checkOccurrenceTotal(group.Disposition, group.OccurrenceCount); err != nil {
			return err
		}
		aggregator.addOccurrenceTotal(group.Disposition, group.OccurrenceCount)
		return aggregator.addUnrepresented(group.OccurrenceCount)
	}
	if err := aggregator.checkOccurrenceTotal(group.Disposition, group.OccurrenceCount); err != nil {
		return err
	}
	if group.Disposition == contracts.DiagnosticDispositionWarning {
		aggregator.warningGroups++
	} else {
		aggregator.nonWarningGroups++
	}
	aggregator.addOccurrenceTotal(group.Disposition, group.OccurrenceCount)
	aggregator.indices[key] = len(aggregator.groups)
	aggregator.groups = append(aggregator.groups, contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: []contracts.DiagnosticGroup{group}}).Groups[0])
	return nil
}

func (aggregator *Aggregator) checkOccurrenceTotal(disposition contracts.DiagnosticDisposition, count int) error {
	current := aggregator.nonWarningOccurrences
	if disposition == contracts.DiagnosticDispositionWarning {
		current = aggregator.warningOccurrences
	}
	if count > maximumInt()-current {
		return errors.New("diagnostic occurrence count overflow")
	}
	return nil
}

func (aggregator *Aggregator) addOccurrenceTotal(disposition contracts.DiagnosticDisposition, count int) {
	if disposition == contracts.DiagnosticDispositionWarning {
		aggregator.warningOccurrences += count
		return
	}
	aggregator.nonWarningOccurrences += count
}

// Collection returns an independently owned grouped result in first-occurrence
// order.
func (aggregator *Aggregator) Collection() contracts.DiagnosticCollection {
	if aggregator == nil {
		return contracts.DiagnosticCollection{}
	}
	return contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{
		Groups:                       aggregator.groups,
		Truncated:                    aggregator.unrepresentedOccurrences > 0,
		UnrepresentedOccurrenceCount: aggregator.unrepresentedOccurrences,
	})
}

func (aggregator *Aggregator) merge(index int, incoming contracts.DiagnosticGroup) error {
	current := &aggregator.groups[index]
	if incoming.OccurrenceCount > maximumInt()-current.OccurrenceCount {
		return errors.New("diagnostic occurrence count overflow")
	}
	current.OccurrenceCount += incoming.OccurrenceCount
	for _, sample := range incoming.Samples {
		if len(current.Samples) == contracts.MaxDiagnosticSamples || containsGroupSample(current.Samples, sample) {
			continue
		}
		current.Samples = append(current.Samples, cloneGroupSample(sample))
	}
	current.OmittedSampleCount = current.OccurrenceCount - len(current.Samples)
	return nil
}

func (aggregator *Aggregator) addUnrepresented(count int) error {
	if count > maximumInt()-aggregator.unrepresentedOccurrences {
		return errors.New("diagnostic unrepresented occurrence count overflow")
	}
	aggregator.unrepresentedOccurrences += count
	return nil
}

func containsGroupSample(samples []contracts.DiagnosticSample, candidate contracts.DiagnosticSample) bool {
	for _, sample := range samples {
		if sample.Scope != candidate.Scope || sample.Message != candidate.Message || sample.ChunkID != candidate.ChunkID {
			continue
		}
		if sample.ChunkIndex == nil || candidate.ChunkIndex == nil {
			if sample.ChunkIndex == candidate.ChunkIndex {
				return true
			}
			continue
		}
		if *sample.ChunkIndex == *candidate.ChunkIndex {
			return true
		}
	}
	return false
}

func cloneGroupSample(sample contracts.DiagnosticSample) contracts.DiagnosticSample {
	if sample.ChunkIndex != nil {
		value := *sample.ChunkIndex
		sample.ChunkIndex = &value
	}
	return sample
}

type groupKey struct {
	disposition contracts.DiagnosticDisposition
	category    contracts.DiagnosticCategory
	reasonCode  string
	origin      contracts.DiagnosticOrigin
}

func groupKeyFromGroup(group contracts.DiagnosticGroup) groupKey {
	return groupKey{disposition: group.Disposition, category: group.Category, reasonCode: group.ReasonCode, origin: group.Origin}
}
