package contracts

import (
	"errors"
	"fmt"
	"strings"
	"unicode/utf8"
)

const (
	MaxDiagnosticReasonCodeBytes = 128
	MaxDiagnosticScopeBytes      = 512
	MaxDiagnosticMessageBytes    = 4 * 1024
	MaxDiagnosticSamples         = 3
	MaxProducerDiagnosticGroups  = 64
)

// DiagnosticDisposition identifies the operator significance of a producer
// finding. Warnings are reserved for process-level degradation or incomplete
// configured work.
type DiagnosticDisposition string

const (
	DiagnosticDispositionWarning     DiagnosticDisposition = "warning"
	DiagnosticDispositionAdvisory    DiagnosticDisposition = "advisory"
	DiagnosticDispositionObservation DiagnosticDisposition = "observation"
)

// DiagnosticCategory gives a stable, bounded classification for a producer
// finding.
type DiagnosticCategory string

const (
	DiagnosticCategoryConfiguration        DiagnosticCategory = "configuration"
	DiagnosticCategoryDegradation          DiagnosticCategory = "degradation"
	DiagnosticCategoryValidationIncomplete DiagnosticCategory = "validation_incomplete"
	DiagnosticCategoryFallback             DiagnosticCategory = "fallback"
	DiagnosticCategoryDataQuality          DiagnosticCategory = "data_quality"
	DiagnosticCategoryNormalization        DiagnosticCategory = "normalization"
)

// DiagnosticOriginStage identifies the framework operation that promoted a
// diagnostic. It is framework-owned rather than producer-owned.
type DiagnosticOriginStage string

const (
	DiagnosticOriginStageReferences DiagnosticOriginStage = "references"
	DiagnosticOriginStageChunk      DiagnosticOriginStage = "chunk"
	DiagnosticOriginStageExtract    DiagnosticOriginStage = "extract"
	DiagnosticOriginStageMerge      DiagnosticOriginStage = "merge"
	DiagnosticOriginStageNormalize  DiagnosticOriginStage = "normalize"
)

// DiagnosticSample is a bounded, safe example of a diagnostic occurrence.
// Chunk identity is attached by the framework when it promotes a producer
// diagnostic into a final group.
type DiagnosticSample struct {
	Scope      string `json:"scope"`
	Message    string `json:"message"`
	ChunkID    string `json:"chunk_id,omitempty"`
	ChunkIndex *int   `json:"chunk_index,omitempty"`
}

// ProducerDiagnostic is the locally grouped form returned by one producer or
// validator. It intentionally has no pipeline origin.
type ProducerDiagnostic struct {
	Disposition        DiagnosticDisposition `json:"disposition"`
	Category           DiagnosticCategory    `json:"category"`
	ReasonCode         string                `json:"reason_code"`
	OccurrenceCount    int                   `json:"occurrence_count"`
	Samples            []DiagnosticSample    `json:"samples"`
	OmittedSampleCount int                   `json:"omitted_sample_count"`
}

// DiagnosticOrigin is framework-owned context used to distinguish findings
// from different pipeline locations during final aggregation.
type DiagnosticOrigin struct {
	Stage        DiagnosticOriginStage `json:"stage"`
	StepID       string                `json:"step_id,omitempty"`
	LaneID       string                `json:"lane_id,omitempty"`
	ModuleKey    string                `json:"module_key,omitempty"`
	ValidatorKey string                `json:"validator_key,omitempty"`
}

// DiagnosticGroup is a producer diagnostic after framework origin enrichment.
type DiagnosticGroup struct {
	Disposition        DiagnosticDisposition `json:"disposition"`
	Category           DiagnosticCategory    `json:"category"`
	ReasonCode         string                `json:"reason_code"`
	Origin             DiagnosticOrigin      `json:"origin"`
	OccurrenceCount    int                   `json:"occurrence_count"`
	Samples            []DiagnosticSample    `json:"samples"`
	OmittedSampleCount int                   `json:"omitted_sample_count"`
}

// DiagnosticCollection is the grouped collection supplied to later durable
// and presentation boundaries. Global aggregation policy is applied by the
// framework before it reaches those boundaries.
type DiagnosticCollection struct {
	Groups                       []DiagnosticGroup `json:"groups"`
	Truncated                    bool              `json:"truncated"`
	UnrepresentedOccurrenceCount int               `json:"unrepresented_occurrence_count"`
}

// DiagnosticProjection is the validated warning/non-warning view used by
// durable and presentation boundaries. Occurrence totals are checked before
// they leave the framework contract.
type DiagnosticProjection struct {
	Warnings                  []DiagnosticGroup
	Diagnostics               []DiagnosticGroup
	WarningOccurrenceCount    int
	DiagnosticOccurrenceCount int
}

// Validate checks a producer-local diagnostic against the public safety and
// classification contract.
func (diagnostic ProducerDiagnostic) Validate() error {
	return validateDiagnostic(
		diagnostic.Disposition,
		diagnostic.Category,
		diagnostic.ReasonCode,
		diagnostic.OccurrenceCount,
		diagnostic.Samples,
		diagnostic.OmittedSampleCount,
		false,
	)
}

// ValidateProducerDiagnostics validates the complete set returned by one
// producer or validator result.
func ValidateProducerDiagnostics(diagnostics []ProducerDiagnostic) error {
	if len(diagnostics) > MaxProducerDiagnosticGroups {
		return errors.New("producer diagnostics exceed maximum group count")
	}
	for index, diagnostic := range diagnostics {
		if err := diagnostic.Validate(); err != nil {
			return fmt.Errorf("producer diagnostic %d: %w", index, err)
		}
	}
	return nil
}

// Validate checks framework-owned origin fields.
func (origin DiagnosticOrigin) Validate() error {
	switch origin.Stage {
	case DiagnosticOriginStageReferences, DiagnosticOriginStageChunk, DiagnosticOriginStageExtract, DiagnosticOriginStageMerge, DiagnosticOriginStageNormalize:
	default:
		return errors.New("diagnostic origin stage is invalid")
	}
	for _, field := range []struct {
		name  string
		value string
	}{
		{name: "diagnostic origin step ID", value: origin.StepID},
		{name: "diagnostic origin lane ID", value: origin.LaneID},
		{name: "diagnostic origin module key", value: origin.ModuleKey},
		{name: "diagnostic origin validator key", value: origin.ValidatorKey},
	} {
		if field.value != "" && (!utf8.ValidString(field.value) || strings.TrimSpace(field.value) == "") {
			return fmt.Errorf("%s must be valid nonblank UTF-8 when present", field.name)
		}
	}
	return nil
}

// Validate checks a final origin-enriched group.
func (group DiagnosticGroup) Validate() error {
	if err := group.Origin.Validate(); err != nil {
		return err
	}
	return validateDiagnostic(
		group.Disposition,
		group.Category,
		group.ReasonCode,
		group.OccurrenceCount,
		group.Samples,
		group.OmittedSampleCount,
		true,
	)
}

// Validate checks the collection shape without imposing later global
// aggregation limits.
func (collection DiagnosticCollection) Validate() error {
	if collection.UnrepresentedOccurrenceCount < 0 {
		return errors.New("diagnostic collection unrepresented occurrence count must not be negative")
	}
	if !collection.Truncated && collection.UnrepresentedOccurrenceCount != 0 {
		return errors.New("diagnostic collection has unrepresented occurrences without truncation")
	}
	seen := make(map[diagnosticGroupKey]struct{}, len(collection.Groups))
	for index, group := range collection.Groups {
		if err := group.Validate(); err != nil {
			return fmt.Errorf("diagnostic group %d: %w", index, err)
		}
		key := diagnosticGroupKeyFromGroup(group)
		if _, exists := seen[key]; exists {
			return errors.New("diagnostic collection contains duplicate group identity")
		}
		seen[key] = struct{}{}
	}
	return nil
}

// ProjectDiagnosticCollection validates, partitions, and totals one finalized
// collection. Unrepresented occurrences belong to the non-warning projection.
func ProjectDiagnosticCollection(collection DiagnosticCollection) (DiagnosticProjection, error) {
	if err := collection.Validate(); err != nil {
		return DiagnosticProjection{}, err
	}
	projection := DiagnosticProjection{
		Warnings:    make([]DiagnosticGroup, 0),
		Diagnostics: make([]DiagnosticGroup, 0),
	}
	for _, group := range collection.Groups {
		if group.Disposition == DiagnosticDispositionWarning {
			count, err := addDiagnosticOccurrences(projection.WarningOccurrenceCount, group.OccurrenceCount)
			if err != nil {
				return DiagnosticProjection{}, fmt.Errorf("warning occurrences: %w", err)
			}
			projection.WarningOccurrenceCount = count
			projection.Warnings = append(projection.Warnings, cloneDiagnosticGroup(group))
			continue
		}
		count, err := addDiagnosticOccurrences(projection.DiagnosticOccurrenceCount, group.OccurrenceCount)
		if err != nil {
			return DiagnosticProjection{}, fmt.Errorf("diagnostic occurrences: %w", err)
		}
		projection.DiagnosticOccurrenceCount = count
		projection.Diagnostics = append(projection.Diagnostics, cloneDiagnosticGroup(group))
	}
	count, err := addDiagnosticOccurrences(projection.DiagnosticOccurrenceCount, collection.UnrepresentedOccurrenceCount)
	if err != nil {
		return DiagnosticProjection{}, fmt.Errorf("diagnostic occurrences: %w", err)
	}
	projection.DiagnosticOccurrenceCount = count
	return projection, nil
}

// CloneProducerDiagnostics returns independent diagnostic slice ownership.
func CloneProducerDiagnostics(diagnostics []ProducerDiagnostic) []ProducerDiagnostic {
	if len(diagnostics) == 0 {
		return nil
	}
	cloned := make([]ProducerDiagnostic, len(diagnostics))
	for index, diagnostic := range diagnostics {
		cloned[index] = cloneProducerDiagnostic(diagnostic)
	}
	return cloned
}

// CloneDiagnosticCollection returns independent collection ownership.
func CloneDiagnosticCollection(collection DiagnosticCollection) DiagnosticCollection {
	groups := collection.Groups
	collection.Groups = make([]DiagnosticGroup, len(groups))
	for index, group := range groups {
		collection.Groups[index] = cloneDiagnosticGroup(group)
	}
	return collection
}

func validateDiagnostic(disposition DiagnosticDisposition, category DiagnosticCategory, reasonCode string, occurrenceCount int, samples []DiagnosticSample, omittedSampleCount int, allowChunkContext bool) error {
	if !diagnosticCategoryAllowed(disposition, category) {
		return errors.New("diagnostic disposition and category combination is invalid")
	}
	if err := validateDiagnosticText(reasonCode, MaxDiagnosticReasonCodeBytes, "diagnostic reason code"); err != nil {
		return err
	}
	if occurrenceCount <= 0 {
		return errors.New("diagnostic occurrence count must be positive")
	}
	if len(samples) == 0 {
		return errors.New("diagnostic samples must not be empty")
	}
	if len(samples) > MaxDiagnosticSamples {
		return errors.New("diagnostic samples exceed maximum count")
	}
	seen := make(map[diagnosticSampleKey]struct{}, len(samples))
	for index, sample := range samples {
		if err := validateDiagnosticSample(sample, allowChunkContext); err != nil {
			return fmt.Errorf("diagnostic sample %d: %w", index, err)
		}
		key := diagnosticSampleKeyFromSample(sample)
		if _, exists := seen[key]; exists {
			return errors.New("diagnostic samples must be distinct")
		}
		seen[key] = struct{}{}
	}
	if occurrenceCount < len(samples) {
		return errors.New("diagnostic occurrence count is smaller than sample count")
	}
	if omittedSampleCount != occurrenceCount-len(samples) {
		return errors.New("diagnostic omitted sample count is inconsistent")
	}
	return nil
}

func diagnosticCategoryAllowed(disposition DiagnosticDisposition, category DiagnosticCategory) bool {
	switch disposition {
	case DiagnosticDispositionWarning:
		return category == DiagnosticCategoryConfiguration || category == DiagnosticCategoryDegradation || category == DiagnosticCategoryValidationIncomplete || category == DiagnosticCategoryFallback
	case DiagnosticDispositionAdvisory:
		return category == DiagnosticCategoryDataQuality
	case DiagnosticDispositionObservation:
		return category == DiagnosticCategoryNormalization
	default:
		return false
	}
}

func validateDiagnosticSample(sample DiagnosticSample, allowChunkContext bool) error {
	if err := validateDiagnosticText(sample.Scope, MaxDiagnosticScopeBytes, "diagnostic sample scope"); err != nil {
		return err
	}
	if err := validateDiagnosticText(sample.Message, MaxDiagnosticMessageBytes, "diagnostic sample message"); err != nil {
		return err
	}
	if !allowChunkContext && (sample.ChunkID != "" || sample.ChunkIndex != nil) {
		return errors.New("producer diagnostic sample must not include framework chunk context")
	}
	if sample.ChunkID != "" && (!utf8.ValidString(sample.ChunkID) || strings.TrimSpace(sample.ChunkID) == "") {
		return errors.New("diagnostic sample chunk ID must be valid nonblank UTF-8 when present")
	}
	if sample.ChunkIndex != nil && *sample.ChunkIndex < 0 {
		return errors.New("diagnostic sample chunk index must not be negative")
	}
	return nil
}

func validateDiagnosticText(value string, maximum int, name string) error {
	if !utf8.ValidString(value) {
		return fmt.Errorf("%s must be valid UTF-8", name)
	}
	if strings.TrimSpace(value) == "" {
		return fmt.Errorf("%s must not be blank", name)
	}
	if len(value) > maximum {
		return fmt.Errorf("%s exceeds maximum length", name)
	}
	return nil
}

func cloneProducerDiagnostic(diagnostic ProducerDiagnostic) ProducerDiagnostic {
	diagnostic.Samples = cloneDiagnosticSamples(diagnostic.Samples)
	return diagnostic
}

func cloneDiagnosticGroup(group DiagnosticGroup) DiagnosticGroup {
	group.Samples = cloneDiagnosticSamples(group.Samples)
	return group
}

func addDiagnosticOccurrences(current, incoming int) (int, error) {
	if incoming > int(^uint(0)>>1)-current {
		return 0, errors.New("occurrence count overflow")
	}
	return current + incoming, nil
}

func cloneDiagnosticSamples(samples []DiagnosticSample) []DiagnosticSample {
	if len(samples) == 0 {
		return nil
	}
	cloned := make([]DiagnosticSample, len(samples))
	for index, sample := range samples {
		if sample.ChunkIndex != nil {
			chunkIndex := *sample.ChunkIndex
			sample.ChunkIndex = &chunkIndex
		}
		cloned[index] = sample
	}
	return cloned
}

type diagnosticSampleKey struct {
	scope         string
	message       string
	chunkID       string
	chunkIndex    int
	hasChunkIndex bool
}

func diagnosticSampleKeyFromSample(sample DiagnosticSample) diagnosticSampleKey {
	key := diagnosticSampleKey{scope: sample.Scope, message: sample.Message, chunkID: sample.ChunkID}
	if sample.ChunkIndex != nil {
		key.chunkIndex = *sample.ChunkIndex
		key.hasChunkIndex = true
	}
	return key
}

type diagnosticGroupKey struct {
	disposition DiagnosticDisposition
	category    DiagnosticCategory
	reasonCode  string
	origin      DiagnosticOrigin
}

func diagnosticGroupKeyFromGroup(group DiagnosticGroup) diagnosticGroupKey {
	return diagnosticGroupKey{disposition: group.Disposition, category: group.Category, reasonCode: group.ReasonCode, origin: group.Origin}
}
