package querycodec

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

const (
	historySuffix                 = "History"
	opportunityFieldHistoryObject = "OpportunityFieldHistory"
	opportunityHistoryObject      = "OpportunityHistory"
)

// HistoryBatchSize is the CRM product default for field-history pages.
const (
	HistoryBatchSize              = historyBatchSize
	HistorySuffix                 = historySuffix
	OpportunityFieldHistoryObject = opportunityFieldHistoryObject
	OpportunityHistoryObject      = opportunityHistoryObject
)

// History result field names are the CRM wire vocabulary used by both query
// projection and consumers that describe or author references to that result.
const (
	HistoryIDField          = "Id"
	HistoryChangedField     = "Field"
	HistoryOldValueField    = "OldValue"
	HistoryNewValueField    = "NewValue"
	HistoryDataTypeField    = "DataType"
	HistoryCreatedDateField = "CreatedDate"
	HistoryCreatedByField   = "CreatedById"
)

func HistoryObjectName(parent string) string {
	if parent == "Opportunity" {
		return opportunityFieldHistoryObject
	}
	if strings.HasSuffix(parent, "__c") {
		return strings.TrimSuffix(parent, "__c") + "__" + historySuffix
	}
	return parent + historySuffix
}

func IsHistoryObject(name string) bool {
	return name != opportunityHistoryObject && strings.HasSuffix(name, historySuffix) && len(name) > len(historySuffix)
}

func ParentObjectFromHistoryName(name string) string {
	if name == opportunityFieldHistoryObject {
		return "Opportunity"
	}
	if strings.HasSuffix(name, "__"+historySuffix) {
		return strings.TrimSuffix(name, "__"+historySuffix) + "__c"
	}
	return strings.TrimSuffix(name, historySuffix)
}

func HistoryParentFieldName(historyObjectName string) string {
	if strings.HasSuffix(historyObjectName, "__"+historySuffix) {
		return "ParentId"
	}
	parent := ParentObjectFromHistoryName(historyObjectName)
	if parent == "" {
		return "ParentId"
	}
	return parent + "Id"
}

func SupportsFieldHistory(parent string) bool {
	switch strings.ToLower(strings.TrimSpace(parent)) {
	case "event", "task", "listview", "report", "dashboard", "playground", "flowrecord":
		return false
	default:
		return true
	}
}

func (h *Handler) executeHistory(ctx context.Context, request Request, parsed *QueryRequest, raw string, pageSize, cursorOffset int) Response {
	parentField := HistoryParentFieldName(parsed.ObjectAPIName)
	parentObject := ParentObjectFromHistoryName(parsed.ObjectAPIName)
	if !SupportsFieldHistory(parentObject) {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", fmt.Sprintf("%s does not support field history", parentObject))
	}
	if pageSize <= 0 {
		pageSize = historyBatchSize
	}
	cursorOffset = max(0, cursorOffset)
	parentID, filter, field, ok := lowerHistoryPredicate(parsed.Filter, parentField)
	if !ok {
		return crmError(http.StatusBadRequest, "MALFORMED_QUERY", fmt.Sprintf("<Object>History supports only '%s = <literal>' and optional 'Field = <literal>'; unsupported predicate on %q", parentField, field))
	}
	result, err := h.executor.History(ctx, HistoryRequest{
		ParentObjectAPIName: parentObject,
		SelectFields:        append([]string(nil), parsed.SelectFields...),
		ParentID:            parentID, Filter: filter, Limit: parsed.Limit, Offset: parsed.Offset,
		SortDescending: !strings.EqualFold(parsed.SortDirection, "ASC"), IncludeDeleted: parsed.ScanAll,
		PageSize: pageSize, CursorOffset: cursorOffset,
	})
	if err != nil {
		return projectFailure(err)
	}
	records := make([]any, 0, len(result.Rows))
	for _, row := range result.Rows {
		records = append(records, historyRecord(versionOrDefault(request.Route.Version), parsed.ObjectAPIName, parentField, row))
	}
	body := map[string]any{"totalSize": result.TotalSize, "done": result.Done, "records": records}
	if !result.Done {
		body["nextRecordsUrl"] = historyNextURL(request.Route.Version, parsed, raw, pageSize, cursorOffset, len(result.Rows))
	}
	return Response{Status: http.StatusOK, Body: body}
}

func lowerHistoryPredicate(predicate Predicate, parentField string) (parentID string, filter Predicate, unsupported string, ok bool) {
	criteria := flattenEqualStringPredicates(predicate)
	if criteria == nil && predicate.Kind != PredicateNone {
		return "", Predicate{}, "predicate", false
	}
	parentSeen := false
	for _, criterion := range criteria {
		field := strings.TrimSpace(criterion.Field)
		value, isString := criterion.Value.(string)
		if !isString {
			return "", Predicate{}, field, false
		}
		switch {
		case strings.EqualFold(field, parentField):
			if parentSeen || strings.TrimSpace(value) == "" {
				return "", Predicate{}, field, false
			}
			parentSeen, parentID = true, value
		case strings.EqualFold(field, "Field"), strings.EqualFold(field, "FieldName"):
			if filter.Kind != PredicateNone {
				return "", Predicate{}, field, false
			}
			filter = Predicate{Kind: PredicateFieldComparison, Field: "Field", Op: "=", Value: value}
		default:
			return "", Predicate{}, field, false
		}
	}
	if filter.Kind != PredicateNone && !parentSeen {
		return "", Predicate{}, parentField, false
	}
	return parentID, filter, "", true
}

type historyCriterion struct {
	Field string
	Value any
}

func flattenEqualStringPredicates(predicate Predicate) []historyCriterion {
	switch predicate.Kind {
	case PredicateNone:
		return []historyCriterion{}
	case PredicateAnd:
		var result []historyCriterion
		for _, child := range predicate.Children {
			criteria := flattenEqualStringPredicates(child)
			if criteria == nil {
				return nil
			}
			result = append(result, criteria...)
		}
		return result
	case PredicateComparison:
		if predicate.Op != "=" && predicate.Op != "==" || predicate.Left.Kind != OperandField || predicate.Right.Kind != OperandLiteral {
			return nil
		}
		return []historyCriterion{{Field: predicate.Left.Field, Value: predicate.Right.Value}}
	case PredicateFieldComparison:
		if predicate.Op != "=" && predicate.Op != "==" {
			return nil
		}
		return []historyCriterion{{Field: predicate.Field, Value: predicate.Value}}
	default:
		return nil
	}
}

func historyRecord(version, objectName, parentField string, row HistoryRecord) map[string]any {
	record := map[string]any{
		HistoryIDField: row.ID, HistoryChangedField: row.Field, HistoryOldValueField: nullableString(row.OldValue), HistoryNewValueField: nullableString(row.NewValue),
		HistoryDataTypeField: row.DataType, HistoryCreatedDateField: row.CreatedDate, HistoryCreatedByField: row.CreatedByID,
		"attributes": map[string]any{"type": objectName, "url": fmt.Sprintf("/services/data/%s/sobjects/%s/%s", version, objectName, row.ID)},
	}
	record[parentField] = row.ParentID
	return record
}

func nullableString(value *string) any {
	if value == nil {
		return nil
	}
	return *value
}

func historyNextURL(version string, request *QueryRequest, raw string, pageSize, cursorOffset, returned int) string {
	locator := locatorData{DateAnchor: request.DateAnchor, Query: url.QueryEscape(raw), Offset: cursorOffset + returned, Limit: pageSize, ScanAll: request.ScanAll}
	data, _ := json.Marshal(locator)
	return fmt.Sprintf("/services/data/%s/query/%s", versionOrDefault(version), base64.URLEncoding.EncodeToString(data))
}
