package catalogsnapshot

import (
	"fmt"
	"sort"
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
)

const (
	// ReportDefinitionIRV1 is the canonical, app-neutral grammar carried by a
	// metadata.report.v1 AppRelease component and the report Runtime view.
	ReportDefinitionIRV1 = "nexuscrm.report-definition/v1"

	maxReportDefinitionItems = 512
	maxReportTextBytes       = 8 * 1024
)

func inputHasReportExecutionProjection(values []RuntimeProjectionInput) bool {
	for _, value := range values {
		if value.Profile == ProjectionReportExecutionV1 {
			return true
		}
	}
	return false
}

type reportDefinitionRecord struct {
	Schema            string                  `json:"schema"`
	ComponentID       string                  `json:"componentId"`
	DefinitionVersion DefinitionVersion       `json:"definitionVersion"`
	ContentDigest     Digest                  `json:"contentDigest"`
	ID                string                  `json:"id"`
	Name              string                  `json:"name"`
	SourceObject      string                  `json:"sourceObject"`
	SourceRef         string                  `json:"sourceRef,omitempty"`
	DisplayMode       string                  `json:"displayMode"`
	Scope             string                  `json:"scope,omitempty"`
	DateFilter        *reportDateFilterRecord `json:"dateFilter,omitempty"`
	Columns           []string                `json:"columns"`
	Filters           []reportFilterRecord    `json:"filters"`
	Grouping          []string                `json:"grouping"`
	Aggregates        []reportAggregateRecord `json:"aggregates"`
}

type reportDateFilterRecord struct {
	Column    string `json:"column"`
	Preset    string `json:"preset,omitempty"`
	StartDate string `json:"startDate,omitempty"`
	EndDate   string `json:"endDate,omitempty"`
}

type reportFilterRecord struct {
	Column   string `json:"column"`
	Operator string `json:"operator"`
	Value    string `json:"value"`
}

type reportAggregateRecord struct {
	Field string `json:"field"`
	Type  string `json:"type"`
}

func buildReportDefinitions(
	revision CatalogRevision,
	inputs []ReportDefinitionInput,
	objects map[string]*models.ObjectMetadata,
) ([]reportDefinitionRecord, error) {
	result := make([]reportDefinitionRecord, 0, len(inputs))
	seenComponents := make(map[string]struct{}, len(inputs))
	seenIDs := make(map[string]struct{}, len(inputs))
	for i, input := range inputs {
		path := fmt.Sprintf("reportDefinitions[%d]", i)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		if err := requireCanonicalIdentity(path+".componentId", input.ComponentID); err != nil {
			return nil, err
		}
		if strings.TrimSpace(string(input.DefinitionVersion)) == "" ||
			strings.TrimSpace(string(input.DefinitionVersion)) != string(input.DefinitionVersion) {
			return nil, conflict(ConflictInvalidDefinition, path+".definitionVersion", "definition version must be non-blank and canonical")
		}
		if !validDigest(input.ContentDigest) {
			return nil, conflict(ConflictInvalidDefinition, path+".contentDigest", "content digest %q is invalid", input.ContentDigest)
		}
		componentKey := foldIdentity(input.ComponentID)
		if _, exists := seenComponents[componentKey]; exists {
			return nil, conflict(ConflictInvalidDefinition, path+".componentId", "report component %q is duplicated", input.ComponentID)
		}
		seenComponents[componentKey] = struct{}{}

		record, err := reportDefinitionRecordFromModel(input, path)
		if err != nil {
			return nil, err
		}
		if err := validateReportDefinitionRecord(path, record, objects); err != nil {
			return nil, err
		}
		idKey := foldIdentity(record.ID)
		if _, exists := seenIDs[idKey]; exists {
			return nil, conflict(ConflictInvalidDefinition, path+".id", "report id %q is duplicated", record.ID)
		}
		seenIDs[idKey] = struct{}{}
		result = append(result, record)
	}
	sort.Slice(result, func(i, j int) bool {
		return identityLess(result[i].ComponentID, result[j].ComponentID)
	})
	return result, nil
}

func reportDefinitionRecordFromModel(input ReportDefinitionInput, path string) (reportDefinitionRecord, error) {
	if input.Definition == nil {
		return reportDefinitionRecord{}, conflict(ConflictInvalidDefinition, path+".definition", "report definition is required")
	}
	definition, err := cloneJSON(*input.Definition)
	if err != nil {
		return reportDefinitionRecord{}, conflict(ConflictEncoding, path+".definition", "clone report definition: %v", err)
	}
	definition.NormalizeBoundaryAliases()
	record := reportDefinitionRecord{
		Schema: ReportDefinitionIRV1, ComponentID: input.ComponentID,
		DefinitionVersion: input.DefinitionVersion, ContentDigest: input.ContentDigest,
		ID: definition.ID, Name: definition.Name, SourceObject: definition.SourceObject,
		SourceRef: definition.SourceRef, DisplayMode: definition.DisplayMode, Scope: definition.Scope,
		Columns:    append([]string(nil), definition.Columns...),
		Filters:    make([]reportFilterRecord, len(definition.Filters)),
		Grouping:   append([]string(nil), definition.Grouping...),
		Aggregates: make([]reportAggregateRecord, len(definition.Aggregates)),
	}
	if definition.DateFilter != nil {
		record.DateFilter = &reportDateFilterRecord{
			Column: definition.DateFilter.Column, Preset: definition.DateFilter.Preset,
			StartDate: definition.DateFilter.StartDate, EndDate: definition.DateFilter.EndDate,
		}
	}
	for i, filter := range definition.Filters {
		record.Filters[i] = reportFilterRecord(filter)
	}
	for i, aggregate := range definition.Aggregates {
		record.Aggregates[i] = reportAggregateRecord(aggregate)
	}
	if definition.FilterLogic != "" {
		return reportDefinitionRecord{}, conflict(ConflictInvalidDefinition, path+".definition.filterLogic", "v1 neutral report IR does not support product filter logic")
	}
	if definition.DegradeOnError {
		return reportDefinitionRecord{}, conflict(ConflictInvalidDefinition, path+".definition.degradeOnError", "published reports must fail closed")
	}
	return record, nil
}

func validateReportDefinitionRecord(path string, record reportDefinitionRecord, objects map[string]*models.ObjectMetadata) error {
	if record.Schema != ReportDefinitionIRV1 {
		return conflict(ConflictInvalidDefinition, path+".schema", "unsupported report definition schema %q", record.Schema)
	}
	for name, value := range map[string]string{
		"componentId": record.ComponentID, "id": record.ID, "name": record.Name,
		"sourceObject": record.SourceObject, "displayMode": record.DisplayMode,
	} {
		if err := requireBoundedReportText(path+"."+name, value, true); err != nil {
			return err
		}
	}
	if record.SourceRef != "" && record.SourceRef != record.SourceObject {
		return conflict(ConflictInvalidDefinition, path+".sourceRef", "v1 report IR sourceRef must be empty or equal sourceObject")
	}
	if record.DisplayMode != "TABULAR" && record.DisplayMode != "SUMMARY" {
		return conflict(ConflictInvalidDefinition, path+".displayMode", "unsupported neutral display mode %q", record.DisplayMode)
	}
	switch record.Scope {
	case "", "all", "mine", "myTeam":
	default:
		return conflict(ConflictInvalidDefinition, path+".scope", "unsupported report scope %q", record.Scope)
	}
	if len(record.Columns) > maxReportDefinitionItems || len(record.Filters) > maxReportDefinitionItems ||
		len(record.Grouping) > maxReportDefinitionItems || len(record.Aggregates) > maxReportDefinitionItems {
		return conflict(ConflictInvalidDefinition, path, "report definition exceeds the %d-item bound", maxReportDefinitionItems)
	}
	object := objects[foldIdentity(record.SourceObject)]
	if object == nil {
		return conflict(ConflictInvalidDefinition, path+".sourceObject", "report source object %q is absent from this Catalog revision", record.SourceObject)
	}
	fields := make(map[string]struct{}, len(object.Fields))
	for _, field := range object.Fields {
		fields[foldIdentity(field.APIName)] = struct{}{}
	}
	validateField := func(fieldPath, value string) error {
		if err := requireBoundedReportText(fieldPath, value, true); err != nil {
			return err
		}
		if _, ok := fields[foldIdentity(value)]; !ok {
			return conflict(ConflictInvalidDefinition, fieldPath, "field %q is absent from report source object %q", value, record.SourceObject)
		}
		return nil
	}
	seenColumns := map[string]struct{}{}
	for i, value := range record.Columns {
		fieldPath := fmt.Sprintf("%s.columns[%d]", path, i)
		if err := validateField(fieldPath, value); err != nil {
			return err
		}
		key := foldIdentity(value)
		if _, exists := seenColumns[key]; exists {
			return conflict(ConflictInvalidDefinition, fieldPath, "report column %q is duplicated", value)
		}
		seenColumns[key] = struct{}{}
	}
	for i, value := range record.Grouping {
		if err := validateField(fmt.Sprintf("%s.grouping[%d]", path, i), value); err != nil {
			return err
		}
	}
	for i, value := range record.Filters {
		filterPath := fmt.Sprintf("%s.filters[%d]", path, i)
		if err := validateField(filterPath+".column", value.Column); err != nil {
			return err
		}
		if err := requireBoundedReportText(filterPath+".operator", value.Operator, true); err != nil {
			return err
		}
		if err := requireBoundedReportText(filterPath+".value", value.Value, false); err != nil {
			return err
		}
	}
	for i, value := range record.Aggregates {
		aggregatePath := fmt.Sprintf("%s.aggregates[%d]", path, i)
		if err := validateField(aggregatePath+".field", value.Field); err != nil {
			return err
		}
		switch value.Type {
		case "COUNT", "SUM", "AVG", "MIN", "MAX":
		default:
			return conflict(ConflictInvalidDefinition, aggregatePath+".type", "unsupported neutral aggregate %q", value.Type)
		}
	}
	if record.DateFilter != nil {
		if err := validateField(path+".dateFilter.column", record.DateFilter.Column); err != nil {
			return err
		}
		for name, value := range map[string]string{
			"preset": record.DateFilter.Preset, "startDate": record.DateFilter.StartDate, "endDate": record.DateFilter.EndDate,
		} {
			if err := requireBoundedReportText(path+".dateFilter."+name, value, false); err != nil {
				return err
			}
		}
	}
	return nil
}

func requireBoundedReportText(path, value string, required bool) error {
	if required && (value == "" || strings.TrimSpace(value) != value) {
		return conflict(ConflictInvalidDefinition, path, "value must be non-blank and canonical")
	}
	if len(value) > maxReportTextBytes {
		return conflict(ConflictInvalidDefinition, path, "value exceeds %d bytes", maxReportTextBytes)
	}
	return nil
}

func reportDefinitionToModel(record reportDefinitionRecord) *models.ReportDefinition {
	definition := &models.ReportDefinition{
		ID: record.ID, Name: record.Name, SourceObject: record.SourceObject,
		SourceRef: record.SourceRef, DisplayMode: record.DisplayMode, Scope: record.Scope,
		Columns:    append([]string(nil), record.Columns...),
		Filters:    make([]models.ReportFilter, len(record.Filters)),
		Grouping:   append([]string(nil), record.Grouping...),
		Aggregates: make([]models.ReportAggregate, len(record.Aggregates)),
	}
	if record.DateFilter != nil {
		definition.DateFilter = &models.ReportDateFilter{
			Column: record.DateFilter.Column, Preset: record.DateFilter.Preset,
			DurationValue: record.DateFilter.Preset,
			StartDate:     record.DateFilter.StartDate, EndDate: record.DateFilter.EndDate,
		}
	}
	for i, filter := range record.Filters {
		definition.Filters[i] = models.ReportFilter(filter)
	}
	for i, aggregate := range record.Aggregates {
		definition.Aggregates[i] = models.ReportAggregate(aggregate)
	}
	return definition
}

// ReportDefinition resolves an exact AppRelease-owned report from the
// request-bound immutable Runtime view.
func (v *RuntimeMetadataView) ReportDefinition(reportID string) (*models.ReportDefinition, DefinitionVersion, Digest, bool) {
	if v == nil {
		return nil, "", "", false
	}
	index, ok := v.reportDefinitionsByID[foldIdentity(reportID)]
	if !ok {
		return nil, "", "", false
	}
	record := v.doc.Reports[index]
	return reportDefinitionToModel(record), record.DefinitionVersion, record.ContentDigest, true
}
