package providers

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/subscription/armsubscription"

	"github.com/Azure/draft/pkg/prompts"
	"github.com/Azure/draft/pkg/spinner"

	bo "github.com/cenkalti/backoff/v4"
	log "github.com/sirupsen/logrus"
)

type SetUpCmd struct {
	AppName           string
	SubscriptionID    string
	ResourceGroupName string
	Provider          string
	Repo              string
	appId             string
	TenantId          string
	appObjectId       string
	spObjectId        string
	AzClient          AzClientInterface
}

const CONTRIBUTOR_ROLE_ID = "b24988ac-6180-42a0-ab88-20f7382dd24c"

func InitiateAzureOIDCFlow(ctx context.Context, sc *SetUpCmd, s spinner.Spinner, gh GhClient, az AzClientInterface) error {
	log.Debug("Commencing github connection with azure...")

	s.Start()

	if err := sc.ValidateSetUpConfig(gh, az); err != nil {
		return err
	}

	if !az.AzAppExists(sc.AppName) {
		appId, err := az.CreateAzApp(sc.AppName)
		if err != nil {
			return err
		}
		sc.appId = appId
	}

	spObjId, err := az.CreateServicePrincipal(sc.appId)
	if err != nil {
		return err
	}
	sc.spObjectId = spObjId

	if err := sc.getAppObjectId(); err != nil {
		return err
	}

	if err := az.AssignSpRole(ctx, sc.SubscriptionID, sc.ResourceGroupName, sc.spObjectId, CONTRIBUTOR_ROLE_ID); err != nil {
		return err
	}

	if !sc.hasFederatedCredentials() {
		if err := sc.createFederatedCredentials(); err != nil {
			return err
		}
	}

	if err := sc.setAzClientId(); err != nil {
		return err
	}
	if err := sc.setAzSubscriptionId(); err != nil {
		return err
	}
	if err := sc.setAzTenantId(); err != nil {
		return err
	}

	log.Debug("Github connection with azure completed successfully!")
	return nil
}

// CreateAzApp creates an Azure app with the given name
// Returns the appId of the created app
func (az *AzClient) CreateAzApp(appName string) (string, error) {
	log.Debug("Commencing Azure app creation...")
	start := time.Now()
	log.Debug(start)
	createdAppId := ""

	createApp := func() error {
		out, err := az.CommandRunner.RunCommand("az", "ad", "app", "create", "--only-show-errors", "--display-name", appName)
		if err != nil {
			log.Printf("%s\n", out)
			return err
		}

		if az.AzAppExists(appName) {
			var azApp map[string]interface{}
			if err := json.Unmarshal([]byte(out), &azApp); err != nil {
				return err
			}
			createdAppId = fmt.Sprint(azApp["appId"])

			end := time.Since(start)
			log.Debugf("App with appId '%s' created successfully!", createdAppId)
			log.Debug(end)
			return nil
		}

		return errors.New("app creation time has exceeded max elapsed time for exponential backoff")
	}

	backoff := bo.NewExponentialBackOff()
	backoff.MaxElapsedTime = 5 * time.Second

	err := bo.Retry(createApp, backoff)
	if err != nil {
		log.Debug(err)
		return "", err
	}

	return createdAppId, nil
}

// CreateServicePrincipal creates a service principal with the given appId
// Returns the objectId of the created service principal
func (az *AzClient) CreateServicePrincipal(appId string) (string, error) {
	log.Debug("creating Azure service principal...")
	start := time.Now()
	log.Debug(start)

	if appId == "" {
		return "", errors.New("appId cannot be empty")
	}
	createdObjectId := ""

	createServicePrincipal := func() error {
		out, err := az.CommandRunner.RunCommand("az", "ad", "sp", "create", "--id", appId, "--only-show-errors")
		if err != nil {
			log.Printf("%s\n", out)
			return err
		}

		log.Debug("checking sp was created...")
		spObjId, err := az.GetServicePrincipal(appId)
		if err != nil {
			return errors.New("service principal not found")
		}
		log.Debug("Service principal created successfully!")
		end := time.Since(start)
		log.Debug(end)
		createdObjectId = spObjId
		return nil

	}

	backoff := bo.NewExponentialBackOff()
	backoff.MaxElapsedTime = 5 * time.Second

	err := bo.Retry(createServicePrincipal, backoff)
	if err != nil {
		log.Debug(err)
		return "", err
	}

	return createdObjectId, nil
}

// Prompt the user to select a tenant ID if there are multiple tenants, or return the only tenant ID if there is only one
func PromptTenantId(azc AzClientInterface, ctx context.Context) (string, error) {
	log.Debug("getting Azure tenant ID")

	selectedTenant := ""
	tenants, err := azc.ListTenants(ctx)
	if err != nil {
		return selectedTenant, fmt.Errorf("listing tenants: %w", err)
	}

	if len(tenants) == 0 {
		return selectedTenant, errors.New("no tenants found")
	}

	if len(tenants) == 1 {
		if tenants[0].TenantID == nil {
			return selectedTenant, errors.New("nil tenant ID")
		}
		selectedTenant = *tenants[0].TenantID
		log.Debugf("Selected only tenant ID found: %s", selectedTenant)
		return selectedTenant, nil
	}
	if len(tenants) > 1 {
		prompts.Select[armsubscription.TenantIDDescription]("Select the tenant you want to use", tenants, &prompts.SelectOpt[armsubscription.TenantIDDescription]{})
	}

	return selectedTenant, nil
}

func (sc *SetUpCmd) ValidateSetUpConfig(gh GhClient, az AzClientInterface) error {
	log.Debug("Checking that provided information is valid...")

	if err := az.IsSubscriptionIdValid(sc.SubscriptionID); err != nil {
		return err
	}

	if err := az.IsValidResourceGroup(sc.SubscriptionID, sc.ResourceGroupName); err != nil {
		return err
	}

	if sc.AppName == "" {
		return errors.New("invalid app name")
	}

	if err := gh.IsValidGhRepo(sc.Repo); err != nil {
		return err
	}

	return nil
}

func (sc *SetUpCmd) hasFederatedCredentials() bool {
	log.Debug("Checking for existing federated credentials...")
	uri := fmt.Sprintf("https://graph.microsoft.com/beta/applications/%s/federatedIdentityCredentials", sc.appObjectId)
	getFicCmd := exec.Command("az", "rest", "--method", "GET", "--uri", uri, "--query", "value")
	out, err := getFicCmd.CombinedOutput()
	if err != nil {
		log.Errorf("error getting fic: %s", err)
		return false
	}

	var fics []interface{}
	if err = json.Unmarshal(out, &fics); err != nil {
		log.Errorf("error marshaling fics: %s", err)
		return false
	}

	if len(fics) > 0 {
		log.Debug("Credentials found")
		// TODO: ask user if they want to use current credentials?
		// TODO: check if fics with the name we want exist already
		return true
	}

	log.Debug("No existing credentials found")
	return false
}

func (sc *SetUpCmd) createFederatedCredentials() error {
	log.Debug("Creating federated credentials...")
	fics := &[]string{
		`{"name":"prfic","subject":"repo:%s:pull_request","issuer":"https://token.actions.githubusercontent.com","description":"pr","audiences":["api://AzureADTokenExchange"]}`,
		`{"name":"mainfic","subject":"repo:%s:ref:refs/heads/main","issuer":"https://token.actions.githubusercontent.com","description":"main","audiences":["api://AzureADTokenExchange"]}`,
		`{"name":"masterfic","subject":"repo:%s:ref:refs/heads/master","issuer":"https://token.actions.githubusercontent.com","description":"master","audiences":["api://AzureADTokenExchange"]}`,
	}

	uri := "https://graph.microsoft.com/beta/applications/%s/federatedIdentityCredentials"

	for _, fic := range *fics {
		createFicCmd := exec.Command("az", "rest", "--method", "POST", "--uri", fmt.Sprintf(uri, sc.appObjectId), "--body", fmt.Sprintf(fic, sc.Repo))
		out, err := createFicCmd.CombinedOutput()
		if err != nil {
			log.Printf("%s\n", out)
			return err
		}

	}

	log.Debug("Waiting 10 seconds to allow credentials time to populate")
	time.Sleep(10 * time.Second)
	count := 0

	// check to make sure credentials were created
	// count to prevent infinite loop
	for count < 10 {
		if sc.hasFederatedCredentials() {
			break
		}

		log.Debug("Credentials not yet created, retrying...")
		count += 1
	}

	return nil

}

func (sc *SetUpCmd) getAppObjectId() error {
	log.Debug("Fetching Azure application object ID")

	getObjectIdCmd := exec.Command("az", "ad", "app", "show", "--only-show-errors", "--id", sc.appId, "--query", "id")
	out, err := getObjectIdCmd.CombinedOutput()
	if err != nil {
		log.Printf("%s\n", out)
		return err
	}

	var objectId string
	if err := json.Unmarshal(out, &objectId); err != nil {
		return err
	}

	sc.appObjectId = objectId

	return nil
}

func (sc *SetUpCmd) setAzClientId() error {
	log.Debug("Setting AZURE_CLIENT_ID in github...")
	setClientIdCmd := exec.Command("gh", "secret", "set", "AZURE_CLIENT_ID", "-b", sc.appId, "--repo", sc.Repo)
	out, err := setClientIdCmd.CombinedOutput()
	if err != nil {
		log.Printf("%s\n", out)
		return err
	}
	return nil
}

func (sc *SetUpCmd) setAzSubscriptionId() error {
	log.Debug("Setting AZURE_SUBSCRIPTION_ID in github...")
	setSubscriptionIdCmd := exec.Command("gh", "secret", "set", "AZURE_SUBSCRIPTION_ID", "-b", sc.SubscriptionID, "--repo", sc.Repo)
	out, err := setSubscriptionIdCmd.CombinedOutput()
	if err != nil {
		log.Printf("%s\n", out)
		return err
	}
	return nil
}

func (sc *SetUpCmd) setAzTenantId() error {
	log.Debug("Setting AZURE_TENANT_ID in github...")
	setTenantIdCmd := exec.Command("gh", "secret", "set", "AZURE_TENANT_ID", "-b", sc.TenantId, "--repo", sc.Repo)
	out, err := setTenantIdCmd.CombinedOutput()
	if err != nil {
		log.Printf("%s\n", out)
		return err
	}
	return err
}
