//go:generate ${GOBIN}/mockgen -destination client_mock.go -package=accountcli ${GO_MODULE}/pkg/infra/clients/accountcli Client
package accountcli

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/grpcutil"

	"github.com/go-logr/logr"
	"github.com/go-resty/resty/v2"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	RoleOrgOwner  = "org:owner"
	RoleOrgMember = "org:member"

	UserStatusActive = "ACTIVE"
)

type Client interface {
	// GetProxyHandler get a handler to proxy requests for account service
	// The handler will inject the signature headers and forward the request to the account service
	GetProxyHandler(getProxyPath func(ctx *gin.Context) string) gin.HandlerFunc

	ListTenants(ctx context.Context, in *account.OrgFilter) ([]*account.Org, error)
	GetTenant(ctx context.Context, id uint64) (*account.Org, error)
	DeleteTenant(ctx context.Context, id uint64, applicant string) error
	UpdateTenantKind(ctx context.Context, id uint64, kind account.OrgKind) error
	UpdateTenant(ctx context.Context, req *UpdateTenantReq, tenantID string, applicant string) error

	AddImpersonateUser(ctx context.Context, tenantID uint64, userEmail string) error
	DeleteImpersonateUser(ctx context.Context, tenantID uint64, userEmail string) error
	GetImpersonateUser(ctx context.Context, email string) (uint64, bool, error)

	SendEmail(ctx context.Context, req *account.SendEmailReq) error
	SendEmailByLabels(ctx context.Context, req *account.SendEmailByLabelsReq) error

	GetProject(ctx context.Context, id uint64) (*account.Project, error)
	ListProjects(ctx context.Context, in *account.ProjectFilter) ([]*account.Project, error)
	DeleteProjectByID(ctx context.Context, tenantID string, projectID string, applicant string) error
	GetProjectUsersByProjectID(ctx context.Context, projectId uint64) ([]*account.User, error)

	GetUserFromToken(ctx context.Context, token string) (*account.GetUserByTokenResponse, error)
	GetTenantUsers(ctx context.Context, tenantID string, query GetUsersReq, applicant string) (*GetUsersResp, error)
	GetProjectUsers(ctx context.Context, tenantID, projectID string, query GetUsersReq, applicant string) (*GetUsersResp, error)
	AddUserToTenant(ctx context.Context, tenantID, userID, orgRole, applicant string) error
	AddUserToProject(ctx context.Context, tenantID, projectID, userID, projectRole, applicant string) error
	RemoveUserFromTenant(ctx context.Context, tenantID, userID, applicant string) error
	RemoveUserFromProject(ctx context.Context, tenantID, projectID, userID, applicant string) error
	DeleteUser(ctx context.Context, userID, applicant string) error
	CreateFeatureWhitelist(ctx context.Context, featureWl *FeatureWhitelistInfo) error
	UpdateFeatureWhitelist(ctx context.Context, featureWl *FeatureWhitelistInfo) error
	DeleteFeatureWhitelist(ctx context.Context, name string) error
	ListFeatureWhitelists(ctx context.Context, req *ListFeatureWhitelistsReq) (*ListFeatureWhitelistsResp, error)
}

type client struct {
	accountCli  account.AccountAPIServiceClient
	httpCli     *resty.Client
	httpCliAddr *url.URL
	ak          string
	sk          string
	logger      logr.Logger
}

type ProjectRoleReq struct {
	ProjectRole string `json:"project_role"`
}

type OrgRoleReq struct {
	OrgRole string `json:"org_role"`
}

type UpdateTenantReq struct {
	Email   *string `json:"email"`
	Status  *string `json:"status"`
	OrgType *string `json:"org_type"`
}

type GetUsersReq struct {
	Search    string `json:"search"`
	Page      int64  `json:"page"`
	PageSize  int64  `json:"page_size"`
	FullEmail bool   `json:"full_email"`
}

type GetUsersResp struct {
	Total int64   `json:"total"`
	Items []*User `json:"items"`
}

type Project struct {
	Name           string                 `json:"name"`
	ID             uint64                 `json:"id"`
	TenantID       uint64                 `json:"tenant_id"`
	Status         string                 `json:"status"`
	CreatedAt      *timestamppb.Timestamp `json:"created_at"`
	UpdatedAt      *timestamppb.Timestamp `json:"updated_at"`
	DeletedAt      *timestamppb.Timestamp `json:"deleted_at"`
	IsCrossAccount bool                   `json:"is_cross_account"`
	AwsCmekEnabled bool                   `json:"aws_cmek_enabled"`
}

type User struct {
	Id         string                 `json:"id"`
	FirstName  string                 `json:"first_name"`
	LastName   string                 `json:"last_name"`
	Email      string                 `json:"email"`
	Status     string                 `json:"status"`
	Role       string                 `json:"role"`
	InviteTime *timestamppb.Timestamp `json:"invite_time"`
	LoginTime  *timestamppb.Timestamp `json:"login_time"`
}

type FeatureWhitelistInfo struct {
	Name          string                 `json:"name"`
	Description   string                 `json:"description"`
	Owner         string                 `json:"owner"`
	EmailSuffixes []string               `json:"email_suffixes"`
	MinOrgId      string                 `json:"min_org_id"`
	OrgIds        []uint64               `json:"org_ids"`
	CreatedAt     *timestamppb.Timestamp `json:"created_at"`
	UpdatedAt     *timestamppb.Timestamp `json:"updated_at"`
}

type ListFeatureWhitelistsReq struct {
	Name     string `form:"name"`
	Page     uint32 `form:"page"`
	PageSize uint32 `form:"page_size"`
}

type ListFeatureWhitelistsResp struct {
	Total      uint64                  `json:"total"`
	TotalPages uint64                  `json:"total_pages"`
	Page       uint32                  `json:"page"`
	Items      []*FeatureWhitelistInfo `json:"items"`
}

func NewClient(addr string, httpCliAddr string, ak string, sk string, logger logr.Logger) (Client, error) {
	c := &client{
		logger: logger.WithName("AccountCli"),
	}

	opts := grpcutil.PresetDailOption().Default()
	coreSvcConn, err := grpc.NewClient(addr, opts...)
	if err != nil {
		return nil, fmt.Errorf("dial account svc %s failed: %s", addr, err)
	}
	c.accountCli = account.NewAccountAPIServiceClient(coreSvcConn)

	c.httpCliAddr, err = url.Parse(httpCliAddr)
	if err != nil {
		return nil, fmt.Errorf("parse http client addr %s failed: %w", httpCliAddr, err)
	}

	c.ak = ak
	c.sk = sk
	httpCli := resty.New().
		SetTransport(http.DefaultTransport).
		SetTimeout(60 * time.Second).
		SetBaseURL(httpCliAddr).
		SetPreRequestHook(func(_ *resty.Client, r *http.Request) error {
			return signMiddleware(ak, sk, c.logger, r)
		}).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("call account service failed, url: %s, method: %s, status: %s, body: %s",
					r.Request.URL, r.Request.Method, r.Status(), string(r.Body()))
			}
			return nil
		})
	c.httpCli = httpCli

	return c, nil
}

func (c *client) GetProxyHandler(getProxyPath func(ctx *gin.Context) string) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		proxyPath := getProxyPath(ctx)
		logger := log.FromContext(ctx).WithName("AccountCli")
		logger.Info("handle account proxy", "method", ctx.Request.Method, "path", ctx.Request.URL.Path, "proxy_path", proxyPath, "user", ctx.GetString(consts.CtxOperator))

		director := func(req *http.Request) {
			req.URL.Scheme = c.httpCliAddr.Scheme
			req.URL.Host = c.httpCliAddr.Host
			req.URL.Path = proxyPath
			req.Host = c.httpCliAddr.Host // Set the Host to make Kong route correctly
			signMiddleware(c.ak, c.sk, logger, req)
		}
		proxy := &httputil.ReverseProxy{
			Transport: http.DefaultTransport, // same as the client's transport
			Director:  director,
		}
		proxy.ModifyResponse = func(resp *http.Response) error {
			resp.Header.Del("Access-Control-Allow-Origin")
			return nil
		}
		proxy.ServeHTTP(ctx.Writer, ctx.Request)
	}
}

func (c *client) ListTenants(ctx context.Context, in *account.OrgFilter) ([]*account.Org, error) {
	orgs := make([]*account.Org, 0)
	page := int64(1)
	pageSize := int64(1000)

	for {
		resp, err := c.accountCli.ListOrgsAdvance(ctx, &account.ListOrgsParam{
			Filter: in,
			Pager: &base.Pager{
				Page:     page,
				PageSize: pageSize,
			},
		})
		if err != nil {
			return nil, err
		}
		if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
			return nil, err
		}

		orgs = append(orgs, resp.Orgs...)
		if page*pageSize >= resp.Total {
			break
		}
		page++
	}

	return orgs, nil
}

func (c *client) GetTenant(ctx context.Context, id uint64) (*account.Org, error) {
	resp, err := c.accountCli.GetOrgByID(ctx, &account.GetOrgByIDRequest{
		Id:      id,
		IsBrief: false,
	})
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}
	return resp.Org, nil
}

func (c *client) UpdateTenantKind(ctx context.Context, id uint64, kind account.OrgKind) error {
	resp, err := c.accountCli.UpdateOrg(ctx, &account.UpdateOrgRequest{
		Id:   id,
		Kind: kind,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) DeleteTenant(ctx context.Context, tenantID uint64, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id": strconv.FormatUint(tenantID, 10),
		}).
		SetHeader("X-Requester-Email", applicant).
		Delete("/api/v1/admin/orgs/{org_id}")
	return err
}

func (c *client) AddImpersonateUser(ctx context.Context, tenantID uint64, userEmail string) error {
	c.logger.Info("add impersonate user", "tenantID", tenantID, "userEmail", userEmail)
	resp, err := c.accountCli.AddImpersonateUser(ctx, &account.AddImpersonateUserReq{
		OrgId: tenantID,
		Email: userEmail,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) DeleteImpersonateUser(ctx context.Context, tenantID uint64, userEmail string) error {
	c.logger.Info("delete impersonate user", "tenantID", tenantID, "userEmail", userEmail)
	resp, err := c.accountCli.DeleteImpersonateUser(ctx, &account.DeleteImpersonateUserReq{
		OrgId: tenantID,
		Email: userEmail,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) GetImpersonateUser(ctx context.Context, email string) (uint64, bool, error) {
	resp, err := c.accountCli.GetImpersonateUser(ctx, &account.GetImpersonateUserReq{
		Email: email,
	})
	if err != nil {
		return 0, false, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return 0, false, err
	}
	return resp.OrgId, resp.IsImpersonated, nil
}

func (c *client) SendEmail(ctx context.Context, req *account.SendEmailReq) error {
	resp, err := c.accountCli.SendEmail(ctx, req)
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) SendEmailByLabels(ctx context.Context, req *account.SendEmailByLabelsReq) error {
	resp, err := c.accountCli.SendEmailByLabels(ctx, req)
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) ListProjects(ctx context.Context, in *account.ProjectFilter) ([]*account.Project, error) {
	resp, err := c.accountCli.ListProjects(ctx, in)
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}
	return resp.Projects, nil
}

func (c *client) GetProject(ctx context.Context, id uint64) (*account.Project, error) {
	resp, err := c.accountCli.GetProjectByID(ctx, &account.GetProjectByIDRequest{
		Id: id,
	})
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}
	return resp.Project, nil
}

func (c *client) GetUserFromToken(ctx context.Context, token string) (*account.GetUserByTokenResponse, error) {
	resp, err := c.accountCli.GetUserByToken(ctx, &account.GetUserByTokenRequest{
		Token: token,
	})
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}
	return resp, nil
}

func (c *client) UpdateTenant(ctx context.Context, req *UpdateTenantReq, tenantID string, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id": tenantID,
		}).
		SetHeader("X-Requester-Email", applicant).
		SetBody(req).
		Patch("/api/v1/admin/orgs/{org_id}")
	return err
}

func (c *client) AddUserToTenant(ctx context.Context, tenantID, userID, orgRole string, applicant string) error {
	req := &OrgRoleReq{
		OrgRole: orgRole,
	}
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":  tenantID,
			"user_id": userID,
		}).
		SetHeader("X-Requester-Email", applicant).
		SetBody(req).
		Post("/api/v1/admin/orgs/{org_id}/users/{user_id}")
	return err
}

func (c *client) AddUserToProject(ctx context.Context, tenantID, projectID, userID, projectRole string, applicant string) error {
	req := &ProjectRoleReq{
		ProjectRole: projectRole,
	}
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":     tenantID,
			"project_id": projectID,
			"user_id":    userID,
		}).
		SetHeader("X-Requester-Email", applicant).
		SetBody(req).
		Post("/api/v1/admin/orgs/{org_id}/projects/{project_id}/users/{user_id}")
	return err
}

func (c *client) RemoveUserFromTenant(ctx context.Context, tenantID, userID string, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":  tenantID,
			"user_id": userID,
		}).
		SetHeader("X-Requester-Email", applicant).
		Delete("/api/v1/admin/orgs/{org_id}/users/{user_id}")
	return err
}

func (c *client) RemoveUserFromProject(ctx context.Context, tenantID, projectID, userID string, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":     tenantID,
			"project_id": projectID,
			"user_id":    userID,
		}).
		SetHeader("X-Requester-Email", applicant).
		Delete("/api/v1/admin/orgs/{org_id}/projects/{project_id}/users/{user_id}")
	return err
}

func (c *client) DeleteProjectByID(ctx context.Context, tenantID, projectID string, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":     tenantID,
			"project_id": projectID,
		}).
		SetHeader("X-Requester-Email", applicant).
		Delete("/api/v1/admin/orgs/{org_id}/projects/{project_id}")
	return err
}

func (c *client) DeleteUser(ctx context.Context, userID string, applicant string) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"user_id": userID,
		}).
		SetHeader("X-Requester-Email", applicant).
		Delete("/api/v1/admin/users/{user_id}")
	return err
}

func (c *client) GetTenantUsers(ctx context.Context, tenantID string, query GetUsersReq, applicant string) (*GetUsersResp, error) {
	var output GetUsersResp
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id": tenantID,
		}).
		SetQueryParams(map[string]string{
			"search":    query.Search,
			"page":      strconv.FormatInt(query.Page, 10),
			"page_size": strconv.FormatInt(query.PageSize, 10),
		}).
		SetResult(&output).
		SetHeader("X-Requester-Email", applicant).
		Get("/api/v1/admin/orgs/{org_id}/users")

	if err != nil {
		return nil, err
	}

	if !query.FullEmail {
		for _, u := range output.Items {
			u.Email = maskEmail(u.Email)
		}
	}

	return &output, nil
}

func (c *client) GetProjectUsers(ctx context.Context, tenantID, projectID string, query GetUsersReq, applicant string) (*GetUsersResp, error) {
	var output GetUsersResp
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"org_id":     tenantID,
			"project_id": projectID,
		}).
		SetQueryParams(map[string]string{
			"search":    query.Search,
			"page":      strconv.FormatInt(int64(query.Page), 10),
			"page_size": strconv.FormatInt(int64(query.PageSize), 10),
		}).
		SetResult(&output).
		SetHeader("X-Requester-Email", applicant).
		Get("/api/v1/admin/orgs/{org_id}/projects/{project_id}/users")
	if err != nil {
		return nil, err
	}

	if !query.FullEmail {
		for _, u := range output.Items {
			u.Email = maskEmail(u.Email)
		}
	}
	return &output, nil
}

func (c *client) GetProjectUsersByProjectID(ctx context.Context, projectId uint64) ([]*account.User, error) {
	req := account.ListUsersByFilterRequest{
		Filter: &account.ListUsersFilter{
			ScopeTypes:  []account.ScopeType{account.ScopeType_SCOPE_TYPE_PROJECT},
			ScopeIds:    []uint64{projectId},
			TargetTypes: []account.TargetType{account.TargetType_TARGET_TYPE_USER},
		},
	}
	resp, err := c.accountCli.ListUsersByFilter(ctx, &req)
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}
	c.logger.Info("total users got", "count", len(resp.Users))
	return resp.Users, nil
	// project, err := c.GetProject(ctx, projectId)
	// if err != nil {
	// 	return nil, err
	// }
	// return c.GetProjectUsersWithoutPage(ctx, strconv.FormatUint(project.TenantId, 10), strconv.FormatUint(projectId, 10), c.requester)
}

func maskEmail(email string) string {
	atIndex := strings.Index(email, "@")
	if atIndex != -1 && atIndex > 0 {
		email = fmt.Sprintf("%s%s%s", string(email[0]), strings.Repeat("*", atIndex-1), email[atIndex:])
	}
	return email
}

func (c *client) CreateFeatureWhitelist(ctx context.Context, featureWl *FeatureWhitelistInfo) error {
	resp, err := c.accountCli.CreateFeatureFlag(ctx, &account.CreateFeatureFlagReq{
		FlagName:    featureWl.Name,
		Description: featureWl.Description,
		Owner:       featureWl.Owner,
		EmailSuffix: featureWl.EmailSuffixes,
		MinOrgId:    featureWl.MinOrgId,
		OrgIds:      featureWl.OrgIds,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) UpdateFeatureWhitelist(ctx context.Context, featureWl *FeatureWhitelistInfo) error {
	resp, err := c.accountCli.UpdateFeatureFlag(ctx, &account.UpdateFeatureFlagReq{
		FlagName:    featureWl.Name,
		Description: featureWl.Description,
		Owner:       featureWl.Owner,
		EmailSuffix: featureWl.EmailSuffixes,
		MinOrgId:    featureWl.MinOrgId,
		OrgIds:      featureWl.OrgIds,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) DeleteFeatureWhitelist(ctx context.Context, name string) error {
	resp, err := c.accountCli.DeleteFeatureFlag(ctx, &account.DeleteFeatureFlagReq{
		FlagName: name,
	})
	if err != nil {
		return err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return err
	}
	return nil
}

func (c *client) ListFeatureWhitelists(ctx context.Context, req *ListFeatureWhitelistsReq) (*ListFeatureWhitelistsResp, error) {
	page := req.Page
	if page == 0 {
		page = 1
	}
	pageSize := req.PageSize
	if pageSize == 0 {
		pageSize = 20
	}

	resp, err := c.accountCli.ListFeatureFlags(ctx, &account.ListFeatureFlagsReq{
		FlagName: req.Name,
		Pager: &account.Pager{
			Page:     page,
			PageSize: pageSize,
		},
	})
	if err != nil {
		return nil, err
	}
	if err := grpcutil.RespToErr(resp.BaseResp); err != nil {
		return nil, err
	}

	var items []*FeatureWhitelistInfo
	for _, f := range resp.Flags {
		items = append(items, &FeatureWhitelistInfo{
			Name:          f.FlagName,
			Description:   f.Description,
			Owner:         f.Owner,
			EmailSuffixes: f.EmailSuffixes,
			MinOrgId:      f.MinOrgId,
			OrgIds:        f.OrgIds,
			CreatedAt:     f.CreatedAt,
			UpdatedAt:     f.UpdatedAt,
		})
	}

	result := &ListFeatureWhitelistsResp{
		Total:      0,
		TotalPages: 0,
		Page:       page,
		Items:      items,
	}
	if resp.PagerResp != nil {
		result.Total = resp.PagerResp.Total
		result.TotalPages = resp.PagerResp.TotalPages
	}
	return result, nil
}
