package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	metadataadminsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/metadataadminreadv1"
)

const MetadataAdminReadCapabilityID = "io.nexuscrm.sfcrm.metadata-admin-read.v1"

type RuntimeReadObjectAdminIndexFunc func(
	context.Context,
	string,
	metadataadminsemantic.ObjectAdminIndexRequest,
) (metadataadminsemantic.ObjectAdminIndexResponse, error)

type RuntimeReadValidationRuleAdminIndexFunc func(
	context.Context,
	string,
	metadataadminsemantic.ValidationRuleAdminIndexRequest,
) (metadataadminsemantic.ValidationRuleAdminIndexResponse, error)

type RuntimeMetadataAdminReadClient struct {
	readObjectIndex     RuntimeReadObjectAdminIndexFunc
	readValidationRules RuntimeReadValidationRuleAdminIndexFunc
}

func NewRuntimeMetadataAdminReadClient(
	readObjectIndex RuntimeReadObjectAdminIndexFunc,
	readValidationRules RuntimeReadValidationRuleAdminIndexFunc,
) (*RuntimeMetadataAdminReadClient, error) {
	if readObjectIndex == nil || readValidationRules == nil {
		return nil, errors.New("sfcrm AppService: complete Runtime metadata administration read ports are required")
	}
	return &RuntimeMetadataAdminReadClient{
		readObjectIndex:     readObjectIndex,
		readValidationRules: readValidationRules,
	}, nil
}

type metadataAdminRouteKind uint8

const (
	metadataAdminObjectIndex metadataAdminRouteKind = iota + 1
	metadataAdminObjectDetail
	metadataAdminValidationRuleIndex
)

type metadataAdminRoute struct {
	kind       metadataAdminRouteKind
	identifier string
}

func matchMetadataAdminRoute(path string) (metadataAdminRoute, bool) {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 5 || parts[0] != "services" || parts[1] != "data" {
		return metadataAdminRoute{}, false
	}
	if _, supported := recordViewVersions[parts[2]]; !supported {
		return metadataAdminRoute{}, false
	}
	if len(parts) == 5 && parts[3] == "metadata" && parts[4] == "objects" {
		return metadataAdminRoute{kind: metadataAdminObjectIndex}, true
	}
	if len(parts) == 5 && parts[3] == "metadata" && parts[4] == "validation-rules" {
		return metadataAdminRoute{kind: metadataAdminValidationRuleIndex}, true
	}
	if len(parts) != 6 {
		return metadataAdminRoute{}, false
	}
	identifier, err := url.PathUnescape(parts[5])
	if err != nil || identifier == "" || strings.TrimSpace(identifier) != identifier || strings.Contains(identifier, "/") {
		return metadataAdminRoute{}, false
	}
	switch {
	case parts[3] == "metadata" && parts[4] == "objects" && validSearchIdentifier(identifier):
		return metadataAdminRoute{kind: metadataAdminObjectDetail, identifier: identifier}, true
	default:
		return metadataAdminRoute{}, false
	}
}

type metadataAdminObjectProjection struct {
	Name        string  `json:"name"`
	DeleteMode  string  `json:"deleteMode,omitempty"`
	Description *string `json:"description,omitempty"`
}

type metadataAdminValidationRuleProjection struct {
	ID                string                                    `json:"id"`
	ObjectAPIName     string                                    `json:"objectApiName"`
	Name              string                                    `json:"name"`
	Description       *string                                   `json:"description,omitempty"`
	Active            bool                                      `json:"active"`
	Condition         string                                    `json:"condition"`
	ErrorMessage      string                                    `json:"errorMessage"`
	ErrorDisplayField *string                                   `json:"errorDisplayField,omitempty"`
	RecordSetBindings []metadataAdminRecordSetBindingProjection `json:"recordSetBindings,omitempty"`
	IsCustomized      bool                                      `json:"isCustomized"`
	OwnerPack         *string                                   `json:"ownerPack,omitempty"`
}

type metadataAdminRecordSetBindingProjection struct {
	Name          string                                   `json:"name"`
	Mode          string                                   `json:"mode"`
	ObjectAPIName string                                   `json:"objectApiName"`
	FilterLogic   string                                   `json:"filterLogic,omitempty"`
	Filters       []metadataAdminRecordSetFilterProjection `json:"filters"`
}

type metadataAdminRecordSetFilterProjection struct {
	Field       string          `json:"field"`
	Operator    string          `json:"operator"`
	ValueSource string          `json:"valueSource"`
	Value       json.RawMessage `json:"value"`
}

func (service *Service) serveMetadataAdminRead(writer http.ResponseWriter, request *http.Request) bool {
	// This capability owns only the read side of shared metadata paths. Let the
	// existing CRM mutation handlers retain POST/PATCH/DELETE ownership instead
	// of turning a read-route match into an AppService-wide 405.
	if service.metadataAdmin == nil || request.Method != http.MethodGet {
		return false
	}
	route, matched := matchMetadataAdminRoute(request.URL.EscapedPath())
	if !matched {
		return false
	}
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()

	switch route.kind {
	case metadataAdminValidationRuleIndex:
		objectAPIName := request.URL.Query().Get("objectApiName")
		if objectAPIName == "" {
			service.localRejected.Add(1)
			service.failures.Add(1)
			writeCRMErrorFields(
				writer, http.StatusBadRequest, crmwire.SFCodeInvalidField,
				"validation error on field 'objectApiName': is required", []string{"objectApiName"},
			)
			return true
		}
		if !validSearchIdentifier(objectAPIName) {
			service.localRejected.Add(1)
			service.failures.Add(1)
			writeCRMErrorFields(
				writer, http.StatusBadRequest, crmwire.SFCodeInvalidField,
				"validation error on field 'objectApiName': invalid object API name", []string{"objectApiName"},
			)
			return true
		}
		semanticRequest := metadataadminsemantic.ValidationRuleAdminIndexRequest{ObjectID: objectAPIName}
		response, err := service.metadataAdmin.readValidationRules(ctx, bearer, semanticRequest)
		if err != nil {
			service.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return true
		}
		if err := metadataadminsemantic.ValidateValidationRuleAdminIndexResponse(semanticRequest, response); err != nil {
			service.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return true
		}
		rules := make([]metadataAdminValidationRuleProjection, 0, len(response.Rules))
		for _, rule := range response.Rules {
			bindings := make([]metadataAdminRecordSetBindingProjection, 0, len(rule.RecordSetBindings))
			for _, binding := range rule.RecordSetBindings {
				filters := make([]metadataAdminRecordSetFilterProjection, 0, len(binding.Filters))
				for _, filter := range binding.Filters {
					filters = append(filters, metadataAdminRecordSetFilterProjection{
						Field: filter.Field, Operator: filter.Operator, ValueSource: filter.ValueSource,
						Value: append(json.RawMessage(nil), filter.Value...),
					})
				}
				bindings = append(bindings, metadataAdminRecordSetBindingProjection{
					Name: binding.Name, Mode: binding.Mode, ObjectAPIName: binding.ObjectID,
					FilterLogic: binding.FilterLogic, Filters: filters,
				})
			}
			rules = append(rules, metadataAdminValidationRuleProjection{
				ID: rule.RuleID, ObjectAPIName: rule.ObjectID, Name: rule.Name,
				Description: rule.Description, Active: rule.Active, Condition: rule.Condition,
				ErrorMessage: rule.ErrorMessage, ErrorDisplayField: rule.ErrorDisplayField,
				RecordSetBindings: bindings, IsCustomized: rule.IsCustomized, OwnerPack: rule.OwnerPack,
			})
		}
		service.successes.Add(1)
		writeJSON(writer, http.StatusOK, map[string]any{"rules": rules})
		return true
	case metadataAdminObjectIndex, metadataAdminObjectDetail:
		response, err := service.metadataAdmin.readObjectIndex(
			ctx, bearer, metadataadminsemantic.ObjectAdminIndexRequest{},
		)
		if err != nil {
			service.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return true
		}
		if err := metadataadminsemantic.ValidateObjectAdminIndexResponse(
			metadataadminsemantic.ObjectAdminIndexRequest{}, response,
		); err != nil {
			service.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return true
		}
		objects := make([]metadataAdminObjectProjection, 0, len(response.Objects))
		for _, object := range response.Objects {
			projected := metadataAdminObjectProjection{
				Name: object.ObjectID, DeleteMode: object.DeleteMode, Description: object.Description,
			}
			if route.kind == metadataAdminObjectDetail {
				if strings.EqualFold(object.ObjectID, route.identifier) {
					service.successes.Add(1)
					writeJSON(writer, http.StatusOK, projected)
					return true
				}
				continue
			}
			objects = append(objects, projected)
		}
		if route.kind == metadataAdminObjectDetail {
			service.failures.Add(1)
			writeCRMError(writer, http.StatusNotFound, crmwire.SFCodeNotFound, "Object not found")
			return true
		}
		service.successes.Add(1)
		writeJSON(writer, http.StatusOK, map[string]any{"sobjects": objects})
		return true
	default:
		return false
	}
}
