package service

import (
	"context"
	"errors"
	"net/url"
	"path"
	"strconv"
	"strings"

	appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
	"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
	"github.com/opencloud-eu/reva/v2/pkg/storagespace"
	"github.com/opencloud-eu/reva/v2/pkg/utils"
	microstore "go-micro.dev/v4/store"

	"github.com/qsfera/server/pkg/log"
	"github.com/qsfera/server/services/collaboration/pkg/config"
	"github.com/qsfera/server/services/collaboration/pkg/helpers"
	"github.com/qsfera/server/services/collaboration/pkg/middleware"
	"github.com/qsfera/server/services/collaboration/pkg/wopisrc"
)

// NewHandler creates a new grpc service implementing the OpenInApp interface
func NewHandler(opts ...Option) (*Service, func(), error) {
	teardown := func() {
		/* this is required as a argument for the return value to satisfy the interface */
		/* in case you are wondering about the necessity of this comment, sonarcloud is asking for it */
	}
	options := newOptions(opts...)

	gatewaySelector := options.GatewaySelector
	var err error
	if gatewaySelector == nil {
		gatewaySelector, err = pool.GatewaySelector(options.Config.CS3Api.Gateway.Name)
		if err != nil {
			return nil, teardown, err
		}
	}

	if options.AppURLs == nil {
		return nil, teardown, errors.New("AppURLs option is required")
	}

	return &Service{
		id:              options.Config.GRPC.Namespace + "." + options.Config.Service.Name,
		appURLs:         options.AppURLs,
		logger:          options.Logger,
		config:          options.Config,
		gatewaySelector: gatewaySelector,
		store:           options.Store,
	}, teardown, nil
}

// Service implements the OpenInApp interface
type Service struct {
	id              string
	appURLs         *helpers.AppURLs
	logger          log.Logger
	config          *config.Config
	gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
	store           microstore.Store
}

// OpenInApp will implement the OpenInApp interface of the app provider
func (s *Service) OpenInApp(
	ctx context.Context,
	req *appproviderv1beta1.OpenInAppRequest,
) (*appproviderv1beta1.OpenInAppResponse, error) {

	// get the current user
	var user *userv1beta1.User = nil
	meReq := &gatewayv1beta1.WhoAmIRequest{
		Token: req.GetAccessToken(),
	}
	gwc, err := s.gatewaySelector.Next()
	if err != nil {
		s.logger.Error().Err(err).Msg("OpenInApp: could not select a gateway client")
		return nil, err
	}

	meResp, err := gwc.WhoAmI(ctx, meReq)
	if err == nil {
		if meResp.GetStatus().GetCode() == rpcv1beta1.Code_CODE_OK {
			user = meResp.GetUser()
		}
	}

	// required for the response, it will be used also for logs
	providerFileRef := providerv1beta1.Reference{
		ResourceId: req.GetResourceInfo().GetId(),
		Path:       ".",
	}

	logger := s.logger.With().
		Str("FileReference", providerFileRef.String()).
		Str("ViewMode", req.GetViewMode().String()).
		Str("Requester", user.GetId().String()).
		Logger()

	// get the file extension to use the right wopi app url
	fileExt := path.Ext(req.GetResourceInfo().GetPath())

	// get the appURL we need to use
	appURL := s.getAppUrl(fileExt, req.GetViewMode())
	if appURL == "" {
		logger.Error().Msg("OpenInApp: neither edit nor view app URL found")
		return nil, errors.New("neither edit nor view app URL found")
	}

	// append the parameters we need
	appURL, err = s.addQueryToURL(appURL, req)
	if err != nil {
		logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl")
		return &appproviderv1beta1.OpenInAppResponse{
			Status: &rpcv1beta1.Status{
				Code:    rpcv1beta1.Code_CODE_INVALID_ARGUMENT,
				Message: "OpenInApp: error parsing appUrl",
			},
		}, nil
	}

	// create the wopiContext and generate the token
	wopiContext := middleware.WopiContext{
		AccessToken:   req.GetAccessToken(), // it will be encrypted
		ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
		FileReference: &providerFileRef,
		ViewMode:      req.GetViewMode(),
	}

	if templateID := utils.ReadPlainFromOpaque(req.GetOpaque(), "template"); templateID != "" {
		// we can ignore the error here, as we are sure that the templateID is not empty
		templateRes, _ := storagespace.ParseID(templateID)
		// we need to have at least both opaqueID and spaceID set
		if templateRes.GetOpaqueId() == "" || templateRes.GetSpaceId() == "" {
			logger.Error().Err(err).Msg("OpenInApp: error parsing templateID")
			return &appproviderv1beta1.OpenInAppResponse{
				Status: &rpcv1beta1.Status{
					Code:    rpcv1beta1.Code_CODE_INVALID_ARGUMENT,
					Message: "OpenInApp: error parsing templateID",
				},
			}, nil
		}
		wopiContext.TemplateReference = &providerv1beta1.Reference{
			ResourceId: &templateRes,
			Path:       ".",
		}
	}

	accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config, s.store)
	if err != nil {
		logger.Error().Err(err).Msg("OpenInApp: error generating the token")
		return &appproviderv1beta1.OpenInAppResponse{
			Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
		}, err
	}

	logger.Debug().Msg("OpenInApp: success")

	return &appproviderv1beta1.OpenInAppResponse{
		Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
		AppUrl: &appproviderv1beta1.OpenInAppURL{
			AppUrl: appURL,
			Method: "POST",
			FormParameters: map[string]string{
				// these parameters will be passed to the web server by the app provider application
				"access_token": accessToken,
				// milliseconds since Jan 1, 1970 UTC as required in https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#access_token_ttl
				//"access_token_ttl": strconv.FormatInt(claims.ExpiresAt.UnixMilli(), 10),
				"access_token_ttl": strconv.FormatInt(accessExpiration, 10),
			},
		},
	}, nil
}

// getAppUrl will get the appURL that should be used based on the extension
// and the provided view mode.
// "view" urls will be chosen first, then if the view mode is "read/write",
// "edit" urls will be prioritized. Note that "view" url might be returned for
// "read/write" view mode if no "edit" url is found.
func (s *Service) getAppUrl(fileExt string, viewMode appproviderv1beta1.ViewMode) string {
	// prioritize view action if possible
	appURL := s.appURLs.GetAppURLFor("view", fileExt)

	if strings.ToLower(s.config.App.Product) == "collabora" {
		// collabora provides only one action per extension. usual options
		// are "view" (checked above), "edit" or "view_comment" (this last one
		// is exclusive of collabora)
		if appURL == "" {
			if editURL := s.appURLs.GetAppURLFor("edit", fileExt); editURL != "" {
				return editURL
			}
			if commentURL := s.appURLs.GetAppURLFor("view_comment", fileExt); commentURL != "" {
				return commentURL
			}
		}
	} else {
		// If not collabora, there might be an edit action for the extension.
		// If read/write mode has been requested, prioritize edit action.
		if viewMode == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
			if editAppURL := s.appURLs.GetAppURLFor("edit", fileExt); editAppURL != "" {
				appURL = editAppURL
			}
		}
	}

	return appURL
}

// addQueryToURL will add specific query parameters to the baseURL. These
// parameters are:
// * "WOPISrc" pointing to the requested resource in the OpenInAppRequest
// * "dchat" to disable the chat, based on configuration
// * "lang" (WOPI app dependent) with the language in the request. "lang"
// for collabora, "ui" for onlyoffice and "UI_LLCC" for the rest
func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAppRequest) (string, error) {
	u, err := url.Parse(baseURL)
	if err != nil {
		return "", err
	}

	// build a urlsafe and stable file reference that can be used for proxy routing,
	// so that all sessions on one file end on the same office server
	fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())

	wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, s.config)
	if err != nil {
		return "", err
	}

	q := u.Query()
	q.Add("WOPISrc", wopiSrcURL.String())

	if s.config.Wopi.DisableChat {
		q.Add("dchat", "1")
	}

	lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")

	// @TODO: this is a temporary solution until we figure out how to send these from oc web
	switch lang {
	case "bg":
		lang = "bg-BG"
	case "cs":
		lang = "cs-CZ"
	case "de":
		lang = "de-DE"
	case "en":
		lang = "en-GB"
	case "es":
		lang = "es-ES"
	case "fr":
		lang = "fr-FR"
	case "gl":
		lang = "gl-ES"
	case "it":
		lang = "it-IT"
	case "nl":
		lang = "nl-NL"
	case "ko":
		lang = "ko-KR"
	case "sq":
		lang = "sq-AL"
	case "sv":
		lang = "sv-SE"
	case "tr":
		lang = "tr-TR"
	case "zh":
		lang = "zh-CN"
	}

	if lang != "" {
		switch strings.ToLower(s.config.App.Product) {
		case "collabora":
			q.Add("lang", lang)
		case "onlyoffice":
			q.Add("ui", lang)
		default:
			q.Add("UI_LLCC", lang)
		}
	}

	if strings.ToLower(s.config.App.Product) == "collabora" {
		q.Add("closebutton", "false")
	}

	qs := q.Encode()
	u.RawQuery = qs

	return u.String(), nil
}
