/*
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 api

import (
	gocontext "context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"

	"github.com/apache/incubator-devlake/core/log"

	"github.com/apache/incubator-devlake/core/context"
	"github.com/apache/incubator-devlake/core/errors"
	"github.com/apache/incubator-devlake/core/plugin"
	"github.com/go-playground/validator/v10"
)

type RemoteScopesChild struct {
	Type     string      `json:"type"`
	ParentId *string     `json:"parentId"`
	Id       string      `json:"id"`
	Name     string      `json:"name"`
	FullName string      `json:"fullName"`
	Data     interface{} `json:"data"`
}

type RemoteQueryData struct {
	Page       int    `json:"page"`
	PerPage    int    `json:"per_page"`
	CustomInfo string `json:"custom"`
	Tag        string `json:"tag"`
	Search     []string
}

type FirstPageTokenOutput struct {
	PageToken string `json:"pageToken"`
}

type RemoteScopesOutput struct {
	Children      []RemoteScopesChild `json:"children"`
	NextPageToken string              `json:"nextPageToken"`
}

type SearchRemoteScopesOutput struct {
	Children []RemoteScopesChild `json:"children"`
	Page     int                 `json:"page"`
	PageSize int                 `json:"pageSize"`
}

// RemoteApiHelper is used to write the CURD of connection
type RemoteApiHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup] struct {
	basicRes        context.BasicRes
	validator       *validator.Validate
	connHelper      *ConnectionApiHelper
	httpClientCache map[string]*ApiClient
	logger          log.Logger
}

// NewRemoteHelper creates a ScopeHelper for connection management
func NewRemoteHelper[Conn plugin.ApiConnection, Scope plugin.ToolLayerScope, ApiScope plugin.ApiScope, Group plugin.ApiGroup](
	basicRes context.BasicRes,
	vld *validator.Validate,
	connHelper *ConnectionApiHelper,
) *RemoteApiHelper[Conn, Scope, ApiScope, Group] {
	if vld == nil {
		vld = validator.New()
	}
	if connHelper == nil {
		return nil
	}
	return &RemoteApiHelper[Conn, Scope, ApiScope, Group]{
		basicRes:        basicRes,
		validator:       vld,
		connHelper:      connHelper,
		httpClientCache: make(map[string]*ApiClient),
		logger:          basicRes.GetLogger(),
	}
}

type NoRemoteGroupResponse struct {
}

func (NoRemoteGroupResponse) GroupId() string {
	return ""
}

func (NoRemoteGroupResponse) GroupName() string {
	return ""
}

type BaseRemoteGroupResponse struct {
	Id   string
	Name string
}

func (g BaseRemoteGroupResponse) GroupId() string {
	return g.Id
}

func (g BaseRemoteGroupResponse) GroupName() string {
	return g.Name
}

const remoteScopesPerPage int = 100
const (
	TypeGroup string = "group" // group is just like a directory or a folder, that holds some scopes.
	TypeScope string = "scope" // scope, sometimes we call it project. But scope is a more standard noun.
	TypeMixed string = "mixed"
)

func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) GetApiClient(connection plugin.CacheableConnection) (*ApiClient, errors.Error) {
	key := connection.GetHash()
	// empty key means no connection reuse
	if key == "" {
		r.logger.Info("No api client reuse")
		return NewApiClientFromConnection(gocontext.TODO(), r.basicRes, connection)
	}

	if client, ok := r.httpClientCache[key]; ok {
		r.logger.Info("Reused api client")
		return client, nil
	}
	r.logger.Info("Creating new api client")
	newClient, err := NewApiClientFromConnection(gocontext.TODO(), r.basicRes, connection)
	if err != nil {
		return nil, err
	}
	r.httpClientCache[key] = newClient
	return newClient, nil
}

func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) ProxyApiGet(conn plugin.CacheableConnection, path string, query url.Values) (*plugin.ApiResourceOutput, errors.Error) {
	apiClient, err := r.GetApiClient(conn)
	if err != nil {
		return nil, err
	}

	resp, err := apiClient.Get(path, query, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := errors.Convert01(io.ReadAll(resp.Body))
	if err != nil {
		return nil, err
	}
	// verify response body is json
	var tmp interface{}
	err = errors.Convert(json.Unmarshal(body, &tmp))
	if err != nil {
		return nil, err
	}
	return &plugin.ApiResourceOutput{Status: resp.StatusCode, Body: json.RawMessage(body)}, nil
}

// PrepareFirstPageToken prepares the first page token
func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) PrepareFirstPageToken(customInfo string) (*plugin.ApiResourceOutput, errors.Error) {
	outputBody := &FirstPageTokenOutput{}
	pageToken, err := getPageTokenFromPageData(&RemoteQueryData{
		Page:       1,
		PerPage:    remoteScopesPerPage,
		CustomInfo: customInfo,
		Tag:        TypeGroup,
	})
	if err != nil {
		return nil, err
	}
	outputBody.PageToken = pageToken
	return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil
}

func (r *RemoteApiHelper[Conn, Scope, ApiScope, ApiGroup]) GetRemoteScopesOutput(
	input *plugin.ApiResourceInput,
	getter func(basicRes context.BasicRes, groupId string, queryData *RemoteQueryData, connection Conn) (*RemoteScopesOutput, errors.Error),
) (*RemoteScopesOutput, errors.Error) {
	connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
	if err != nil || connectionId == 0 {
		return nil, errors.BadInput.New("invalid connectionId")
	}
	var connection Conn
	err = r.connHelper.First(&connection, input.Params)
	if err != nil {
		r.logger.Error(err, "find connection: %d", connectionId)
		return nil, err
	}
	groupId := input.Query.Get("groupId")
	pageToken := input.Query.Get("pageToken")
	queryData, err := getPageDataFromPageTokenWithTag(pageToken, TypeMixed)
	if err != nil {
		r.logger.Error(err, "get page data from page token")
		return nil, err
	}
	resp, err := getter(r.basicRes, groupId, queryData, connection)
	if err != nil {
		r.logger.Error(err, "call getter")
		return nil, err
	}

	queryData.Page += 1
	resp.NextPageToken, err = getPageTokenFromPageData(queryData)
	if err != nil {
		r.logger.Error(err, "get next page token")
		return nil, err
	}
	if len(resp.Children) < queryData.PerPage {
		// there are no more pages
		resp.NextPageToken = ""
	}
	return resp, nil
}

// GetScopesFromRemote gets the scopes from api
func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) GetScopesFromRemote(
	input *plugin.ApiResourceInput,
	getGroup func(basicRes context.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]Group, errors.Error),
	getScope func(basicRes context.BasicRes, gid string, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error),
) (*plugin.ApiResourceOutput, errors.Error) {

	connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
	if err != nil || connectionId == 0 {
		return nil, errors.BadInput.New("invalid connectionId")
	}
	var connection Conn
	err = r.connHelper.First(&connection, input.Params)
	if err != nil {
		return nil, err
	}
	// get groupId and pageData
	groupId := input.Query.Get("groupId")
	pageToken := input.Query.Get("pageToken")

	queryData, err := getPageDataFromPageToken(pageToken)
	if err != nil {
		return nil, errors.BadInput.New("failed to get page token")
	}
	outputBody := &RemoteScopesOutput{}

	// list groups part
	if queryData.Tag == TypeGroup {
		var resBody []Group
		if getGroup != nil {
			resBody, err = getGroup(r.basicRes, groupId, queryData, connection)
		}
		if err != nil {
			return nil, err
		}
		// if len(resBody) == 0, will skip the following steps, this will happen in some plugins which don't have group
		// append group to output
		for _, group := range resBody {
			child := RemoteScopesChild{
				Type: TypeGroup,
				Id:   group.GroupId(),
				Name: group.GroupName(),
				// don't need to save group into data
				Data: nil,
			}
			child.ParentId = &groupId
			if *child.ParentId == "" {
				child.ParentId = nil
			}
			outputBody.Children = append(outputBody.Children, child)
		}
		// check groups count
		if len(resBody) < queryData.PerPage {
			queryData.Tag = TypeScope
			queryData.Page = 1
			queryData.PerPage = queryData.PerPage - len(resBody)
		}
	}

	// list projects part
	if queryData.Tag == TypeScope && getScope != nil {
		var resBody []ApiScope
		resBody, err = getScope(r.basicRes, groupId, queryData, connection)
		if err != nil {
			return nil, err
		}

		// append project to output
		for _, project := range resBody {
			scope := project.ConvertApiScope()
			child := RemoteScopesChild{
				Type:     TypeScope,
				Id:       scope.ScopeId(),
				Name:     scope.ScopeName(),
				FullName: scope.ScopeFullName(),
				Data:     &scope,
			}
			child.ParentId = &groupId
			if *child.ParentId == "" {
				child.ParentId = nil
			}
			outputBody.Children = append(outputBody.Children, child)
		}

		// check scopes count
		if len(resBody) < queryData.PerPage {
			queryData = nil
		}
	}

	// get the next page token
	outputBody.NextPageToken = ""
	if queryData != nil {
		queryData.Page += 1
		outputBody.NextPageToken, err = getPageTokenFromPageData(queryData)
		if err != nil {
			return nil, err
		}
	}
	return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil
}

func (r *RemoteApiHelper[Conn, Scope, ApiScope, Group]) SearchRemoteScopes(input *plugin.ApiResourceInput,
	searchScope func(basicRes context.BasicRes, queryData *RemoteQueryData, connection Conn) ([]ApiScope, errors.Error),
) (*plugin.ApiResourceOutput, errors.Error) {
	connectionId, err := errors.Convert01(strconv.ParseUint(input.Params["connectionId"], 10, 64))
	if err != nil || connectionId == 0 {
		return nil, errors.BadInput.New("invalid connectionId")
	}

	var connection Conn
	err = r.connHelper.First(&connection, input.Params)
	if err != nil {
		return nil, err
	}

	search, ok := input.Query["search"]
	if !ok || len(search) == 0 {
		search = []string{""}
	}

	var p int
	var err1 error
	page, ok := input.Query["page"]
	if !ok || len(page) == 0 {
		p = 1
	} else {
		p, err1 = strconv.Atoi(page[0])
		if err1 != nil {
			return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to Atoi page:%s", page[0]))
		}
	}
	var ps int
	pageSize, ok := input.Query["pageSize"]
	if !ok || len(pageSize) == 0 {
		ps = remoteScopesPerPage
	} else {
		ps, err1 = strconv.Atoi(pageSize[0])
		if err1 != nil {
			return nil, errors.BadInput.Wrap(err1, fmt.Sprintf("failed to Atoi pageSize:%s", pageSize[0]))
		}
	}

	queryData := &RemoteQueryData{
		Page:    p,
		PerPage: ps,
		Search:  search,
	}

	var resBody []ApiScope
	resBody, err = searchScope(r.basicRes, queryData, connection)
	if err != nil {
		return nil, err
	}

	outputBody := &SearchRemoteScopesOutput{}

	// append project to output
	for _, project := range resBody {
		scope := project.ConvertApiScope()
		child := RemoteScopesChild{
			Type:     TypeScope,
			Id:       scope.ScopeId(),
			ParentId: nil,
			Name:     scope.ScopeName(),
			FullName: scope.ScopeFullName(),
			Data:     scope,
		}

		outputBody.Children = append(outputBody.Children, child)
	}

	outputBody.Page = p
	outputBody.PageSize = ps

	return &plugin.ApiResourceOutput{Body: outputBody, Status: http.StatusOK}, nil
}

func getPageTokenFromPageData(pageData *RemoteQueryData) (string, errors.Error) {
	// Marshal json
	pageTokenDecode, err := json.Marshal(pageData)
	if err != nil {
		return "", errors.Default.Wrap(err, fmt.Sprintf("Marshal pageToken failed %+v", pageData))
	}

	// Encode pageToken Base64
	return base64.StdEncoding.EncodeToString(pageTokenDecode), nil
}

func getPageDataFromPageToken(pageToken string) (*RemoteQueryData, errors.Error) {
	return getPageDataFromPageTokenWithTag(pageToken, TypeGroup)
}

func getPageDataFromPageTokenWithTag(pageToken string, queryTag string) (*RemoteQueryData, errors.Error) {
	if pageToken == "" {
		return &RemoteQueryData{
			Page:    1,
			PerPage: remoteScopesPerPage,
			Tag:     queryTag,
		}, nil
	}

	// Decode pageToken Base64
	pageTokenDecode, err := base64.StdEncoding.DecodeString(pageToken)
	if err != nil {
		return nil, errors.Default.Wrap(err, fmt.Sprintf("decode pageToken failed %s", pageToken))
	}
	// Unmarshal json
	pt := &RemoteQueryData{}
	err = json.Unmarshal(pageTokenDecode, pt)
	if err != nil {
		return nil, errors.Default.Wrap(err, fmt.Sprintf("json Unmarshal pageTokenDecode failed %s", pageTokenDecode))
	}

	return pt, nil
}
