package aws

import (
	"context"
	"fmt"
	"os"
	"strings"

	ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester"
	"github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models"
	"github.com/BishopFox/cloudfox/aws/sdk"
	"github.com/BishopFox/cloudfox/internal"
	"github.com/BishopFox/cloudfox/internal/aws/policy"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	"github.com/bishopfox/knownawsaccountslookup"
	"github.com/sirupsen/logrus"
)

type GraphCommand struct {

	// General configuration data
	Caller              sts.GetCallerIdentityOutput
	AWSRegions          []string
	Goroutines          int
	AWSProfile          string
	WrapTable           bool
	AWSOutputType       string
	AWSTableCols        string
	Verbosity           int
	AWSOutputDirectory  string
	AWSConfig           aws.Config
	Version             string
	SkipAdminCheck      bool
	PmapperDataBasePath string

	pmapperMod   PmapperModule
	pmapperError error

	vendors *knownawsaccountslookup.Vendors

	// Main module data
	// Used to store output data for pretty printing
	output internal.OutputData2

	modLog *logrus.Entry
}

func (m *GraphCommand) RunGraphCommand() {

	// These struct values are used by the output module
	m.output.Verbosity = m.Verbosity
	m.output.Directory = m.AWSOutputDirectory
	m.output.CallingModule = "graph"
	m.modLog = internal.TxtLog.WithFields(logrus.Fields{
		"module": m.output.CallingModule,
	})
	if m.AWSProfile == "" {
		m.AWSProfile = internal.BuildAWSPath(m.Caller)
	}

	m.vendors = knownawsaccountslookup.NewVendorMap()
	m.vendors.PopulateKnownAWSAccounts()

	m.modLog.Info("Collecting data for graph ingestor...")

	m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)

	////////////////
	// Accounts
	////////////////

	accounts := m.collectAccountDataForGraph()
	// write data to jsonl file for ingestor to read
	fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts")
	// create file and directory if it doesn't exist
	if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
		m.modLog.Error(err)
		return
	}

	outputFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		m.modLog.Error(err)
		return
	}
	defer outputFile.Close()

	for _, account := range accounts {
		if err := internal.WriteJsonlFile(outputFile, account); err != nil {
			m.modLog.Error(err)
			return
		}
	}

	////////////////
	// Users
	////////////////

	// users := m.collectUserDataForGraph()
	// // write data to jsonl file for ingestor to read
	// fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users")
	// // create file and directory if it doesn't exist
	// if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
	// 	m.modLog.Error(err)
	// 	return
	// }

	// outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	// if err != nil {
	// 	m.modLog.Error(err)
	// 	return
	// }
	// defer outputFile.Close()

	// for _, user := range users {
	// 	if err := internal.WriteJsonlFile(outputFile, user); err != nil {
	// 		m.modLog.Error(err)
	// 		return
	// 	}
	// }

	////////////////
	// Roles
	////////////////

	roles := m.collectRoleDataForGraph()
	// write data to jsonl file for ingestor to read
	fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles")
	// create file and directory if it doesn't exist
	if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil {
		m.modLog.Error(err)
		return
	}

	outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		m.modLog.Error(err)
		return
	}
	defer outputFile.Close()

	for _, role := range roles {
		if err := internal.WriteJsonlFile(outputFile, role); err != nil {
			m.modLog.Error(err)
			return
		}
	}

	ingestor, err := ingestor.NewCloudFoxIngestor()
	if err != nil {
		return
	}

	// Pmapper hack

	goCtx := context.Background()
	sharedLogger.Infof("Verifying connectivity to Neo4J at %s", ingestor.Uri)
	if err := ingestor.Driver.VerifyConnectivity(goCtx); err != nil {
		sharedLogger.Error(err)
	}
	defer ingestor.Driver.Close(goCtx)
	m.pmapperMod.GenerateCypherStatements(goCtx, ingestor.Driver)

	// back to regular stuff

	ingestor.Run(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)))

}

func (m *GraphCommand) collectAccountDataForGraph() []models.Account {
	//OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
	var accounts []models.Account
	OrganizationsCommandClient := InitOrgClient(m.AWSConfig)
	DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account))
	if err != nil {
		m.modLog.Fatal(err)
	}
	if DescribeOrgOutput.MasterAccountId == nil {
		m.modLog.Error("Organization is not configured")
	}
	// If the account is not the org mgmt account, it can only see some info about itself and some info about the org mgmt account.
	// populate both of them here.
	if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) {

		//create new object of type models.Account for this account
		thisAccount := models.Account{
			Id:  aws.ToString(m.Caller.Account),
			Arn: fmt.Sprintf("arn:aws:iam::%s:root", aws.ToString(m.Caller.Account)),
			//Name:             aws.ToString(m.Caller.Account),
			IsOrgMgmt:        false,
			IsChildAccount:   true,
			OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
			OrganizationID:   aws.ToString(DescribeOrgOutput.Id),
		}
		accounts = append(accounts, thisAccount)

		//create new object of type models.Account for the mgmt account
		mgmtAccount := models.Account{
			Id:    aws.ToString(DescribeOrgOutput.MasterAccountId),
			Arn:   aws.ToString(DescribeOrgOutput.MasterAccountArn),
			Email: aws.ToString(DescribeOrgOutput.MasterAccountEmail),
			//Status:           string(account.Status),
			//JoinedMethod:     string(account.JoinedMethod),
			//JoinedTimestamp:  account.JoinedTimestamp.String(),
			IsOrgMgmt:        true,
			IsChildAccount:   false,
			OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
			OrganizationID:   aws.ToString(DescribeOrgOutput.Id),
		}
		accounts = append(accounts, mgmtAccount)
		return accounts

	} else {
		// In this case we are the org mgmt account, so we can see all the accounts in the org.
		ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId))
		if err != nil {
			m.modLog.Fatal(err)
		}
		for _, account := range ListAccounts {
			var isMgmtAccount bool
			var isChildAccount bool
			m.modLog.Info("Account: ", aws.ToString(account.Name))
			if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(account.Id) {
				// this is the org mgmt account
				isMgmtAccount = true
				isChildAccount = false
			} else if DescribeOrgOutput.MasterAccountId == nil {
				// this is a standalone account
				isChildAccount = false
				isMgmtAccount = false
			} else {
				// this is a child account
				isChildAccount = true
				isMgmtAccount = false
			}

			//create new object of type models.Account
			account := models.Account{
				Id:               aws.ToString(account.Id),
				Arn:              aws.ToString(account.Arn),
				Name:             aws.ToString(account.Name),
				Email:            aws.ToString(account.Email),
				Status:           string(account.Status),
				JoinedMethod:     string(account.JoinedMethod),
				JoinedTimestamp:  account.JoinedTimestamp.String(),
				IsOrgMgmt:        isMgmtAccount,
				IsChildAccount:   isChildAccount,
				OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId),
				OrganizationID:   aws.ToString(DescribeOrgOutput.Id),
			}
			accounts = append(accounts, account)
		}
		return accounts
	}
}

func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
	var isAdmin, canPrivEscToAdmin string

	// iamClient := InitIAMClient(m.AWSConfig)
	// iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines)
	// localAdminMap := make(map[string]bool)

	var roles []models.Role
	IAMCommandClient := InitIAMClient(m.AWSConfig)
	ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, aws.ToString(m.Caller.Account))
	if err != nil {
		m.modLog.Error(err)
	}

	for _, role := range ListRolesOutput {
		accountId := strings.Split(aws.ToString(role.Arn), ":")[4]
		trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
		if err != nil {
			m.modLog.Error(err.Error())
			break
		}

		// if m.pmapperError == nil {
		// 	isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn)
		// } else {
		// 	isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap)
		// }

		// for _, row := range m.PermissionRowsFromAllProfiles {
		// 	if row.Arn == aws.ToString(role.Arn) {

		// 		// look for cases where there is a permission that allows sts:assumeRole or *
		// 		// lowercase the action and compare it against the list of checks below
		// 		if strings.EqualFold(row.Action, "sts:AssumeRole") ||
		// 			strings.EqualFold(row.Action, "*") ||
		// 			strings.EqualFold(row.Action, "sts:Assume*") ||
		// 			strings.EqualFold(row.Action, "sts:*") {

		// 			if row.Effect == "Allow" {
		// 				//PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
		// 				if row.Resource == "*" {
		// 					PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
		// 				} else if strings.EqualFold(row.Resource, aws.ToString(role.Arn)) {
		// 					PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole")
		// 				}
		// 			}
		// 		}
		// 		if row.Effect == "Deny" {
		// 			// Remove the string sts:AssumeRole from the PrivEscPermissions slice
		// 			for i, v := range PrivEscPermissions {
		// 				if v == "sts:AssumeRole" {
		// 					PrivEscPermissions = append(PrivEscPermissions[:i], PrivEscPermissions[i+1:]...)
		// 				}
		// 			}
		// 		}
		// 	}
		// }

		var TrustedPrincipals []models.TrustedPrincipal
		var TrustedServices []models.TrustedService
		var TrustedFederatedProviders []models.TrustedFederatedProvider
		//var TrustedFederatedSubjects string
		var trustedProvider string
		var trustedSubjects string
		var vendorName string

		for _, statement := range trustsdoc.Statement {
			for _, principal := range statement.Principal.AWS {
				if strings.Contains(principal, ":root") {
					//check to see if the accountID is known
					accountID := strings.Split(principal, ":")[4]
					vendorName = m.vendors.GetVendorNameFromAccountID(accountID)
				}

				TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
					TrustedPrincipal: principal,
					ExternalID:       strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
					VendorName:       vendorName,
					//IsAdmin:           false,
					//CanPrivEscToAdmin: false,
				})

			}
			for _, service := range statement.Principal.Service {
				TrustedServices = append(TrustedServices, models.TrustedService{
					TrustedService: service,
					AccountID:      accountId,
					//IsAdmin:           false,
					//CanPrivEscToAdmin: false,
				})

			}
			for _, federated := range statement.Principal.Federated {
				if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
					trustedProvider = "GitHub"
					trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",")
					if trustedSubjects == "" {
						trustedSubjects = "ALL REPOS!!!"
					} else {
						trustedSubjects = "Repos: " + trustedSubjects
					}

				} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
					if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
						trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
					} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
						trustedProvider = "Okta" //  (" + statement.Principal.Federated[0] + ")"
					}
					trustedSubjects = "Not applicable"
				} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil {
					trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
					// if statement.Condition.StringEquals.OidcEksSub != "" {
					// 	trustedSubjects = statement.Condition.StringEquals.OidcEksSub
					// } else if statement.Condition.StringLike.OidcEksSub != "" {
					// 	trustedSubjects = statement.Condition.StringLike.OidcEksSub
					// } else {
					// 	trustedSubjects = "ALL SERVICE ACCOUNTS!"
					// }
					trustedSubjects = "ALL SERVICE ACCOUNTS!"
				} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
					trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")"
					if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
						trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR
					}
				} else {
					if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
						trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")"
						trustedSubjects = "ALL SERVICE ACCOUNTS!"
					} else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
						trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
					}
					trustedSubjects = "Not applicable"
				}

				TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{
					TrustedFederatedProvider: federated,
					ProviderShortName:        trustedProvider,
					TrustedSubjects:          trustedSubjects,
					//IsAdmin:                  false,
					//CanPrivEscToAdmin:        false,
				})
			}
		}

		//create new object of type models.Role
		role := models.Role{
			Id:                        aws.ToString(role.Arn),
			AccountID:                 accountId,
			ARN:                       aws.ToString(role.Arn),
			Name:                      aws.ToString(role.RoleName),
			TrustsDoc:                 trustsdoc,
			TrustedPrincipals:         TrustedPrincipals,
			TrustedServices:           TrustedServices,
			TrustedFederatedProviders: TrustedFederatedProviders,
			CanPrivEscToAdmin:         canPrivEscToAdmin,
			IsAdmin:                   isAdmin,
		}
		roles = append(roles, role)
	}
	return roles
}
