/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package buildin

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/go-chassis/cari/discovery"
	rbacmodel "github.com/go-chassis/cari/rbac"

	"github.com/apache/servicecomb-service-center/datasource"
	"github.com/apache/servicecomb-service-center/pkg/rest"
	"github.com/apache/servicecomb-service-center/server/plugin/auth"
	"github.com/apache/servicecomb-service-center/server/service/rbac"
)

const (
	LabelEnvironment = "environment"
	LabelAppID       = "appId"
	LabelServiceName = "serviceName"
	QueryEnv         = "env"
	HeaderConsumerID = "X-ConsumerId"
)

var (
	// APIServiceExistence Apply by service key or serviceId
	// - /v4/:project/registry/existence?env=xxx&appId=xxx&serviceName=xxx
	// - /v4/:project/registry/existence?serviceId=xxx&schemaId=xxx
	APIServiceExistence = "/v4/:project/registry/existence"
	// APIServicesList Method GET: apply all by optional service key
	// Method POST or DELETE: apply by request body
	APIServicesList     = "/v4/:project/registry/microservices"
	APIServiceInfo      = "/v4/:project/registry/microservices/:serviceId"
	APIProConDependency = "/v4/:project/registry/microservices/:providerId/consumers"
	APIConProDependency = "/v4/:project/registry/microservices/:consumerId/providers"
	// APIDiscovery Apply by service key
	APIDiscovery = "/v4/:project/registry/instances"
	// APIBatchDiscovery Apply by request body
	APIBatchDiscovery = "/v4/:project/registry/instances/action"
	// APIHeartbeats Apply by request body
	APIHeartbeats = "/v4/:project/registry/heartbeats"
	// APIGovServicesList Apply by optional service key
	// - /v4/:project/govern/microservices?appId=xxx
	// Apply all:
	// - /v4/:project/govern/microservices?options=statistics
	// - /v4/:project/govern/microservices/statistics
	APIGovServicesList = "/v4/:project/govern/microservices"
	APIGovServiceInfo  = "/v4/:project/govern/microservices/:serviceId"
)

func init() {
	RegisterParseFunc(APIServiceInfo, ByServiceID)
	RegisterParseFunc(APIGovServiceInfo, ByServiceID)
	RegisterParseFunc(APIProConDependency, func(r *http.Request) (*auth.ResourceScope, error) {
		return fromQueryKey(r, ":providerId")
	})
	RegisterParseFunc(APIConProDependency, func(r *http.Request) (*auth.ResourceScope, error) {
		return fromQueryKey(r, ":consumerId")
	})
	RegisterParseFunc(APIDiscovery, ByServiceKey)
	RegisterParseFunc(APIServiceExistence, ByServiceKey)
	RegisterParseFunc(APIGovServicesList, ApplyAll)
	RegisterParseFunc(APIServicesList, ByRequestBody)
	RegisterParseFunc(APIBatchDiscovery, ByDiscoveryRequestBody)
	RegisterParseFunc(APIHeartbeats, ByHeartbeatRequestBody)
}

func ByServiceID(r *http.Request) (*auth.ResourceScope, error) {
	return fromQueryKey(r, ":serviceId")
}
func fromQueryKey(r *http.Request, queryKey string) (*auth.ResourceScope, error) {
	ctx := r.Context()
	apiPath, ok := ctx.Value(rest.CtxMatchPattern).(string)
	if !ok {
		return nil, ErrCtxMatchPatternNotFound
	}
	serviceID := r.URL.Query().Get(queryKey)
	labels, err := serviceIDToLabels(ctx, serviceID)
	if err != nil {
		return nil, err
	}
	return &auth.ResourceScope{
		Type:   rbacmodel.GetResource(apiPath),
		Labels: labels,
		Verb:   rbac.MethodToVerbs[r.Method],
	}, nil
}
func serviceIDToLabels(ctx context.Context, serviceID string) ([]map[string]string, error) {
	service, err := datasource.GetMetadataManager().GetService(ctx, &discovery.GetServiceRequest{ServiceId: serviceID})
	if err != nil {
		return nil, err
	}
	return []map[string]string{{
		LabelEnvironment: service.Environment,
		LabelAppID:       service.AppId,
		LabelServiceName: service.ServiceName,
	}}, nil
}

func ByServiceKey(r *http.Request) (*auth.ResourceScope, error) {
	query := r.URL.Query()

	if _, ok := query["serviceId"]; ok {
		return fromQueryKey(r, "serviceId")
	}

	apiPath, ok := r.Context().Value(rest.CtxMatchPattern).(string)
	if !ok {
		return nil, ErrCtxMatchPatternNotFound
	}

	env, err := fromServiceKeyEnv(r, query.Get(QueryEnv))
	if err != nil {
		return nil, err
	}

	return &auth.ResourceScope{
		Type: rbacmodel.GetResource(apiPath),
		Labels: []map[string]string{{
			LabelEnvironment: env,
			LabelAppID:       query.Get(LabelAppID),
			LabelServiceName: query.Get(LabelServiceName),
		}},
		Verb: rbac.MethodToVerbs[r.Method],
	}, nil
}

func fromServiceKeyEnv(r *http.Request, queryEnv string) (string, error) {
	env := queryEnv
	if len(env) > 0 {
		return env, nil
	}
	consumerID := r.Header.Get(HeaderConsumerID)
	if len(consumerID) != 0 {
		service, err := datasource.GetMetadataManager().GetService(r.Context(), &discovery.GetServiceRequest{ServiceId: consumerID})
		if err != nil {
			return "", err
		}
		env = service.Environment
	}
	return env, nil
}

func ByRequestBody(r *http.Request) (*auth.ResourceScope, error) {
	if r.Method == http.MethodGet {
		// get or list by query string
		return ApplyAll(r)
	}
	return fromRequestBody(r)
}
func fromRequestBody(r *http.Request) (*auth.ResourceScope, error) {
	apiPath, ok := r.Context().Value(rest.CtxMatchPattern).(string)
	if !ok {
		return nil, ErrCtxMatchPatternNotFound
	}

	var (
		labels []map[string]string
		err    error
	)
	if r.Method == http.MethodDelete {
		// batch delete
		labels, err = deleteServicesToLabels(r)
	} else {
		// create service
		labels, err = createServiceToLabels(r)
	}
	if err != nil {
		return nil, err
	}

	return &auth.ResourceScope{
		Type:   rbacmodel.GetResource(apiPath),
		Labels: labels,
		Verb:   rbac.MethodToVerbs[r.Method],
	}, nil
}
func createServiceToLabels(r *http.Request) ([]map[string]string, error) {
	message, err := rest.ReadBody(r)
	if err != nil {
		return nil, err
	}

	request := &discovery.CreateServiceRequest{}
	err = json.Unmarshal(message, request)
	if err != nil {
		return nil, err
	}

	service := request.Service
	if service == nil {
		return nil, fmt.Errorf("invalid CreateServiceRequest")
	}

	return []map[string]string{{
		LabelEnvironment: service.Environment,
		LabelAppID:       service.AppId,
		LabelServiceName: service.ServiceName,
	}}, nil
}
func deleteServicesToLabels(r *http.Request) ([]map[string]string, error) {
	message, err := rest.ReadBody(r)
	if err != nil {
		return nil, err
	}

	request := &discovery.DelServicesRequest{}

	err = json.Unmarshal(message, request)
	if err != nil {
		return nil, err
	}

	ctx := r.Context()
	var labels []map[string]string
	for _, serviceID := range request.ServiceIds {
		ls, err := serviceIDToLabels(ctx, serviceID)
		if err != nil {
			return nil, err
		}
		labels = append(labels, ls...)
	}
	return labels, nil
}

func ByDiscoveryRequestBody(r *http.Request) (*auth.ResourceScope, error) {
	apiPath, ok := r.Context().Value(rest.CtxMatchPattern).(string)
	if !ok {
		return nil, ErrCtxMatchPatternNotFound
	}

	message, err := rest.ReadBody(r)
	if err != nil {
		return nil, err
	}

	request := &discovery.BatchFindInstancesRequest{}

	err = json.Unmarshal(message, request)
	if err != nil {
		return nil, err
	}

	ctx := r.Context()
	var labels []map[string]string
	for _, it := range request.Instances {
		ls, err := serviceIDToLabels(ctx, it.Instance.ServiceId)
		if err != nil {
			return nil, err
		}
		labels = append(labels, ls...)
	}

	return &auth.ResourceScope{
		Type:   rbacmodel.GetResource(apiPath),
		Labels: labels,
		Verb:   "get",
	}, nil
}

func ByHeartbeatRequestBody(r *http.Request) (*auth.ResourceScope, error) {
	apiPath, ok := r.Context().Value(rest.CtxMatchPattern).(string)
	if !ok {
		return nil, ErrCtxMatchPatternNotFound
	}

	message, err := rest.ReadBody(r)
	if err != nil {
		return nil, err
	}

	request := &discovery.HeartbeatSetRequest{}

	err = json.Unmarshal(message, request)
	if err != nil {
		return nil, err
	}

	ctx := r.Context()
	var labels []map[string]string
	for _, instance := range request.Instances {
		ls, err := serviceIDToLabels(ctx, instance.ServiceId)
		if err != nil {
			return nil, err
		}
		labels = append(labels, ls...)
	}

	return &auth.ResourceScope{
		Type:   rbacmodel.GetResource(apiPath),
		Labels: labels,
		Verb:   "update",
	}, nil
}
