package oauth2

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

	"go.uber.org/zap"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/auth-server/commons/config"
	"github.com/tidbcloud/auth-server/commons/errs"
	"github.com/tidbcloud/auth-server/commons/grpc"
	"github.com/tidbcloud/auth-server/internal/entity"
	"github.com/tidbcloud/auth-server/internal/repository"
	tierrs "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
)

const (
	// codeExpiresInSeconds is the expiration time of the authorization code
	codeExpiresInSeconds = 60
)

type AuthorizeRequest struct {
	Application *entity.Oauth2Application
	ClientID    string
	RedirectURI *url.URL
	OrgRole     string
	ProjectRole string
	State       string
}

func (o *Oauth2ServiceImpl) InnerAuthorizationCodeRequestValidate(
	ctx context.Context, user *account.User, clientID, responseType, rawRedirectURI, state string, scopes []string,
) (*AuthorizeRequest, tierrs.Error) {
	db := o.db.WithContext(ctx)
	app, err := repository.GetOauth2ApplicationByClientID(ctx, db, clientID)
	tilog.Info(ctx, "InnerAuthorizationCodeRequestValidate GetOauth2ApplicationByClientID", zap.Any("clientID", clientID), zap.Any("app", app))
	if err != nil {
		return nil, tierrs.NewBadRequest(errs.ErrOauth2InvalidClient.Error(), nil)
	}
	if app.GrantType != entity.Oauth2AuthorizationCode {
		return nil, tierrs.NewBadRequest(errs.ErrOauth2InvalidClient.Error(), nil)
	}

	req := &AuthorizeRequest{
		Application: app,
		ClientID:    clientID,
		State:       state,
	}
	// we need to first validate redirectURI to get the correct redirectURI
	if req.RedirectURI, err = validateRedirectURI(app.RedirectURI, rawRedirectURI); err != nil {
		return req, tierrs.NewBadRequest(errs.ErrOauth2RedirectURIMismatch.Error(), nil)
	}
	if responseType != "code" {
		return req, tierrs.NewBadRequest(errs.ErrOauth2UnsupportedResponseType.Error(), nil)
	}
	if req.OrgRole, req.ProjectRole, err = validateScope(ctx, app.Scopes, scopes); err != nil {
		return req, tierrs.NewBadRequest(errs.ErrOauth2InvalidScope.Error(), nil)
	}
	return req, nil
}

// InnerAuthorizationCodeApprove generates grant by AuthorizeRequest and return redirectURI
func (o *Oauth2ServiceImpl) InnerAuthorizationCodeApprove(
	ctx context.Context, user *account.User, req *AuthorizeRequest, orgID uint64, projectIDs []uint64,
) (*entity.Oauth2CodeGrant, *url.URL, tierrs.Error) {
	tilog.Info(
		ctx, "InnerAuthorizationCodeApprove", zap.Uint64("appID", uint64(req.Application.ID)), zap.Uint64("userID", user.Id),
		zap.Uint64("orgId", orgID), zap.Any("projectIDs", projectIDs),
	)
	if err := o.checkUserScopeOrgProjectsPermission(
		ctx, user, orgID, projectIDs, req.OrgRole, req.ProjectRole,
	); err != nil {
		return nil, nil, tierrs.NewBadRequest(err.Error(), nil)
	}
	db := o.db.WithContext(ctx)
	code := NewAuthorizationCode()
	grantObj := &entity.Oauth2CodeGrant{
		Code:          code,
		ExpiresIn:     codeExpiresInSeconds,
		RedirectURI:   req.RedirectURI.String(),
		Scopes:        entity.Oauth2Scope{Org: req.OrgRole, Project: req.ProjectRole},
		ApplicationID: uint64(req.Application.ID),
		UserID:        user.Id,
		UserEmail:     user.Email,
		OrgID:         orgID,
		ProjectIDs:    entity.ProjectIDs{IDs: projectIDs},
	}

	if err := repository.CreateOauth2CodeGrant(ctx, db, grantObj); err != nil {
		tilog.Info(ctx, "CreateOauth2CodeGrant failed", zap.Error(err))
		return nil, nil, tierrs.NewInternalServerError("failed to create oauth2 code grant", err)
	}

	return grantObj, req.RedirectURI, nil
}

func (o *Oauth2ServiceImpl) checkUserScopeOrgProjectsPermission(
	ctx context.Context, user *account.User, orgID uint64, projectIDs []uint64, orgRole, projectRole string,
) error {
	if config.IsLocalEnv() {
		return nil
	}
	resp, err := o.accountProviderClient.VerifyUserOrgAndProjects(ctx, &account.VerifyUserOrgAndProjectsReq{
		UserId:      user.Id,
		OrgId:       orgID,
		ProjectIds:  projectIDs,
		OrgRole:     orgRole,
		ProjectRole: projectRole,
	})

	if err = grpc.FormatError(ctx, err, resp.BaseResp); err != nil {
		tilog.Error(ctx, "failed to call accountProviderClient.VerifyUserOrgAndProjects", zap.Error(err))
		return err
	}

	if !resp.Result {
		return errors.New("user has no permission to access org or projects")
	}
	return nil
}

// validateRedirectURI validates that redirectURI is valid with baseURI
//
// if redirectURI is empty, return baseURI
// if redirectURI is invalid, return baseURI and error
// if redirectURI is not empty, it should meet the following conditions:
//   - its scheme, host, port and path should be the same as baseURI
//   - its query and fragment are ignored
func validateRedirectURI(baseURIs string, reqRedirectURI string) (*url.URL, error) {
	reqRedirect, err := url.Parse(reqRedirectURI)
	if err != nil {
		return nil, err
	}
	dbURIs := strings.Split(baseURIs, ",")

	for _, uri := range dbURIs {
		base, err := url.Parse(uri)
		if err != nil {
			return nil, err
		}
		// if input redirectURI is empty, return app's first redirectURI
		if reqRedirectURI == "" {
			return base, nil
		}

		if reqRedirect.Scheme == base.Scheme && reqRedirect.Host == base.Host &&
			reqRedirect.Port() == base.Port() && reqRedirect.Path == base.Path {
			return base, nil
		}

	}
	return nil, errors.New("redirect_uri mismatch")

}

// validateScope validates that reqScope is valid for appScope
func validateScope(ctx context.Context, appScope entity.Oauth2Scope, reqScopes []string) (orgRole, projectRole string, err error) {
	tilog.Info(ctx, "validateScope", zap.Any("appScope", appScope), zap.Any("reqScopes", reqScopes))
	appOrgRole := appScope.Org
	appProjectRole := appScope.Project

	// if request scope is empty, return app scope
	if len(reqScopes) == 0 {
		return appOrgRole, appProjectRole, nil
	}

	var (
		reqOrgRole     string
		reqProjectRole string
	)

	if len(reqScopes) == 2 && (appOrgRole == "" || appProjectRole == "") {
		msg := "please specify both org and project scope"
		tilog.Info(ctx, "validateScope"+msg)
		return "", "", errors.New(msg)
	}

	for _, s := range reqScopes {
		if strings.HasPrefix(s, "org:") {
			reqOrgRole = s
		} else if strings.HasPrefix(s, "project:") {
			reqProjectRole = s
		}
	}

	if reqOrgRole == "" {
		return "", "", errors.New("please specify both org and project scope")
	}

	// check if request scope's permission is less than or equal to app scope's permission
	result, err := auth.CompareRoleLevels(ctx, auth.RoleScopeOrg, appOrgRole, reqOrgRole)
	if err != nil {
		tilog.Error(ctx, "CompareRoleLevels failed", zap.Error(err))
		return "", "", errors.New("failed to compare org roles")
	}
	if result < 0 {
		return "", "", errors.New("org scope is not allowed")
	}

	// if appProjectRole is empty, ignore check project role
	if reqProjectRole != "" && appProjectRole != "" {
		result, err = auth.CompareRoleLevels(ctx, auth.RoleScopeProject, appProjectRole, reqProjectRole)
		if err != nil {
			tilog.Error(ctx, "CompareRoleLevels failed", zap.Error(err))
			return "", "", errors.New("failed to compare project roles")
		}
		if result < 0 {

			return "", "", errors.New("project scope is not allowed")
		}
	}
	return reqOrgRole, reqProjectRole, nil
}
