package gcp

import (
	"context"
	"errors"
	"fmt"

	"github.com/tidbcloud/observability/pkg/common/metrics"

	cloudiam "cloud.google.com/go/iam"
	"github.com/tidbcloud/observability/pkg/resourceworker/providers/gcp/giam"
	"google.golang.org/api/googleapi"
	iam "google.golang.org/api/iam/v1"
)

const (
	iamClientLabel = "iam-client"

	errCreateServiceAccountFailed = "create service account failed"
	errDeleteServiceAccountFailed = "delete service account failed"
	errGetServiceAccountFailed    = "get service account failed"
	errGetServiceAccountIAMFailed = "get service account iam policy failed"
	errSetServiceAccountIAMFailed = "set service account iam policy failed"
	errGetBucketPolicyFailed      = "get bucket policy failed"
	errSetBucketPolicyFailed      = "set bucket policy failed"
	errDeleteBucketPolicyFailed   = "delete bucket policy failed"
)

type IAMClient struct {
	cscli   CloudStorageClient
	service giam.Iamclient
	project string
}

func NewIAM(project string, region string) (*IAMClient, error) {
	//we need reuse service,so this ctx is useless
	service, err := iam.NewService(context.Background())
	if err != nil {
		return nil, fmt.Errorf("iam.NewService: %v", err)
	}
	return &IAMClient{
		cscli:   *NewCloudStorage(project, region),
		service: &giam.ProjectsServiceAccountsServiceWrap{ProjectsServiceAccountsService: service.Projects.ServiceAccounts},
		project: project,
	}, nil
}

func (ic *IAMClient) AssumeProject(ctx context.Context, project string) *IAMClient {
	// in gcp, we do not need to assume role,we only change project.
	// the service account should have the permission to access the project
	return &IAMClient{
		// we do not need to change cscli, because the cloud storage we only need in own project
		cscli:   ic.cscli,
		service: ic.service,
		project: project,
	}
}

// CreateServiceAccount creates a service account.
func (ic *IAMClient) CreateServiceAccount(ctx context.Context, name string) (*iam.ServiceAccount, error) {
	request := &iam.CreateServiceAccountRequest{
		AccountId: name,
		ServiceAccount: &iam.ServiceAccount{
			DisplayName: name,
		},
	}
	account, err := ic.service.Create(ctx, "projects/"+ic.project, request).Do()
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, iamClientLabel, "Create", name, errCreateServiceAccountFailed)
		return nil, fmt.Errorf("Projects.ServiceAccounts.Create: %v", err)
	}
	return account, nil
}

// DeleteServiceAccount deletes a service account.
func (ic *IAMClient) DeleteServiceAccount(ctx context.Context, name string) error {
	email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, ic.project)
	_, err := ic.service.Delete(ctx, "projects/-/serviceAccounts/"+email).Do()
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, iamClientLabel, "Delete", name, errDeleteServiceAccountFailed)
		return fmt.Errorf("Projects.ServiceAccounts.Delete: %v", err)
	}
	return nil
}

// GetServiceAccount get a service account.
func (ic *IAMClient) GetServiceAccount(ctx context.Context, name string) (*iam.ServiceAccount, bool, error) {
	email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, ic.project)
	resource := "projects/-/serviceAccounts/" + email
	account, err := ic.service.Get(ctx, resource).Do()
	if err != nil {
		var gapierr *googleapi.Error
		if errors.As(err, &gapierr) {
			if gapierr.Code == 403 || gapierr.Code == 404 {
				//if code is 403 or 404 , we think the account is not exists
				return nil, false, nil
			}
		}
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, iamClientLabel, "Get", name, errGetServiceAccountFailed)
		return nil, false, err
	}
	return account, true, nil
}

// GetServiceAccountIAM get a service account IAM binding.
func (ic *IAMClient) GetServiceAccountIAM(ctx context.Context, name string) (*iam.Policy, error) {
	email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, ic.project)
	resource_prefix := fmt.Sprintf("projects/%s/serviceAccounts/", ic.project)

	policy, err := ic.service.GetIamPolicy(ctx, resource_prefix+email).Do()
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, iamClientLabel, "GetIamPolicy", name, errGetServiceAccountIAMFailed)
		return nil, fmt.Errorf("Projects.ServiceAccounts.GetIamPolicy: %v", err)
	}
	return policy, nil
}

// SetServiceAccountIAM set a service account IAM binding.
func (ic *IAMClient) SetServiceAccountIAM(ctx context.Context, name string, policy *iam.Policy) error {
	email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, ic.project)
	resource_prefix := fmt.Sprintf("projects/%s/serviceAccounts/", ic.project)

	rb := &iam.SetIamPolicyRequest{
		Policy: policy,
	}

	_, err := ic.service.SetIamPolicy(ctx, resource_prefix+email, rb).Do()
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, iamClientLabel, "SetIamPolicy", name, errSetServiceAccountIAMFailed)
		return fmt.Errorf("Projects.ServiceAccounts.SetIamPolicy: %v", err)
	}
	return nil
}

// GetBucketPolicy is warp of cloud storage GetBucketPolicy
func (ic *IAMClient) GetBucketPolicy(ctx context.Context, resourcename string) (*cloudiam.Policy3, bool, error) {
	policy, exist, err := ic.cscli.GetBucketPolicy(ctx, resourcename)
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, cloudStorageClient, "GetBucketPolicy", resourcename, errGetBucketPolicyFailed)
	}
	return policy, exist, err
}

// SetBucketPolicy is warp of cloud storage SetBucketPolicy
func (ic *IAMClient) SetBucketPolicy(ctx context.Context, resourcename string, policy *cloudiam.Policy3) error {
	err := ic.cscli.SetBucketPolicy(ctx, resourcename, policy)
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, cloudStorageClient, "SetBucketPolicy", resourcename, errSetBucketPolicyFailed)
	}
	return err
}

// DeleteBucketPolicy is warp of cloud storage DeleteBucketPolicy
func (ic *IAMClient) DeleteBucketPolicy(ctx context.Context, resourcename string, title string) error {
	err := ic.cscli.DeleteBucketPolicy(ctx, resourcename, title)
	if err != nil {
		metrics.HandleGCPAPIRequestMetrics(ic.cscli.region, cloudStorageClient, "DeleteBucketPolicy", resourcename, errDeleteBucketPolicyFailed)
	}
	return err
}
