//go:generate ${GOBIN}/mockgen -destination client_mock.go -package=account github.com/tidbcloud/aisupport/services/jsm-syncer/internal/integrations/account Client
package account

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	accountpb "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/aisupport/services/jsm-syncer/config"
	basepb "go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	defaultPageSize   = 500
	systemEmailSuffix = "-system@pingcap.com"

	activeStatus  = "active"
	pendingStatus = "pending"
)

type Client interface {
	Configured() bool
	GetUserByEmail(ctx context.Context, email string) (*User, error)
	GetUserByID(ctx context.Context, userID string) (*User, error)
	ListOrgs(ctx context.Context, opts ListOrgsOptions) (OrgPage, error)
	ListOrgUsers(ctx context.Context, orgID string) ([]User, error)
	ListPartnerCustomers(ctx context.Context, customerOrgIDs []string) (map[string]PartnerCustomer, error)
}

type ListOrgsOptions struct {
	Page     int
	PageSize int
	Statuses []string
}

type OrgPage struct {
	Orgs  []Org
	Total int
}

type Org struct {
	ID       string
	Name     string
	Status   string
	Plan     string
	TimeZone string
}

type User struct {
	ID        string
	Email     string
	FirstName string
	LastName  string
}

type PartnerCustomer struct {
	ID            string
	PartnerOrgID  string
	CustomerOrgID string
	Status        string
	EffectedAt    time.Time
}

type providerClient interface {
	GetUserByEmail(ctx context.Context, in *accountpb.GetUserByEmailRequest, opts ...grpc.CallOption) (*accountpb.GetUserByEmailResponse, error)
	GetUserByID(ctx context.Context, in *accountpb.GetUserByIDRequest, opts ...grpc.CallOption) (*accountpb.GetUserByIDResponse, error)
	ListOrgsAdvance(ctx context.Context, in *accountpb.ListOrgsParam, opts ...grpc.CallOption) (*accountpb.ListOrgsAdvanceResponse, error)
	ListUsersByFilter(ctx context.Context, in *accountpb.ListUsersByFilterRequest, opts ...grpc.CallOption) (*accountpb.ListUsersByFilterResponse, error)
	ListPartnerCustomers(ctx context.Context, in *accountpb.ListPartnerCustomersReq, opts ...grpc.CallOption) (*accountpb.ListPartnerCustomersResp, error)
}

type grpcClient struct {
	conn     *grpc.ClientConn
	timeout  time.Duration
	pageSize int
	provider providerClient
}

func NewClient(conf config.AccountConfig) (Client, error) {
	address := strings.TrimSpace(conf.GRPCAddress)
	if address == "" {
		return NewUnimplementedClient(), nil
	}

	conn, err := grpc.NewClient(
		address,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
	)
	if err != nil {
		return nil, fmt.Errorf("dial account grpc %s: %w", address, err)
	}

	return newClientWithProvider(conf, conn, accountpb.NewAccountAPIServiceClient(conn)), nil
}

func newClientWithProvider(conf config.AccountConfig, conn *grpc.ClientConn, provider providerClient) Client {
	timeout := conf.Timeout
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	pageSize := conf.PageSize
	if pageSize <= 0 {
		pageSize = defaultPageSize
	}

	return &grpcClient{
		conn:     conn,
		timeout:  timeout,
		pageSize: pageSize,
		provider: provider,
	}
}

type UnimplementedClient struct{}

func NewUnimplementedClient() Client {
	return UnimplementedClient{}
}

func (UnimplementedClient) Configured() bool {
	return false
}

func (UnimplementedClient) GetUserByEmail(ctx context.Context, email string) (*User, error) {
	return nil, nil
}

func (UnimplementedClient) GetUserByID(ctx context.Context, userID string) (*User, error) {
	return nil, nil
}

func (UnimplementedClient) ListOrgs(ctx context.Context, opts ListOrgsOptions) (OrgPage, error) {
	return OrgPage{}, nil
}

func (UnimplementedClient) ListOrgUsers(ctx context.Context, orgID string) ([]User, error) {
	return nil, nil
}

func (UnimplementedClient) ListPartnerCustomers(ctx context.Context, customerOrgIDs []string) (map[string]PartnerCustomer, error) {
	return map[string]PartnerCustomer{}, nil
}

func (c *grpcClient) Configured() bool {
	return c != nil && c.provider != nil
}

func (c *grpcClient) GetUserByEmail(ctx context.Context, email string) (*User, error) {
	email = strings.TrimSpace(email)
	if email == "" {
		return nil, fmt.Errorf("email is required")
	}
	if !c.Configured() {
		return nil, nil
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	resp, err := c.provider.GetUserByEmail(ctx, &accountpb.GetUserByEmailRequest{Email: email})
	if err != nil {
		return nil, fmt.Errorf("account GetUserByEmail %s: %w", email, err)
	}
	user := toUser(resp.GetUser())
	if user.ID == "" {
		return nil, nil
	}
	return &user, nil
}

func (c *grpcClient) GetUserByID(ctx context.Context, userID string) (*User, error) {
	if !c.Configured() {
		return nil, nil
	}

	parsed, err := parseUintID("user_id", userID)
	if err != nil {
		return nil, err
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	resp, err := c.provider.GetUserByID(ctx, &accountpb.GetUserByIDRequest{UserId: parsed})
	if err != nil {
		return nil, fmt.Errorf("account GetUserByID %s: %w", userID, err)
	}
	user := toUser(resp.GetUser())
	if user.ID == "" {
		return nil, nil
	}
	return &user, nil
}

func (c *grpcClient) ListOrgs(ctx context.Context, opts ListOrgsOptions) (OrgPage, error) {
	if !c.Configured() {
		return OrgPage{}, nil
	}

	page := opts.Page
	if page <= 0 {
		page = 1
	}
	pageSize := opts.PageSize
	if pageSize <= 0 {
		pageSize = c.pageSize
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	req := &accountpb.ListOrgsParam{
		Pager: &basepb.Pager{
			Page:     int64(page),
			PageSize: int64(pageSize),
		},
	}
	if statuses := trimStrings(opts.Statuses); len(statuses) > 0 {
		req.Filter = &accountpb.OrgFilter{
			Status: statuses,
		}
	}

	resp, err := c.provider.ListOrgsAdvance(ctx, req)
	if err != nil {
		return OrgPage{}, fmt.Errorf("account ListOrgsAdvance page %d: %w", page, err)
	}

	orgs := make([]Org, 0, len(resp.GetOrgs()))
	for _, item := range resp.GetOrgs() {
		org := toOrg(item)
		if org.ID == "" {
			continue
		}
		orgs = append(orgs, org)
	}

	return OrgPage{
		Orgs:  orgs,
		Total: int(resp.GetTotal()),
	}, nil
}

func (c *grpcClient) ListOrgUsers(ctx context.Context, orgID string) ([]User, error) {
	if !c.Configured() {
		return nil, nil
	}

	orgUintID, err := parseUintID("org_id", orgID)
	if err != nil {
		return nil, err
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	resp, err := c.provider.ListUsersByFilter(ctx, &accountpb.ListUsersByFilterRequest{
		Filter: &accountpb.ListUsersFilter{
			ScopeTypes:  []accountpb.ScopeType{accountpb.ScopeType_SCOPE_TYPE_ORG},
			ScopeIds:    []uint64{orgUintID},
			TargetTypes: []accountpb.TargetType{accountpb.TargetType_TARGET_TYPE_USER},
		},
	})
	if err != nil {
		return nil, fmt.Errorf("account ListUsersByFilter org %s: %w", orgID, err)
	}

	users := make([]User, 0, len(resp.GetUsers()))
	for _, item := range resp.GetUsers() {
		user := toUser(item)
		if user.ID == "" {
			continue
		}
		users = append(users, user)
	}
	return users, nil
}

func (c *grpcClient) ListPartnerCustomers(ctx context.Context, customerOrgIDs []string) (map[string]PartnerCustomer, error) {
	result := map[string]PartnerCustomer{}
	if !c.Configured() || len(customerOrgIDs) == 0 {
		return result, nil
	}

	ids, err := parseUintIDs("customer_org_ids", customerOrgIDs)
	if err != nil {
		return nil, err
	}

	ctx, cancel := c.withTimeout(ctx)
	defer cancel()

	resp, err := c.provider.ListPartnerCustomers(ctx, &accountpb.ListPartnerCustomersReq{
		PartnerCustomerIds: ids,
		Status:             []string{activeStatus},
		PartnerType:        accountpb.PartnerType_PARTNER_TYPE_MSP,
	})
	if err != nil {
		return nil, fmt.Errorf("account ListPartnerCustomers: %w", err)
	}

	for _, item := range resp.GetPartnerCustomers() {
		customer := toPartnerCustomer(item)
		if customer.CustomerOrgID == "" {
			continue
		}
		existing, exists := result[customer.CustomerOrgID]
		if !exists || customer.EffectedAt.After(existing.EffectedAt) {
			result[customer.CustomerOrgID] = customer
		}
	}
	return result, nil
}

func (c *grpcClient) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
	if c.timeout <= 0 {
		return context.WithCancel(ctx)
	}
	return context.WithTimeout(ctx, c.timeout)
}

func toOrg(item *accountpb.Org) Org {
	if item == nil {
		return Org{}
	}
	return Org{
		ID:       strconv.FormatUint(item.GetId(), 10),
		Name:     strings.TrimSpace(item.GetName()),
		Status:   strings.TrimSpace(item.GetStatus()),
		Plan:     strings.TrimSpace(item.GetPlan()),
		TimeZone: strings.TrimSpace(item.GetTimeZone()),
	}
}

func toUser(item *accountpb.User) User {
	if item == nil {
		return User{}
	}
	email := strings.TrimSpace(item.GetEmail())
	if strings.HasSuffix(strings.ToLower(email), systemEmailSuffix) {
		return User{}
	}
	return User{
		ID:        strconv.FormatUint(item.GetId(), 10),
		Email:     email,
		FirstName: strings.TrimSpace(item.GetFirstName()),
		LastName:  strings.TrimSpace(item.GetLastName()),
	}
}

func toPartnerCustomer(item *accountpb.PartnerCustomers) PartnerCustomer {
	if item == nil {
		return PartnerCustomer{}
	}
	return PartnerCustomer{
		ID:            strconv.FormatUint(item.GetId(), 10),
		PartnerOrgID:  strconv.FormatUint(item.GetPartnerOrgId(), 10),
		CustomerOrgID: strconv.FormatUint(item.GetCustomerOrgId(), 10),
		Status:        strings.TrimSpace(item.GetStatus()),
		EffectedAt:    timestampToTime(item.GetEffectedAt()),
	}
}

func timestampToTime(ts *timestamppb.Timestamp) time.Time {
	if ts == nil {
		return time.Time{}
	}
	return ts.AsTime()
}

func trimStrings(values []string) []string {
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		result = append(result, value)
	}
	return result
}

func parseUintIDs(name string, ids []string) ([]uint64, error) {
	result := make([]uint64, 0, len(ids))
	for _, id := range ids {
		parsed, err := parseUintID(name, id)
		if err != nil {
			return nil, err
		}
		result = append(result, parsed)
	}
	return result, nil
}

func parseUintID(name string, id string) (uint64, error) {
	id = strings.TrimSpace(id)
	if id == "" {
		return 0, fmt.Errorf("%s is required", name)
	}
	parsed, err := strconv.ParseUint(id, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("parse %s %q: %w", name, id, err)
	}
	return parsed, nil
}
