package dialectsfcrm

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

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

const opportunityHistoryPageSize = 1000

var opportunitySnapshotColumnFields = map[string]string{
	"Probability":      "Probability",
	"CloseDate":        "CloseDate",
	"ForecastCategory": "ForecastCategory",
}

func newOpportunityHistoryReportSourceRegistration(query contract.Query) contract.VirtualReportSourceRegistration {
	registration, _ := newOpportunityHistoryReportSourceRegistrationWithColumns(query, nil)
	return registration
}

func newOpportunityHistoryReportSourceRegistrationWithColumns(query contract.Query, extensions []OpportunityHistoryReportColumn) (contract.VirtualReportSourceRegistration, error) {
	extensions, err := validateOpportunityHistoryReportColumns(extensions)
	if err != nil {
		return contract.VirtualReportSourceRegistration{}, err
	}
	stageRef := []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "StageName"}}
	columns := []contract.VirtualReportColumn{
		{Name: "Id", MetadataOnly: true},
		{Name: "OpportunityId", MetadataOnly: true},
		{Name: "OpportunityName", FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "Name"}}},
		{Name: "OwnerId", FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "OwnerId"}}},
		{Name: "OwnerName", JoinRef: "owner", FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "User", FieldAPIName: "Name"}}},
		{Name: "OwnerRole", JoinRef: "ownerRole", FieldReferences: []contract.VirtualReportFieldReference{
			{ObjectAPIName: "User", FieldAPIName: "UserRoleId"},
			{ObjectAPIName: "UserRole", FieldAPIName: "Name"},
		}},
		{Name: "FromStageName", Derived: true, FieldReferences: stageRef},
		{Name: "StageName", FieldReferences: stageRef},
		{Name: "StageDuration", Derived: true, FieldReferences: stageRef, Deriver: opportunityHistoryDurationDeriver{}},
		{Name: "LastStageChangeDate", Derived: true, FieldReferences: stageRef},
		{Name: "Probability", Derived: true, FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "Probability"}}},
		{Name: "CloseDate", Derived: true, FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "CloseDate"}}},
		{Name: "ForecastCategory", Derived: true, FieldReferences: []contract.VirtualReportFieldReference{{ObjectAPIName: "Opportunity", FieldAPIName: "ForecastCategory"}}},
		{Name: "CreatedDate", FieldReferences: stageRef},
	}
	for _, extension := range extensions {
		columns = append(columns, contract.VirtualReportColumn{
			Name:    extension.Name,
			Derived: extension.HistoricalSnapshot,
			FieldReferences: []contract.VirtualReportFieldReference{{
				ObjectAPIName: "Opportunity",
				FieldAPIName:  extension.FieldAPIName,
			}},
		})
	}
	return contract.VirtualReportSourceRegistration{
		SourceRef:           "OpportunityHistory",
		ParentObjectAPIName: "Opportunity",
		Capabilities: contract.VirtualReportSourceCapabilities{
			Filters:    true,
			Grouping:   true,
			Joins:      true,
			Derived:    true,
			Pagination: true,
		},
		Columns: columns,
		Joins: []contract.VirtualReportJoin{
			{Name: "owner", ObjectAPIName: "User", SourceFieldAPIName: "OwnerId"},
			{Name: "ownerRole", ObjectAPIName: "UserRole", SourceRecordJoinRef: "owner", SourceFieldAPIName: "UserRoleId", Optional: true},
		},
		Source: &opportunityHistoryReportSource{query: query, extensions: extensions},
	}, nil
}

type opportunityHistoryReportSource struct {
	query      contract.Query
	extensions []OpportunityHistoryReportColumn
}

func validateOpportunityHistoryReportColumns(in []OpportunityHistoryReportColumn) ([]OpportunityHistoryReportColumn, error) {
	baseNames := map[string]struct{}{}
	for _, name := range []string{
		"Id", "OpportunityId", "OpportunityName", "OwnerId", "OwnerName", "OwnerRole",
		"FromStageName", "StageName", "StageDuration", "LastStageChangeDate",
		"Probability", "CloseDate", "ForecastCategory", "CreatedDate",
	} {
		baseNames[strings.ToLower(name)] = struct{}{}
	}
	out := append([]OpportunityHistoryReportColumn(nil), in...)
	fields := make(map[string]struct{}, len(out))
	for i := range out {
		out[i].Name = strings.TrimSpace(out[i].Name)
		out[i].FieldAPIName = strings.TrimSpace(out[i].FieldAPIName)
		nameKey := strings.ToLower(out[i].Name)
		fieldKey := strings.ToLower(out[i].FieldAPIName)
		if !metadataDeploySafeIdentifier.MatchString(out[i].Name) || !metadataDeploySafeIdentifier.MatchString(out[i].FieldAPIName) {
			return nil, fmt.Errorf("dialect-sf-crm: invalid OpportunityHistory extension column %q for field %q", out[i].Name, out[i].FieldAPIName)
		}
		if _, exists := baseNames[nameKey]; exists {
			return nil, fmt.Errorf("dialect-sf-crm: OpportunityHistory extension column %q collides with a product column", out[i].Name)
		}
		if _, exists := fields[fieldKey]; exists {
			return nil, fmt.Errorf("dialect-sf-crm: OpportunityHistory extension field %q is mapped more than once", out[i].FieldAPIName)
		}
		baseNames[nameKey] = struct{}{}
		fields[fieldKey] = struct{}{}
	}
	return out, nil
}

func (s *opportunityHistoryReportSource) Execute(ctx context.Context, request contract.VirtualReportSourceRequest, user *models.UserSession) (*contract.VirtualReportSourceResult, error) {
	if err := validateOpportunityHistoryRequest(request.Definition); err != nil {
		return nil, err
	}
	historyFields := opportunityHistoryFieldsForRequest(request.Columns, s.extensions)
	historyRows, err := s.loadHistoryRows(ctx, historyFields, user)
	if err != nil {
		return nil, err
	}
	if len(historyRows) == 0 {
		return &contract.VirtualReportSourceResult{}, nil
	}
	parentFields := opportunityParentFieldsForRequest(request.Columns, historyFields, s.extensions)
	parents, err := s.loadRecords(ctx, "Opportunity", historyParentIDs(historyRows), parentFields, user)
	if err != nil {
		return nil, err
	}
	owners, roles, err := s.loadOpportunityOwnerFacts(ctx, parents, request.Columns, user)
	if err != nil {
		return nil, err
	}
	rows, err := buildOpportunityHistoryRows(historyRows, parents, owners, roles, s.extensions)
	if err != nil {
		return nil, err
	}
	filtered := make([]opportunityHistoryRow, 0, len(rows))
	for _, row := range rows {
		matches, err := opportunityHistoryRowMatches(row.values, request.Definition, request.RuntimeFilters, user)
		if err != nil {
			return nil, err
		}
		if matches {
			filtered = append(filtered, row)
		}
	}
	total := len(filtered)
	start := request.Offset
	if start > total {
		start = total
	}
	end := start + request.Limit
	if end > total {
		end = total
	}
	out := make([]contract.VirtualReportRow, 0, end-start)
	for _, row := range filtered[start:end] {
		values := make(map[string]contract.VirtualReportValue, len(request.Columns))
		accessRecords := []contract.VirtualReportAccessRecord{{
			ObjectAPIName: "Opportunity",
			RecordID:      opportunityHistoryRecordID(row.parent),
		}}
		joinedAccessRecords := make(map[string]struct{})
		addJoinedAccessRecord := func(objectName string, record contract.Record) {
			if _, exists := joinedAccessRecords[objectName]; exists {
				return
			}
			recordID := opportunityHistoryRecordID(record)
			if recordID == "" {
				return
			}
			accessRecords = append(accessRecords, contract.VirtualReportAccessRecord{ObjectAPIName: objectName, RecordID: recordID})
			joinedAccessRecords[objectName] = struct{}{}
		}
		for _, column := range request.Columns {
			value := row.projections[column.Name]
			switch column.JoinRef {
			case "owner":
				addJoinedAccessRecord("User", row.owner)
			case "ownerRole":
				addJoinedAccessRecord("User", row.owner)
				addJoinedAccessRecord("UserRole", row.role)
			}
			values[column.Name] = value
		}
		out = append(out, contract.VirtualReportRow{
			Values:          values,
			AccessRecords:   accessRecords,
			HistoryRecordID: row.historyRecordID,
		})
	}
	return &contract.VirtualReportSourceResult{TotalCount: total, Rows: out}, nil
}

func (s *opportunityHistoryReportSource) loadHistoryRows(ctx context.Context, fields []string, user *models.UserSession) ([]contract.HistoryRecord, error) {
	rows := make([]contract.HistoryRecord, 0, opportunityHistoryPageSize)
	for offset := 0; ; {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		result, err := s.query.QueryHistory(ctx, contract.HistoryQueryRequest{
			ParentObjectAPIName: "Opportunity",
			SelectFields:        fieldHistorySelectFields,
			Filter:              queryspec.FieldPredicate("Field", "IN", fields),
			SortDesc:            false,
			PageSize:            opportunityHistoryPageSize,
			CursorOffset:        offset,
		}, user)
		if err != nil {
			return nil, err
		}
		if result == nil || len(result.Rows) == 0 {
			if result != nil && !result.Done {
				return nil, fmt.Errorf("dialect-sf-crm: opportunity history query returned an empty incomplete page")
			}
			break
		}
		rows = append(rows, result.Rows...)
		offset += len(result.Rows)
		if result.Done {
			break
		}
	}
	return rows, nil
}

func (s *opportunityHistoryReportSource) loadRecords(ctx context.Context, objectName string, ids []string, selectFields []string, user *models.UserSession) (map[string]contract.Record, error) {
	const batchSize = 500
	records := make(map[string]contract.Record, len(ids))
	for start := 0; start < len(ids); start += batchSize {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		end := start + batchSize
		if end > len(ids) {
			end = len(ids)
		}
		result, err := s.query.Query(ctx, queryspec.QuerySpec{
			ObjectAPIName: objectName,
			SelectFields:  selectFields,
			Filter:        queryspec.FieldPredicate("Id", "IN", ids[start:end]),
			Limit:         end - start,
		}, user)
		if err != nil {
			return nil, err
		}
		if result == nil {
			continue
		}
		for _, record := range result.Records {
			id := opportunityHistoryRecordID(record)
			if id != "" {
				records[id] = record
			}
		}
	}
	return records, nil
}

func (s *opportunityHistoryReportSource) loadOpportunityOwnerFacts(ctx context.Context, parents map[string]contract.Record, columns []contract.VirtualReportColumn, user *models.UserSession) (map[string]contract.Record, map[string]contract.Record, error) {
	needOwner, needRole := false, false
	for _, column := range columns {
		needOwner = needOwner || column.Name == "OwnerName" || column.Name == "OwnerRole"
		needRole = needRole || column.Name == "OwnerRole"
	}
	if !needOwner {
		return nil, nil, nil
	}
	ownerIDs := make([]string, 0, len(parents))
	seen := map[string]struct{}{}
	for _, parent := range parents {
		id := opportunityHistoryRelatedRecordID(parent, "OwnerId")
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		ownerIDs = append(ownerIDs, id)
	}
	ownerFields := []string{"Id", "Name"}
	if needRole {
		ownerFields = append(ownerFields, "UserRoleId")
	}
	owners, err := s.loadRecords(ctx, "User", ownerIDs, ownerFields, user)
	if err != nil || !needRole {
		return owners, nil, err
	}
	roleIDs := make([]string, 0, len(owners))
	seen = map[string]struct{}{}
	for _, owner := range owners {
		id := opportunityHistoryRelatedRecordID(owner, "UserRoleId")
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		roleIDs = append(roleIDs, id)
	}
	roles, err := s.loadRecords(ctx, "UserRole", roleIDs, []string{"Id", "Name"}, user)
	if err != nil {
		return nil, nil, err
	}
	for _, id := range roleIDs {
		if _, ok := roles[id]; !ok {
			return nil, nil, fmt.Errorf("dialect-sf-crm: opportunity owner role %q is not readable", id)
		}
	}
	return owners, roles, nil
}

type opportunityHistoryRow struct {
	values          contract.Record
	projections     map[string]contract.VirtualReportValue
	historyRecordID string
	parent          contract.Record
	owner           contract.Record
	role            contract.Record
}

func physicalOpportunityHistoryValue(objectName, fieldName string) contract.VirtualReportValue {
	return contract.VirtualReportValue{AccessObjectAPIName: objectName, RecordFieldName: fieldName}
}

func opportunityHistoryRecordID(record contract.Record) string {
	if record == nil || record["Id"] == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(record["Id"]))
}

func opportunityHistoryRelatedRecordID(record contract.Record, field string) string {
	if record == nil || record[field] == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(record[field]))
}

func historyOpportunityHistoryValue(recordID string, field contract.VirtualReportHistoryField) contract.VirtualReportValue {
	return contract.VirtualReportValue{History: &contract.VirtualReportHistoryProjection{RecordID: recordID, Field: field}}
}

func opportunityHistoryValue(value *string) interface{} {
	if value == nil {
		return nil
	}
	return *value
}

func historyOpportunityHistoryDuration(recordID, previousRecordID string) contract.VirtualReportValue {
	inputs := []contract.VirtualReportHistoryProjection{{RecordID: recordID, Field: contract.VirtualReportHistoryCreatedDate}}
	if previousRecordID != "" {
		inputs = append(inputs, contract.VirtualReportHistoryProjection{RecordID: previousRecordID, Field: contract.VirtualReportHistoryCreatedDate})
	}
	return contract.VirtualReportValue{HistoryInputs: inputs}
}

type opportunityHistoryDurationDeriver struct{}

func (opportunityHistoryDurationDeriver) DeriveVirtualReportValue(inputs []interface{}) (interface{}, error) {
	if len(inputs) == 1 {
		return nil, nil
	}
	if len(inputs) != 2 {
		return nil, fmt.Errorf("dialect-sf-crm: opportunity history duration requires current and previous timestamps")
	}
	current, err := time.Parse(time.RFC3339Nano, fmt.Sprint(inputs[0]))
	if err != nil {
		return nil, fmt.Errorf("dialect-sf-crm: invalid opportunity history timestamp: %w", err)
	}
	previous, err := time.Parse(time.RFC3339Nano, fmt.Sprint(inputs[1]))
	if err != nil {
		return nil, fmt.Errorf("dialect-sf-crm: invalid opportunity history timestamp: %w", err)
	}
	if current.Before(previous) {
		return nil, fmt.Errorf("dialect-sf-crm: opportunity history duration is negative")
	}
	return current.Sub(previous).Hours() / 24, nil
}

func buildOpportunityHistoryRows(historyRows []contract.HistoryRecord, parents map[string]contract.Record, owners map[string]contract.Record, roles map[string]contract.Record, extensions []OpportunityHistoryReportColumn) ([]opportunityHistoryRow, error) {
	byParent := make(map[string][]contract.HistoryRecord)
	for _, row := range historyRows {
		byParent[row.ParentID] = append(byParent[row.ParentID], row)
	}
	out := make([]opportunityHistoryRow, 0)
	for parentID, events := range byParent {
		parent, ok := parents[parentID]
		if !ok {
			return nil, fmt.Errorf("dialect-sf-crm: opportunity history has no visible parent %q", parentID)
		}
		sort.SliceStable(events, func(i, j int) bool {
			if events[i].CreatedDate == events[j].CreatedDate {
				return events[i].ID < events[j].ID
			}
			return events[i].CreatedDate < events[j].CreatedDate
		})
		state := make(map[string]interface{})
		stateProjection := make(map[string]contract.VirtualReportValue)
		for _, field := range opportunityHistoryAllSnapshotFields(extensions) {
			state[field] = parent[field]
			stateProjection[field] = physicalOpportunityHistoryValue("Opportunity", field)
		}
		seeded := make(map[string]struct{})
		for _, event := range events {
			if _, ok := seeded[event.Field]; ok {
				continue
			}
			seeded[event.Field] = struct{}{}
			state[event.Field] = opportunityHistoryValue(event.OldValue)
			stateProjection[event.Field] = historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryOldValue)
		}
		lastStageChange := ""
		lastStageChangeID := ""
		for _, event := range events {
			if event.Field == "StageName" && event.CreatedDate > lastStageChange {
				lastStageChange = event.CreatedDate
				lastStageChangeID = event.ID
			}
		}
		previousStageDate := ""
		previousStageID := ""
		for start := 0; start < len(events); {
			end := start + 1
			for end < len(events) && events[end].CreatedDate == events[start].CreatedDate {
				end++
			}
			for _, event := range events[start:end] {
				state[event.Field] = opportunityHistoryValue(event.NewValue)
				stateProjection[event.Field] = historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryNewValue)
			}
			for _, event := range events[start:end] {
				if event.Field != "StageName" {
					continue
				}
				durationInputs := []interface{}{event.CreatedDate}
				if previousStageDate != "" {
					durationInputs = append(durationInputs, previousStageDate)
				}
				duration, err := (opportunityHistoryDurationDeriver{}).DeriveVirtualReportValue(durationInputs)
				if err != nil {
					return nil, err
				}
				previousStageDate = event.CreatedDate
				owner := owners[opportunityHistoryRelatedRecordID(parent, "OwnerId")]
				role := roles[opportunityHistoryRelatedRecordID(owner, "UserRoleId")]
				ownerRoleProjection := contract.VirtualReportValue{}
				if opportunityHistoryRecordID(role) != "" {
					ownerRoleProjection = physicalOpportunityHistoryValue("UserRole", "Name")
				}
				values := contract.Record{
					"Id":                  event.ID,
					"OpportunityId":       parentID,
					"OpportunityName":     parent["Name"],
					"OwnerId":             parent["OwnerId"],
					"OwnerName":           owner["Name"],
					"OwnerRole":           role["Name"],
					"FromStageName":       opportunityHistoryValue(event.OldValue),
					"StageName":           opportunityHistoryValue(event.NewValue),
					"StageDuration":       duration,
					"LastStageChangeDate": lastStageChange,
					"Probability":         opportunityHistoryNumber(state["Probability"]),
					"CloseDate":           state["CloseDate"],
					"ForecastCategory":    state["ForecastCategory"],
					"CreatedDate":         event.CreatedDate,
				}
				projections := map[string]contract.VirtualReportValue{
					"Id":                  historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryID),
					"OpportunityId":       physicalOpportunityHistoryValue("Opportunity", "Id"),
					"OpportunityName":     physicalOpportunityHistoryValue("Opportunity", "Name"),
					"OwnerId":             physicalOpportunityHistoryValue("Opportunity", "OwnerId"),
					"OwnerName":           physicalOpportunityHistoryValue("User", "Name"),
					"OwnerRole":           ownerRoleProjection,
					"FromStageName":       historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryOldValue),
					"StageName":           historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryNewValue),
					"StageDuration":       historyOpportunityHistoryDuration(event.ID, previousStageID),
					"LastStageChangeDate": historyOpportunityHistoryValue(lastStageChangeID, contract.VirtualReportHistoryCreatedDate),
					"Probability":         stateProjection["Probability"],
					"CloseDate":           stateProjection["CloseDate"],
					"ForecastCategory":    stateProjection["ForecastCategory"],
					"CreatedDate":         historyOpportunityHistoryValue(event.ID, contract.VirtualReportHistoryCreatedDate),
				}
				for _, extension := range extensions {
					if extension.HistoricalSnapshot {
						values[extension.Name] = state[extension.FieldAPIName]
						projections[extension.Name] = stateProjection[extension.FieldAPIName]
						continue
					}
					values[extension.Name] = parent[extension.FieldAPIName]
					projections[extension.Name] = physicalOpportunityHistoryValue("Opportunity", extension.FieldAPIName)
				}
				out = append(out, opportunityHistoryRow{
					values: values, projections: projections, historyRecordID: event.ID, parent: parent, owner: owner, role: role,
				})
				previousStageID = event.ID
			}
			start = end
		}
	}
	sort.SliceStable(out, func(i, j int) bool {
		left, right := fmt.Sprint(out[i].values["CreatedDate"]), fmt.Sprint(out[j].values["CreatedDate"])
		if left == right {
			return fmt.Sprint(out[i].values["Id"]) < fmt.Sprint(out[j].values["Id"])
		}
		return left > right
	})
	return out, nil
}

func validateOpportunityHistoryRequest(definition models.ReportDefinition) error {
	if strings.TrimSpace(definition.FilterLogic) != "" {
		return fmt.Errorf("dialect-sf-crm: OpportunityHistory does not support custom filter logic")
	}
	scope := strings.ToLower(strings.TrimSpace(definition.Scope))
	if scope != "" && scope != "all" && scope != "mine" {
		return fmt.Errorf("dialect-sf-crm: OpportunityHistory does not support scope %q", definition.Scope)
	}
	return nil
}

func opportunityHistoryRowMatches(values contract.Record, definition models.ReportDefinition, runtimeFilters map[string]interface{}, user *models.UserSession) (bool, error) {
	if strings.EqualFold(strings.TrimSpace(definition.Scope), "mine") && (user == nil || fmt.Sprint(values["OwnerId"]) != user.ID) {
		return false, nil
	}
	for _, filter := range definition.Filters {
		matches, err := opportunityHistoryValueMatches(values[filter.Column], filter.Operator, filter.Value)
		if err != nil || !matches {
			return false, err
		}
	}
	for column, value := range runtimeFilters {
		matches, err := opportunityHistoryValueMatches(values[column], "=", fmt.Sprint(value))
		if err != nil || !matches {
			return false, err
		}
	}
	if dateFilter := definition.DateFilter; dateFilter != nil {
		value := fmt.Sprint(values[dateFilter.Column])
		if dateFilter.StartDate != "" && value < dateFilter.StartDate {
			return false, nil
		}
		if dateFilter.EndDate != "" && value > dateFilter.EndDate {
			return false, nil
		}
	}
	return true, nil
}

func opportunityHistoryValueMatches(raw interface{}, operator, expected string) (bool, error) {
	actual := fmt.Sprint(raw)
	switch strings.ToLower(strings.TrimSpace(operator)) {
	case "equals", "eq", "=":
		return actual == expected, nil
	case "notequal", "neq", "!=", "<>":
		return actual != expected, nil
	case "contains":
		return strings.Contains(actual, expected), nil
	case "notcontain", "doesnotcontain":
		return !strings.Contains(actual, expected), nil
	case "startswith":
		return strings.HasPrefix(actual, expected), nil
	case "greaterthan", "gt", ">":
		return opportunityHistoryCompare(actual, expected) > 0, nil
	case "greaterorequal", "gte", ">=":
		return opportunityHistoryCompare(actual, expected) >= 0, nil
	case "lessthan", "lt", "<":
		return opportunityHistoryCompare(actual, expected) < 0, nil
	case "lessorequal", "lte", "<=":
		return opportunityHistoryCompare(actual, expected) <= 0, nil
	default:
		return false, fmt.Errorf("dialect-sf-crm: unsupported OpportunityHistory filter operator")
	}
}

func opportunityHistoryCompare(actual, expected string) int {
	left, leftErr := strconv.ParseFloat(actual, 64)
	right, rightErr := strconv.ParseFloat(expected, 64)
	if leftErr == nil && rightErr == nil {
		switch {
		case left < right:
			return -1
		case left > right:
			return 1
		default:
			return 0
		}
	}
	return strings.Compare(actual, expected)
}

func opportunityHistoryFieldsForRequest(columns []contract.VirtualReportColumn, extensions []OpportunityHistoryReportColumn) []string {
	fields := []string{"StageName"}
	seen := map[string]struct{}{"StageName": {}}
	for _, column := range columns {
		field := opportunitySnapshotColumnFields[column.Name]
		if field == "" {
			for _, extension := range extensions {
				if extension.HistoricalSnapshot && extension.Name == column.Name {
					field = extension.FieldAPIName
					break
				}
			}
		}
		if field == "" {
			continue
		}
		if _, ok := seen[field]; ok {
			continue
		}
		seen[field] = struct{}{}
		fields = append(fields, field)
	}
	return fields
}

func opportunityParentFieldsForRequest(columns []contract.VirtualReportColumn, historyFields []string, extensions []OpportunityHistoryReportColumn) []string {
	fields := []string{"Id", "OwnerId"}
	seen := map[string]struct{}{"Id": {}, "OwnerId": {}}
	add := func(field string) {
		if _, ok := seen[field]; ok {
			return
		}
		seen[field] = struct{}{}
		fields = append(fields, field)
	}
	for _, field := range historyFields {
		add(field)
	}
	for _, column := range columns {
		if column.Name == "OpportunityName" {
			add("Name")
			continue
		}
		for _, extension := range extensions {
			if !extension.HistoricalSnapshot && extension.Name == column.Name {
				add(extension.FieldAPIName)
				break
			}
		}
	}
	return fields
}

func historyParentIDs(rows []contract.HistoryRecord) []string {
	ids := make([]string, 0, len(rows))
	seen := make(map[string]struct{})
	for _, row := range rows {
		if _, ok := seen[row.ParentID]; ok {
			continue
		}
		seen[row.ParentID] = struct{}{}
		ids = append(ids, row.ParentID)
	}
	return ids
}

func opportunityHistoryAllSnapshotFields(extensions []OpportunityHistoryReportColumn) []string {
	fields := []string{"StageName", "Probability", "CloseDate", "ForecastCategory"}
	for _, extension := range extensions {
		if extension.HistoricalSnapshot {
			fields = append(fields, extension.FieldAPIName)
		}
	}
	return fields
}

func opportunityHistoryNumber(value interface{}) interface{} {
	if value == nil || fmt.Sprint(value) == "" {
		return nil
	}
	number, err := strconv.ParseFloat(fmt.Sprint(value), 64)
	if err != nil {
		return value
	}
	return number
}
