package google

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/hashicorp/terraform/helper/logging"
	"github.com/hashicorp/terraform/helper/pathorcontents"
	"github.com/hashicorp/terraform/httpclient"
	"github.com/terraform-providers/terraform-provider-google/version"

	"golang.org/x/oauth2"
	googleoauth "golang.org/x/oauth2/google"
	appengine "google.golang.org/api/appengine/v1"
	"google.golang.org/api/bigquery/v2"
	"google.golang.org/api/cloudbilling/v1"
	"google.golang.org/api/cloudbuild/v1"
	"google.golang.org/api/cloudfunctions/v1"
	"google.golang.org/api/cloudiot/v1"
	"google.golang.org/api/cloudkms/v1"
	"google.golang.org/api/cloudresourcemanager/v1"
	resourceManagerV2Beta1 "google.golang.org/api/cloudresourcemanager/v2beta1"
	composer "google.golang.org/api/composer/v1beta1"
	computeBeta "google.golang.org/api/compute/v0.beta"
	"google.golang.org/api/compute/v1"
	"google.golang.org/api/container/v1"
	containerBeta "google.golang.org/api/container/v1beta1"
	dataflow "google.golang.org/api/dataflow/v1b3"
	"google.golang.org/api/dataproc/v1"
	"google.golang.org/api/dns/v1"
	dnsBeta "google.golang.org/api/dns/v1beta2"
	file "google.golang.org/api/file/v1beta1"
	"google.golang.org/api/iam/v1"
	iamcredentials "google.golang.org/api/iamcredentials/v1"
	cloudlogging "google.golang.org/api/logging/v2"
	"google.golang.org/api/option"
	"google.golang.org/api/pubsub/v1"
	runtimeconfig "google.golang.org/api/runtimeconfig/v1beta1"
	"google.golang.org/api/servicemanagement/v1"
	"google.golang.org/api/serviceusage/v1"
	"google.golang.org/api/sourcerepo/v1"
	"google.golang.org/api/spanner/v1"
	sqladmin "google.golang.org/api/sqladmin/v1beta4"
	"google.golang.org/api/storage/v1"
	"google.golang.org/api/storagetransfer/v1"
)

// Config is the configuration structure used to instantiate the Google
// provider.
type Config struct {
	Credentials string
	AccessToken string
	Project     string
	Region      string
	Zone        string
	Scopes      []string

	client    *http.Client
	userAgent string

	tokenSource oauth2.TokenSource

	clientBilling                *cloudbilling.APIService
	clientBuild                  *cloudbuild.Service
	clientComposer               *composer.Service
	clientCompute                *compute.Service
	clientComputeBeta            *computeBeta.Service
	clientContainer              *container.Service
	clientContainerBeta          *containerBeta.Service
	clientDataproc               *dataproc.Service
	clientDataflow               *dataflow.Service
	clientDns                    *dns.Service
	clientDnsBeta                *dnsBeta.Service
	clientFilestore              *file.Service
	clientIamCredentials         *iamcredentials.Service
	clientKms                    *cloudkms.Service
	clientLogging                *cloudlogging.Service
	clientPubsub                 *pubsub.Service
	clientResourceManager        *cloudresourcemanager.Service
	clientResourceManagerV2Beta1 *resourceManagerV2Beta1.Service
	clientRuntimeconfig          *runtimeconfig.Service
	clientSpanner                *spanner.Service
	clientSourceRepo             *sourcerepo.Service
	clientStorage                *storage.Service
	clientSqlAdmin               *sqladmin.Service
	clientIAM                    *iam.Service
	clientServiceMan             *servicemanagement.APIService
	clientServiceUsage           *serviceusage.Service
	clientBigQuery               *bigquery.Service
	clientCloudFunctions         *cloudfunctions.Service
	clientCloudIoT               *cloudiot.Service
	clientAppEngine              *appengine.APIService
	clientStorageTransfer        *storagetransfer.Service

	bigtableClientFactory *BigtableClientFactory
}

var defaultClientScopes = []string{
	"https://www.googleapis.com/auth/compute",
	"https://www.googleapis.com/auth/cloud-platform",
	"https://www.googleapis.com/auth/ndev.clouddns.readwrite",
	"https://www.googleapis.com/auth/devstorage.full_control",
}

func (c *Config) LoadAndValidate() error {
	if len(c.Scopes) == 0 {
		c.Scopes = defaultClientScopes
	}

	tokenSource, err := c.getTokenSource(c.Scopes)
	if err != nil {
		return err
	}
	c.tokenSource = tokenSource

	client := oauth2.NewClient(context.Background(), tokenSource)
	client.Transport = logging.NewTransport("Google", client.Transport)
	// Each individual request should return within 30s - timeouts will be retried.
	// This is a timeout for, e.g. a single GET request of an operation - not a
	// timeout for the maximum amount of time a logical request can take.
	client.Timeout, _ = time.ParseDuration("30s")

	terraformVersion := httpclient.UserAgentString()
	providerVersion := fmt.Sprintf("terraform-provider-google/%s", version.ProviderVersion)
	terraformWebsite := "(+https://www.terraform.io)"
	userAgent := fmt.Sprintf("%s %s %s", terraformVersion, terraformWebsite, providerVersion)

	c.client = client
	c.userAgent = userAgent

	context := context.Background()

	log.Printf("[INFO] Instantiating GCE client...")
	c.clientCompute, err = compute.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientCompute.UserAgent = userAgent

	log.Printf("[INFO] Instantiating GCE Beta client...")
	c.clientComputeBeta, err = computeBeta.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientComputeBeta.UserAgent = userAgent

	log.Printf("[INFO] Instantiating GKE client...")
	c.clientContainer, err = container.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientContainer.UserAgent = userAgent

	log.Printf("[INFO] Instantiating GKE Beta client...")
	c.clientContainerBeta, err = containerBeta.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientContainerBeta.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud DNS client...")
	c.clientDns, err = dns.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientDns.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud DNS Beta client...")
	c.clientDnsBeta, err = dnsBeta.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientDnsBeta.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud KMS Client...")
	c.clientKms, err = cloudkms.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientKms.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Stackdriver Logging client...")
	c.clientLogging, err = cloudlogging.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientLogging.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Storage Client...")
	c.clientStorage, err = storage.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientStorage.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google SqlAdmin Client...")
	c.clientSqlAdmin, err = sqladmin.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientSqlAdmin.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Pubsub Client...")
	c.clientPubsub, err = pubsub.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientPubsub.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Dataflow Client...")
	c.clientDataflow, err = dataflow.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientDataflow.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud ResourceManager Client...")
	c.clientResourceManager, err = cloudresourcemanager.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientResourceManager.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud ResourceManager V Client...")
	c.clientResourceManagerV2Beta1, err = resourceManagerV2Beta1.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientResourceManagerV2Beta1.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Runtimeconfig Client...")
	c.clientRuntimeconfig, err = runtimeconfig.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientRuntimeconfig.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud IAM Client...")
	c.clientIAM, err = iam.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientIAM.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud IAMCredentials Client...")
	c.clientIamCredentials, err = iamcredentials.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientIamCredentials.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Service Management Client...")
	c.clientServiceMan, err = servicemanagement.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientServiceMan.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Service Usage Client...")
	c.clientServiceUsage, err = serviceusage.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientServiceUsage.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Billing Client...")
	c.clientBilling, err = cloudbilling.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientBilling.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Build Client...")
	c.clientBuild, err = cloudbuild.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientBuild.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud BigQuery Client...")
	c.clientBigQuery, err = bigquery.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientBigQuery.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud CloudFunctions Client...")
	c.clientCloudFunctions, err = cloudfunctions.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientCloudFunctions.UserAgent = userAgent

	c.bigtableClientFactory = &BigtableClientFactory{
		UserAgent:   userAgent,
		TokenSource: tokenSource,
	}

	log.Printf("[INFO] Instantiating Google Cloud Source Repo Client...")
	c.clientSourceRepo, err = sourcerepo.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientSourceRepo.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Spanner Client...")
	c.clientSpanner, err = spanner.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientSpanner.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Dataproc Client...")
	c.clientDataproc, err = dataproc.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientDataproc.UserAgent = userAgent

	c.clientFilestore, err = file.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientFilestore.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud IoT Core Client...")
	c.clientCloudIoT, err = cloudiot.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientCloudIoT.UserAgent = userAgent

	log.Printf("[INFO] Instantiating App Engine Client...")
	c.clientAppEngine, err = appengine.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientAppEngine.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Cloud Composer Client...")
	c.clientComposer, err = composer.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientComposer.UserAgent = userAgent

	log.Printf("[INFO] Instantiating Google Cloud Storage Transfer Client...")
	c.clientStorageTransfer, err = storagetransfer.NewService(context, option.WithHTTPClient(client))
	if err != nil {
		return err
	}
	c.clientStorageTransfer.UserAgent = userAgent

	return nil
}

func (c *Config) getTokenSource(clientScopes []string) (oauth2.TokenSource, error) {
	if c.AccessToken != "" {
		contents, _, err := pathorcontents.Read(c.AccessToken)
		if err != nil {
			return nil, fmt.Errorf("Error loading access token: %s", err)
		}

		log.Printf("[INFO] Authenticating using configured Google JSON 'access_token'...")
		log.Printf("[INFO]   -- Scopes: %s", clientScopes)
		token := &oauth2.Token{AccessToken: contents}
		return oauth2.StaticTokenSource(token), nil
	}

	if c.Credentials != "" {
		contents, _, err := pathorcontents.Read(c.Credentials)
		if err != nil {
			return nil, fmt.Errorf("Error loading credentials: %s", err)
		}

		creds, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(contents), clientScopes...)
		if err != nil {
			return nil, fmt.Errorf("Unable to parse credentials from '%s': %s", contents, err)
		}

		log.Printf("[INFO] Authenticating using configured Google JSON 'credentials'...")
		log.Printf("[INFO]   -- Scopes: %s", clientScopes)
		return creds.TokenSource, nil
	}

	log.Printf("[INFO] Authenticating using DefaultClient...")
	log.Printf("[INFO]   -- Scopes: %s", clientScopes)
	return googleoauth.DefaultTokenSource(context.Background(), clientScopes...)
}
