package sfcrmappservice

import (
	"context"
	"errors"
	"net/http"
	"sort"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	searchsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/searchv1"
)

type RuntimeReadEffectivePermissionsFunc func(
	context.Context,
	string,
	permissionsemantic.Request,
) (permissionsemantic.Response, error)

type RuntimeAuthorizeCapabilitiesFunc func(
	context.Context,
	string,
	[]string,
) (context.Context, error)

type RuntimeReadSearchScopeOrderFunc func(
	context.Context,
	string,
	searchsemantic.ScopeOrderRequest,
) (searchsemantic.ScopeOrderResponse, error)

type RuntimePermissionMetadataClient struct {
	readEffective        RuntimeReadEffectivePermissionsFunc
	appMetadata          RuntimeGetAppMetadataViewV3Func
	reconcileAppMetadata RuntimeReconcileAppMetadataV3ReceiptsFunc
	readSearchScope      RuntimeReadSearchScopeOrderFunc
}

// effectivePermissionAggregate is an L1 assembly result, not one neutral
// Runtime response. Runtime deliberately bounds each semantic response to one
// MiB; consumers that genuinely need a Catalog-wide projection must compose
// disjoint selected responses without pretending that the aggregate crossed
// the L0 boundary as one response.
type effectivePermissionAggregate struct {
	SystemPermissions []permissionsemantic.PermissionGrant
	CustomPermissions []permissionsemantic.PermissionGrant
	ObjectPermissions []permissionsemantic.ObjectPermission
	FieldPermissions  []permissionsemantic.FieldPermission
	Receipts          commonsemantic.Receipts
	hasReceipts       bool
}

func (aggregate *effectivePermissionAggregate) appendResponse(
	response permissionsemantic.Response,
) error {
	if !aggregate.hasReceipts {
		aggregate.Receipts = response.Receipts
		aggregate.hasReceipts = true
	} else if err := validatePermissionBatchReceipts(
		aggregate.Receipts,
		response.Receipts,
	); err != nil {
		return err
	}
	aggregate.SystemPermissions = append(aggregate.SystemPermissions, response.SystemPermissions...)
	aggregate.CustomPermissions = append(aggregate.CustomPermissions, response.CustomPermissions...)
	aggregate.ObjectPermissions = append(aggregate.ObjectPermissions, response.ObjectPermissions...)
	aggregate.FieldPermissions = append(aggregate.FieldPermissions, response.FieldPermissions...)
	return nil
}

func (aggregate effectivePermissionAggregate) response() permissionsemantic.Response {
	return permissionsemantic.Response{
		SystemPermissions: aggregate.SystemPermissions, CustomPermissions: aggregate.CustomPermissions,
		ObjectPermissions: aggregate.ObjectPermissions, FieldPermissions: aggregate.FieldPermissions,
		Receipts: aggregate.Receipts,
	}
}

func validatePermissionBatchReceipts(
	expected commonsemantic.Receipts,
	actual commonsemantic.Receipts,
) error {
	if expected.CatalogRevision != actual.CatalogRevision {
		return &commonsemantic.Failure{
			Category: commonsemantic.FailureStaleState, StaleAxis: commonsemantic.StaleAxisCatalog,
			RequiredRevision: expected.CatalogRevision, ServedRevision: actual.CatalogRevision,
		}
	}
	if expected.SecurityEpoch != actual.SecurityEpoch {
		return &commonsemantic.Failure{
			Category: commonsemantic.FailureStaleState, StaleAxis: commonsemantic.StaleAxisSecurity,
			RequiredRevision: expected.SecurityEpoch, ServedRevision: actual.SecurityEpoch,
		}
	}
	if expected.ProtocolVersion != actual.ProtocolVersion ||
		expected.SchemaGeneration != actual.SchemaGeneration ||
		expected.EngineGeneration != actual.EngineGeneration ||
		expected.CapabilityVersion != actual.CapabilityVersion {
		return unavailablePermissionMetadata()
	}
	return nil
}

// readEffectivePermissionSelection reads one exact L1 consumer projection.
// Resource-scoped facts are split by the neutral contract bound. A response
// that is still too large is divided again by Catalog identity; admission and
// every other failure remain terminal rather than being disguised as sizing.
func readEffectivePermissionSelection(
	ctx context.Context,
	read RuntimeReadEffectivePermissionsFunc,
	bearer string,
	targetPrincipalID string,
	revision uint64,
	sections []permissionsemantic.Section,
	resourceTypes []string,
) (effectivePermissionAggregate, error) {
	if read == nil || !validAggregatePermissionSelection(sections, resourceTypes) {
		return effectivePermissionAggregate{}, unavailablePermissionMetadata()
	}
	if len(resourceTypes) == 0 {
		return readEffectivePermissionBatch(
			ctx, read, bearer, targetPrincipalID, revision, sections, nil,
		)
	}
	var result effectivePermissionAggregate
	for start := 0; start < len(resourceTypes); start += permissionsemantic.MaxSelectedResourceTypes {
		end := min(start+permissionsemantic.MaxSelectedResourceTypes, len(resourceTypes))
		batch, err := readEffectivePermissionBatch(
			ctx, read, bearer, targetPrincipalID, revision, sections, resourceTypes[start:end],
		)
		if err != nil {
			return effectivePermissionAggregate{}, err
		}
		if err := result.appendAggregate(batch); err != nil {
			return effectivePermissionAggregate{}, err
		}
	}
	return result, nil
}

func validAggregatePermissionSelection(
	sections []permissionsemantic.Section,
	resourceTypes []string,
) bool {
	if len(sections) == 0 {
		return false
	}
	previous := -1
	resourceScoped := false
	globalScoped := false
	for _, section := range sections {
		order := -1
		switch section {
		case permissionsemantic.SectionSystemPermissions:
			order, globalScoped = 0, true
		case permissionsemantic.SectionCustomPermissions:
			order, globalScoped = 1, true
		case permissionsemantic.SectionObjectPermissions:
			order, resourceScoped = 2, true
		case permissionsemantic.SectionFieldPermissions:
			order, resourceScoped = 3, true
		}
		if order <= previous {
			return false
		}
		previous = order
	}
	return resourceScoped != globalScoped && resourceScoped == (len(resourceTypes) != 0)
}

func (aggregate *effectivePermissionAggregate) appendAggregate(
	batch effectivePermissionAggregate,
) error {
	if !batch.hasReceipts {
		return unavailablePermissionMetadata()
	}
	return aggregate.appendResponse(batch.response())
}

func readEffectivePermissionBatch(
	ctx context.Context,
	read RuntimeReadEffectivePermissionsFunc,
	bearer string,
	targetPrincipalID string,
	revision uint64,
	sections []permissionsemantic.Section,
	resourceTypes []string,
) (effectivePermissionAggregate, error) {
	request := permissionsemantic.Request{
		TargetPrincipalID:  targetPrincipalID,
		MinCatalogRevision: &revision,
		Selection: &permissionsemantic.Selection{
			Sections:      append([]permissionsemantic.Section(nil), sections...),
			ResourceTypes: append([]string(nil), resourceTypes...),
		},
	}
	response, err := read(ctx, bearer, request)
	if err == nil {
		err = permissionsemantic.ValidateResponseForRequest(request, response)
	}
	if err != nil {
		if len(resourceTypes) > 1 && semanticResponseTooLarge(err) {
			middle := len(resourceTypes) / 2
			left, leftErr := readEffectivePermissionBatch(
				ctx, read, bearer, targetPrincipalID, revision, sections, resourceTypes[:middle],
			)
			if leftErr != nil {
				return effectivePermissionAggregate{}, leftErr
			}
			right, rightErr := readEffectivePermissionBatch(
				ctx, read, bearer, targetPrincipalID, revision, sections, resourceTypes[middle:],
			)
			if rightErr != nil {
				return effectivePermissionAggregate{}, rightErr
			}
			if err := left.appendAggregate(right); err != nil {
				return effectivePermissionAggregate{}, err
			}
			return left, nil
		}
		return effectivePermissionAggregate{}, err
	}
	var result effectivePermissionAggregate
	if err := result.appendResponse(response); err != nil {
		return effectivePermissionAggregate{}, err
	}
	return result, nil
}

func semanticResponseTooLarge(err error) bool {
	var failure *commonsemantic.Failure
	return errors.As(err, &failure) &&
		failure.Category == commonsemantic.FailureResourceExhausted &&
		failure.LimitKind == commonsemantic.LimitSemanticResponse
}

func systemPermissionAuthorityRequest() permissionsemantic.Request {
	return permissionsemantic.Request{Selection: &permissionsemantic.Selection{
		Sections: []permissionsemantic.Section{
			permissionsemantic.SectionSystemPermissions,
		},
	}}
}

type permissionMetadataRoute uint8

const (
	permissionMetadataRouteEffective permissionMetadataRoute = iota + 1
	permissionMetadataRouteCuratedSearchScopeOrder
	permissionMetadataRouteRuntimeSearchScopeOrder
)

func matchPermissionMetadataRoute(path string) permissionMetadataRoute {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 5 || parts[0] != "services" || parts[1] != "data" {
		return 0
	}
	if _, supported := recordViewVersions[parts[2]]; !supported {
		return 0
	}
	switch {
	case len(parts) == 5 && parts[3] == "permissions" &&
		parts[4] == "effective":
		return permissionMetadataRouteEffective
	case len(parts) == 6 && parts[3] == "metadata" &&
		parts[4] == "search" && parts[5] == "scope-order":
		return permissionMetadataRouteCuratedSearchScopeOrder
	case len(parts) == 5 && parts[3] == "search" &&
		parts[4] == "scopeOrder":
		return permissionMetadataRouteRuntimeSearchScopeOrder
	default:
		return 0
	}
}

func (service *Service) servePermissionMetadata(
	writer http.ResponseWriter,
	request *http.Request,
) bool {
	route := matchPermissionMetadataRoute(request.URL.EscapedPath())
	if route == 0 {
		return false
	}
	if route == permissionMetadataRouteRuntimeSearchScopeOrder &&
		service.permissionMetadata.readSearchScope == nil {
		return false
	}
	if request.Method != http.MethodGet {
		writer.Header().Set("Allow", http.MethodGet)
		writeCRMError(
			writer,
			http.StatusMethodNotAllowed,
			crmwire.SFCodeInvalidRequest,
			"Method not allowed",
		)
		return true
	}
	switch route {
	case permissionMetadataRouteEffective:
		service.serveEffectivePermissions(writer, request)
	case permissionMetadataRouteCuratedSearchScopeOrder:
		service.serveCuratedSearchScopeOrder(writer, request)
	case permissionMetadataRouteRuntimeSearchScopeOrder:
		service.serveRuntimeSearchScopeOrder(writer, request)
	}
	return true
}

func NewRuntimePermissionMetadataClient(
	readEffective RuntimeReadEffectivePermissionsFunc,
	appMetadata RuntimeGetAppMetadataViewV3Func,
	readSearchScope RuntimeReadSearchScopeOrderFunc,
) (*RuntimePermissionMetadataClient, error) {
	if readEffective == nil || appMetadata == nil {
		return nil, errors.New(
			"sfcrm AppService: Runtime effective permissions and AppMetadata ports are required",
		)
	}
	return &RuntimePermissionMetadataClient{
		readEffective:   readEffective,
		appMetadata:     appMetadata,
		readSearchScope: readSearchScope,
	}, nil
}

func (client *RuntimePermissionMetadataClient) effectivePermissions(
	ctx context.Context,
	bearer string,
	targetPrincipalID string,
	revision uint64,
	resourceTypes []string,
) (effectivePermissionAggregate, error) {
	if client == nil || client.readEffective == nil {
		return effectivePermissionAggregate{}, unavailablePermissionMetadata()
	}
	global, err := readEffectivePermissionSelection(
		ctx, client.readEffective, bearer, targetPrincipalID, revision,
		[]permissionsemantic.Section{
			permissionsemantic.SectionSystemPermissions,
			permissionsemantic.SectionCustomPermissions,
		},
		nil,
	)
	if err != nil {
		return effectivePermissionAggregate{}, err
	}
	resources, err := readEffectivePermissionSelection(
		ctx, client.readEffective, bearer, targetPrincipalID, revision,
		[]permissionsemantic.Section{
			permissionsemantic.SectionObjectPermissions,
			permissionsemantic.SectionFieldPermissions,
		},
		resourceTypes,
	)
	if err != nil {
		return effectivePermissionAggregate{}, err
	}
	if err := global.appendAggregate(resources); err != nil {
		return effectivePermissionAggregate{}, err
	}
	return global, nil
}

func permissionResourceTypes(
	specs map[string]searchObjectSpec,
) ([]string, error) {
	if len(specs) == 0 {
		return nil, unavailablePermissionMetadata()
	}
	result := make([]string, 0, len(specs))
	for _, spec := range specs {
		if !describeAuthorityIsCurrent(spec) {
			return nil, unavailablePermissionMetadata()
		}
		result = append(result, spec.APIName)
	}
	sort.Strings(result)
	return result, nil
}

func (service *Service) serveEffectivePermissions(
	writer http.ResponseWriter,
	request *http.Request,
) {
	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
	}
	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
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	view, specs, err := readObjectSpecs(ctx, service.appMetadataV3)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	resourceTypes, err := permissionResourceTypes(specs)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	response, err := service.permissionMetadata.effectivePermissions(
		ctx,
		bearer,
		request.URL.Query().Get("userId"),
		view.Receipts.CatalogRevision,
		resourceTypes,
	)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	if err := validateSearchMetadataAuthority(
		ctx, service.reconcileAppMetadataV3, view, response.Receipts,
	); err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, effectivePermissionsWire(response))
}

func (service *Service) serveCuratedSearchScopeOrder(
	writer http.ResponseWriter,
	request *http.Request,
) {
	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
	}
	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
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	response, err := service.permissionMetadata.curatedSearchScopeOrder(ctx, bearer)
	if err != nil {
		service.failures.Add(1)
		writeSearchFailure(writer, err)
		return
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, response)
}

func (service *Service) serveRuntimeSearchScopeOrder(
	writer http.ResponseWriter,
	request *http.Request,
) {
	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
	}
	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
	}
	parts := strings.Split(strings.Trim(request.URL.EscapedPath(), "/"), "/")
	if len(parts) != 5 {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(
			writer,
			http.StatusBadRequest,
			crmwire.SFCodeInvalidRequest,
			"Invalid request path",
		)
		return
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	response, err := service.permissionMetadata.runtimeSearchScopeOrder(
		ctx,
		bearer,
		parts[2],
	)
	if err != nil {
		service.failures.Add(1)
		writeSearchFailure(writer, err)
		return
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, response)
}

func effectivePermissionsWire(
	response effectivePermissionAggregate,
) map[string]any {
	systemPermissions := make(map[string]bool, len(response.SystemPermissions))
	for _, permission := range response.SystemPermissions {
		systemPermissions[permission.PermissionID] = permission.Granted
	}
	customPermissions := make(map[string]bool, len(response.CustomPermissions))
	for _, permission := range response.CustomPermissions {
		customPermissions[permission.PermissionID] = permission.Granted
	}
	objectPermissions := make([]map[string]any, 0, len(response.ObjectPermissions))
	for _, permission := range response.ObjectPermissions {
		objectPermissions = append(objectPermissions, map[string]any{
			"SobjectType":                 permission.ResourceType,
			"PermissionsRead":             permission.CanRead,
			"PermissionsCreate":           permission.CanCreate,
			"PermissionsEdit":             permission.CanEdit,
			"PermissionsDelete":           permission.CanDelete,
			"PermissionsViewAllRecords":   permission.CanViewAll,
			"PermissionsModifyAllRecords": permission.CanModifyAll,
		})
	}
	fieldPermissions := make([]map[string]any, 0, len(response.FieldPermissions))
	for _, permission := range response.FieldPermissions {
		fieldPermissions = append(fieldPermissions, map[string]any{
			"SobjectType": permission.ResourceType,
			"Field": qualifiedPermissionField(
				permission.ResourceType,
				permission.FieldName,
			),
			"PermissionsRead": permission.CanRead,
			"PermissionsEdit": permission.CanEdit,
		})
	}
	return map[string]any{
		"systemPermissions": systemPermissions,
		"customPermissions": customPermissions,
		"objectPermissions": objectPermissions,
		"fieldPermissions":  fieldPermissions,
	}
}

func qualifiedPermissionField(resourceType, rawField string) string {
	resourceType = strings.TrimSpace(resourceType)
	rawField = unqualifiedPermissionField(rawField)
	switch {
	case resourceType == "":
		return rawField
	case rawField == "":
		return resourceType
	default:
		return resourceType + "." + rawField
	}
}

func unqualifiedPermissionField(rawField string) string {
	rawField = strings.TrimSpace(rawField)
	if _, fieldName, qualified := strings.Cut(rawField, "."); qualified {
		rawField = strings.TrimSpace(fieldName)
	}
	return rawField
}

func unavailablePermissionMetadata() *commonsemantic.Failure {
	return &commonsemantic.Failure{
		Category: commonsemantic.FailureUnavailable,
	}
}
