package runtimeapi

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	reportsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/reportv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

type ReportReadEngine interface {
	RunReportDefinitionPage(context.Context, *models.ReportDefinition, *models.UserSession, map[string]interface{}, int, int) (*models.ReportResult, error)
}

type ReportReadServer struct {
	runtimev1.UnimplementedReportReadServiceServer
	engine ReportReadEngine
}

const reportResultIRV1 = "nexuscrm.report-result/v1"

type reportResultDocumentV1 struct {
	Schema               string                     `json:"schema"`
	ReportID             string                     `json:"reportId"`
	Name                 string                     `json:"name"`
	DefinitionVersion    string                     `json:"definitionVersion"`
	DefinitionDigest     string                     `json:"definitionDigest"`
	Definition           reportDefinitionDocumentV1 `json:"definition"`
	DisplayMode          string                     `json:"displayMode"`
	Columns              []string                   `json:"columns"`
	Grouping             []string                   `json:"grouping"`
	Aggregates           []reportResultAggregateV1  `json:"aggregates"`
	Rows                 []models.SObject           `json:"rows"`
	TotalCount           int                        `json:"totalCount"`
	AggregateDedupColumn map[string]string          `json:"aggregateDedupColumn,omitempty"`
}

// reportDefinitionDocumentV1 is the immutable, app-blind definition selected
// by Catalog for this request. The top-level execution fields above remain the
// engine-normalized result shape used to project rows; this nested definition
// lets a consumer describe the exact saved report without rereading mutable
// repository rows or creating a second metadata authority.
type reportDefinitionDocumentV1 struct {
	SourceObject string                    `json:"sourceObject"`
	SourceRef    string                    `json:"sourceRef"`
	DisplayMode  string                    `json:"displayMode"`
	Scope        string                    `json:"scope"`
	Columns      []string                  `json:"columns"`
	Filters      []reportResultFilterV1    `json:"filters"`
	Grouping     []string                  `json:"grouping"`
	Aggregates   []reportResultAggregateV1 `json:"aggregates"`
	DateFilter   *reportResultDateFilterV1 `json:"dateFilter,omitempty"`
}

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

type reportResultDateFilterV1 struct {
	Column        string `json:"column"`
	DurationValue string `json:"durationValue"`
	StartDate     string `json:"startDate"`
	EndDate       string `json:"endDate"`
}

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

func NewReportReadServer(engine ReportReadEngine) (*ReportReadServer, error) {
	if engine == nil {
		return nil, errors.New("runtime report read: engine is required")
	}
	return &ReportReadServer{engine: engine}, nil
}

func (s *ReportReadServer) ReadReport(
	ctx context.Context,
	wireRequest *runtimev1.ReadReportRequest,
) (*runtimev1.ReadReportResponse, error) {
	request, err := mapping.FromWireReadReportRequest(ctx, wireRequest)
	if err != nil {
		return nil, mapping.ToPlatformStatusError(err, reportsemantic.CapabilityName)
	}
	scope, ok := runtimeExecutionScopeFromContext(ctx)
	view, viewOK := RuntimeMetadataViewFromContext(ctx)
	trusted, trustedOK := trustedCallFromContext(ctx)
	if !ok || !viewOK || !trustedOK {
		return nil, mapping.ToPlatformStatusError(
			&recordsemantic.Failure{Category: recordsemantic.FailureUnavailable},
			reportsemantic.CapabilityName,
		)
	}
	if err := requireCatalogRevision(request.MinCatalogRevision, scope.receipts.CatalogRevision); err != nil {
		return nil, mapping.ToPlatformStatusError(err, reportsemantic.CapabilityName)
	}
	definition, definitionVersion, definitionDigest, found := view.ReportDefinition(request.ReportID)
	if !found {
		return nil, mapping.ToPlatformStatusError(
			&recordsemantic.Failure{Category: recordsemantic.FailureMissing},
			reportsemantic.CapabilityName,
		)
	}
	selectedDefinition := cloneReportDefinition(definition)
	executionDefinition := cloneReportDefinition(definition)
	filters := map[string]interface{}{}
	decoder := json.NewDecoder(bytes.NewReader(request.CanonicalFilters))
	decoder.UseNumber()
	if err := decoder.Decode(&filters); err != nil {
		return nil, mapping.ToPlatformStatusError(
			&recordsemantic.Failure{Category: recordsemantic.FailureInvalidRequest},
			reportsemantic.CapabilityName,
		)
	}
	user := trusted.UserSession()
	result, err := s.engine.RunReportDefinitionPage(ctx, executionDefinition, &user, filters, int(request.Limit), int(request.Offset))
	if err != nil {
		return nil, mapping.ToPlatformStatusError(
			classifyPlatformEngineError(
				ctx, err, commonsemantic.InvalidComponentQueryPlan,
			),
			reportsemantic.CapabilityName,
		)
	}
	payload, err := marshalReportResultV1(result, selectedDefinition, definitionVersion, definitionDigest)
	if err != nil {
		return nil, mapping.ToPlatformStatusError(
			&recordsemantic.Failure{Category: recordsemantic.FailureInternal},
			reportsemantic.CapabilityName,
		)
	}
	canonicalPayload, err := mutationsemantic.CanonicalSourceJSON(string(payload))
	if err != nil {
		return nil, mapping.ToPlatformStatusError(
			&recordsemantic.Failure{Category: recordsemantic.FailureInternal},
			reportsemantic.CapabilityName,
		)
	}
	wireResponse, err := mapping.ToWireReadReportResponse(reportsemantic.Response{
		CanonicalResult: []byte(canonicalPayload),
		Receipts:        receiptsForCapability(scope.receipts, reportsemantic.CapabilityName),
	})
	if err != nil {
		return nil, mapping.ToPlatformStatusError(err, reportsemantic.CapabilityName)
	}
	return wireResponse, nil
}

func marshalReportResultV1(
	result *models.ReportResult,
	selected *models.ReportDefinition,
	version catalogsnapshot.DefinitionVersion,
	digest catalogsnapshot.Digest,
) ([]byte, error) {
	if result == nil || selected == nil || result.TotalCount < 0 ||
		len(result.FactMap) != 0 || len(result.Warnings) != 0 {
		return nil, errors.New("runtime report read: engine returned unsupported report result")
	}
	metadata := result.ReportMetadata
	metadata.NormalizeBoundaryAliases()
	if metadata.ID != selected.ID || metadata.SourceObject != selected.SourceObject ||
		metadata.SourceRef != selected.SourceRef {
		return nil, errors.New("runtime report read: engine changed immutable report identity")
	}
	if !validAggregateDedupColumns(metadata.Aggregates, result.Rows, result.AggregateDedupColumn) {
		return nil, errors.New("runtime report read: engine returned invalid aggregate dedup receipt")
	}
	document := reportResultDocumentV1{
		Schema: reportResultIRV1, ReportID: selected.ID, Name: selected.Name,
		DefinitionVersion: string(version), DefinitionDigest: string(digest),
		Definition:           reportDefinitionV1(selected),
		DisplayMode:          metadata.DisplayMode,
		Columns:              append([]string{}, metadata.Columns...),
		Grouping:             append([]string{}, metadata.Grouping...),
		Aggregates:           make([]reportResultAggregateV1, len(metadata.Aggregates)),
		Rows:                 append([]models.SObject{}, result.Rows...),
		TotalCount:           result.TotalCount,
		AggregateDedupColumn: cloneStringMap(result.AggregateDedupColumn),
	}
	for i, aggregate := range metadata.Aggregates {
		document.Aggregates[i] = reportResultAggregateV1(aggregate)
	}
	return json.Marshal(document)
}

func validAggregateDedupColumns(
	aggregates []models.ReportAggregate,
	rows []models.SObject,
	columns map[string]string,
) bool {
	if len(columns) == 0 {
		return true
	}
	fields := make(map[string]struct{}, len(aggregates))
	for _, aggregate := range aggregates {
		if aggregate.Field != "" {
			fields[aggregate.Field] = struct{}{}
		}
	}
	for field, column := range columns {
		if _, ok := fields[field]; !ok || column == "" {
			return false
		}
		for _, row := range rows {
			identity, ok := row[column].(string)
			if !ok || identity == "" {
				return false
			}
		}
	}
	return true
}

func cloneStringMap(source map[string]string) map[string]string {
	if len(source) == 0 {
		return nil
	}
	result := make(map[string]string, len(source))
	for key, value := range source {
		result[key] = value
	}
	return result
}

func reportDefinitionV1(selected *models.ReportDefinition) reportDefinitionDocumentV1 {
	document := reportDefinitionDocumentV1{
		SourceObject: selected.SourceObject,
		SourceRef:    selected.SourceRef,
		DisplayMode:  selected.DisplayMode,
		Scope:        selected.Scope,
		Columns:      append([]string{}, selected.Columns...),
		Filters:      make([]reportResultFilterV1, len(selected.Filters)),
		Grouping:     append([]string{}, selected.Grouping...),
		Aggregates:   make([]reportResultAggregateV1, len(selected.Aggregates)),
	}
	for i, filter := range selected.Filters {
		document.Filters[i] = reportResultFilterV1(filter)
	}
	for i, aggregate := range selected.Aggregates {
		document.Aggregates[i] = reportResultAggregateV1(aggregate)
	}
	if selected.DateFilter != nil {
		dateFilter := *selected.DateFilter
		if dateFilter.DurationValue == "" {
			dateFilter.DurationValue = dateFilter.Preset
		}
		document.DateFilter = &reportResultDateFilterV1{
			Column:        dateFilter.Column,
			DurationValue: dateFilter.DurationValue,
			StartDate:     dateFilter.StartDate, EndDate: dateFilter.EndDate,
		}
	}
	return document
}

func cloneReportDefinition(definition *models.ReportDefinition) *models.ReportDefinition {
	if definition == nil {
		return nil
	}
	clone := *definition
	clone.Columns = append([]string{}, definition.Columns...)
	clone.Filters = append([]models.ReportFilter{}, definition.Filters...)
	clone.Grouping = append([]string{}, definition.Grouping...)
	clone.Aggregates = append([]models.ReportAggregate{}, definition.Aggregates...)
	if definition.DateFilter != nil {
		dateFilter := *definition.DateFilter
		clone.DateFilter = &dateFilter
	}
	return &clone
}

var _ runtimev1.ReportReadServiceServer = (*ReportReadServer)(nil)
