package aws

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"

	"github.com/BishopFox/cloudfox/aws/sdk"
	"github.com/BishopFox/cloudfox/internal"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/efs"
	"github.com/aws/aws-sdk-go-v2/service/efs/types"
	"github.com/aws/aws-sdk-go-v2/service/fsx"
	fsxTypes "github.com/aws/aws-sdk-go-v2/service/fsx/types"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	"github.com/bishopfox/awsservicemap"
	"github.com/sirupsen/logrus"
)

type FilesystemsModule struct {
	// General configuration data

	EFSClient *efs.Client
	FSxClient *fsx.Client

	Caller        sts.GetCallerIdentityOutput
	AWSRegions    []string
	AWSOutputType string
	AWSTableCols  string

	Goroutines int
	AWSProfile string
	WrapTable  bool

	// Main module data
	Filesystems []FilesystemObject

	Regions        [30]FilesystemObject
	CommandCounter internal.CommandCounter
	// Used to store output data for pretty printing
	output internal.OutputData2
	modLog *logrus.Entry
}

type FilesystemObject struct {
	AWSService  string
	Region      string
	Name        string
	DnsName     string
	IP          string
	Policy      string
	MountTarget string
	Permissions string
}

func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity int) {
	// These struct values are used by the output module
	m.output.Verbosity = verbosity
	m.output.Directory = outputDirectory
	m.output.CallingModule = "filesystems"
	m.modLog = internal.TxtLog.WithFields(logrus.Fields{
		"module": m.output.CallingModule,
	})
	if m.AWSProfile == "" {
		m.AWSProfile = internal.BuildAWSPath(m.Caller)
	}

	fmt.Printf("[%s][%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
	fmt.Printf("[%s][%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))

	wg := new(sync.WaitGroup)
	semaphore := make(chan struct{}, m.Goroutines)

	// Create a channel to signal the spinner aka task status goroutine to finish
	spinnerDone := make(chan bool)
	//fire up the the task status spinner/updated
	go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")

	//create a channel to receive the objects
	dataReceiver := make(chan FilesystemObject)

	// Create a channel to signal to stop
	receiverDone := make(chan bool)

	go m.Receiver(dataReceiver, receiverDone)

	//execute regional checks

	for _, region := range m.AWSRegions {
		wg.Add(1)
		m.executeChecks(region, wg, semaphore, dataReceiver)
	}

	wg.Wait()
	//time.Sleep(time.Second * 2)

	// Send a message to the spinner goroutine to close the channel and stop
	spinnerDone <- true
	<-spinnerDone
	receiverDone <- true
	<-receiverDone

	sort.Slice(m.Filesystems, func(i, j int) bool {
		return m.Filesystems[i].AWSService < m.Filesystems[j].AWSService
	})

	m.output.Headers = []string{
		"Account",
		"Region",
		"Name",
		"DNS Name",
		//"IP",
		"Mount Target",
		"Policy",
		"Permissions",
	}

	// If the user specified table columns, use those.
	// If the user specified -o wide, use the wide default cols for this module.
	// Otherwise, use the hardcoded default cols for this module.
	var tableCols []string
	// If the user specified table columns, use those.
	if m.AWSTableCols != "" {
		// If the user specified wide as the output format, use these columns.
		// remove any spaces between any commas and the first letter after the commas
		m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
		m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ",  ", ",")
		tableCols = strings.Split(m.AWSTableCols, ",")
		// If the user specified wide as the output format, use these columns.
	} else if m.AWSOutputType == "wide" {
		tableCols = []string{
			"Account",
			"Region",
			"Name",
			"DNS Name",
			//"IP",
			"Mount Target",
			"Policy",
			"Permissions",
		}
	} else {
		tableCols = []string{
			"Region",
			"Name",
			"DNS Name",
			//"IP",
			"Mount Target",
			"Policy",
			"Permissions",
		}

	}

	// Table rows
	for i := range m.Filesystems {
		m.output.Body = append(
			m.output.Body,
			[]string{
				aws.ToString(m.Caller.Account),
				m.Filesystems[i].Region,
				m.Filesystems[i].Name,
				m.Filesystems[i].DnsName,
				//m.Filesystems[i].IP,
				m.Filesystems[i].MountTarget,
				m.Filesystems[i].Policy,
				m.Filesystems[i].Permissions,
			},
		)

	}
	if len(m.output.Body) > 0 {

		m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))

		o := internal.OutputClient{
			Verbosity:     verbosity,
			CallingModule: m.output.CallingModule,
			Table: internal.TableClient{
				Wrap: m.WrapTable,
			},
		}
		o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
			Header:    m.output.Headers,
			Body:      m.output.Body,
			TableCols: tableCols,
			Name:      m.output.CallingModule,
		})
		o.PrefixIdentifier = m.AWSProfile
		o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
		o.WriteFullOutput(o.Table.TableFiles, nil)
		m.writeLoot(o.Table.DirectoryName, verbosity)
		fmt.Printf("[%s][%s] %s filesystems found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))

	} else {
		fmt.Printf("[%s][%s] No filesystems found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
	}
	fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
}

func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDone chan bool) {

	defer close(receiverDone)
	for {
		select {
		case data := <-receiver:
			m.Filesystems = append(m.Filesystems, data)
		case <-receiverDone:
			receiverDone <- true
			return
		}
	}
}

func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
	defer wg.Done()
	servicemap := &awsservicemap.AwsServiceMap{
		JsonFileSource: "DOWNLOAD_FROM_AWS",
	}
	res, err := servicemap.IsServiceInRegion("efs", r)
	if err != nil {
		m.modLog.Error(err)
	}
	if res {
		wg.Add(1)
		go m.getEFSSharesPerRegion(r, wg, semaphore, dataReceiver)
	}
	//each fsx type has different supported regions so easier to just run this function against all enabled regions.
	wg.Add(1)
	go m.getFSxSharesPerRegion(r, wg, semaphore, dataReceiver)
}

func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
	path := filepath.Join(outputDirectory, "loot")
	err := os.MkdirAll(path, os.ModePerm)
	if err != nil {
		sharedLogger.Error(err.Error())

		m.CommandCounter.Error++
	}
	f := filepath.Join(path, "filesystems-mount-commands.txt")

	var out string

	for i := range m.Filesystems {
		switch m.Filesystems[i].AWSService {
		case "EFS":
			out = out + fmt.Sprintf("##########  Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
			out = out + fmt.Sprintf("mkdir -p /efs%s/\n", m.Filesystems[i].MountTarget)
			out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
		case "FSx [LUSTRE]":
			out = out + fmt.Sprintf("##########  Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
			out = out + fmt.Sprintln("#sudo amazon-linux-extras install -y lustre2.10")
			out = out + fmt.Sprintf("mkdir -p /fsx-lustre/%s/\n", m.Filesystems[i].MountTarget)
			out = out + fmt.Sprintf("sudo mount -t lustre -o noatime,flock %s@tcp:/%s /fsx-lustre/%s/\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget, m.Filesystems[i].MountTarget)
		case "FSx [OPENZFS]":
			out = out + fmt.Sprintf("##########  Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
			out = out + fmt.Sprintf("mkdir -p /fsx-openzfs%s\n", m.Filesystems[i].MountTarget)
			out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1 %s:%s /fsx-openzfs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget, m.Filesystems[i].MountTarget)
		case "FSx [ONTAP]":
			out = out + fmt.Sprintf("##########  Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
			out = out + fmt.Sprintf("mkdir -p /fsx-ontap%s\n", m.Filesystems[i].MountTarget)
			out = out + fmt.Sprintf("sudo mount -t nfs %s:%s /fsx-ontap%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget, m.Filesystems[i].MountTarget)
		case "FSx [WINDOWS]":
			out = out + fmt.Sprintf("##########  Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
			out = out + fmt.Sprintf("crackmapexec smb %s --shares \n", m.Filesystems[i].DnsName)
			out = out + fmt.Sprintf("# mkdir -p /fsx-windows/%s/SHARE-NAME\n", m.Filesystems[i].DnsName)
			out = out + fmt.Sprintf("sudo mount -t cifs //%s/SHARE-NAME /fsx-windows/%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].DnsName)
		}

	}

	err = os.WriteFile(f, []byte(out), 0644)
	if err != nil {
		sharedLogger.Error(err.Error())

		m.CommandCounter.Error++
	}
	fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
	if verbosity > 2 {
		fmt.Println()
		fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to try and mount the identified filesystems."))
		fmt.Print(out)
		fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
	}

}

func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
	defer func() {
		m.CommandCounter.Executing--
		m.CommandCounter.Complete++
		wg.Done()

	}()
	m.CommandCounter.Total++
	m.CommandCounter.Pending--
	m.CommandCounter.Executing++
	var policy string

	DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
	if err != nil {
		sharedLogger.Error(err.Error())
		m.CommandCounter.Error++
		return
	}

	for _, filesystem := range DescribeFileSystems {
		name := filesystem.Name
		id := aws.ToString(filesystem.FileSystemId)

		_, err := m.EFSClient.DescribeFileSystemPolicy(
			context.TODO(),
			&efs.DescribeFileSystemPolicyInput{
				FileSystemId: filesystem.FileSystemId,
			},
			func(o *efs.Options) {
				o.Region = r
			},
		)
		if err != nil {
			policy = "Default (No IAM auth)"
		} else {
			policy = "IAM Authenticated"
		}

		DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
		if err != nil {
			sharedLogger.Error(err.Error())
			m.CommandCounter.Error++
			break
		}

		for _, mountTarget := range DescribeMountTargets {
			ip := *mountTarget.IpAddress
			mountTarget := aws.ToString(mountTarget.MountTargetId)
			awsService := "EFS"

			accessPoints, err := sdk.CachedDescribeAccessPoints(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
			if err != nil {
				sharedLogger.Error(err.Error())

				m.CommandCounter.Error++
				dataReceiver <- FilesystemObject{
					AWSService:  awsService,
					Region:      r,
					Name:        aws.ToString(name),
					DnsName:     ip,
					Policy:      policy,
					MountTarget: mountTarget,
					Permissions: "access denied",
				}
			}

			for _, accessPoint := range accessPoints {
				path, permissions := m.getEFSfilesystemPermissions(accessPoint)
				dataReceiver <- FilesystemObject{
					AWSService:  awsService,
					Region:      r,
					Name:        aws.ToString(name),
					DnsName:     ip,
					Policy:      policy,
					MountTarget: path,
					Permissions: permissions,
				}

			}
		}
	}

}

func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
	defer func() {
		m.CommandCounter.Executing--
		m.CommandCounter.Complete++
		wg.Done()

	}()
	m.CommandCounter.Total++
	m.CommandCounter.Pending--
	m.CommandCounter.Executing++
	// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
	var PaginationMarker *string
	var PaginationMarker2 *string
	var mountTargetId *string
	var name string
	var dnsName string

	// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
	for {
		DescribeFileSystems, err := m.FSxClient.DescribeFileSystems(
			context.TODO(),
			&fsx.DescribeFileSystemsInput{
				NextToken: PaginationMarker,
			},
			func(o *fsx.Options) {
				o.Region = r
			},
		)
		if err != nil {
			sharedLogger.Error(err.Error())

			m.CommandCounter.Error++
			break
		}

		for _, filesystem := range DescribeFileSystems.FileSystems {

			// The name is in a tag so we have to do this to grab the value from the right tag
			for _, tag := range filesystem.Tags {
				if *tag.Key == "Name" {
					name = aws.ToString(tag.Value)
				}

			}

			fsType := filesystem.FileSystemType
			id := *filesystem.FileSystemId
			awsService := fmt.Sprintf("FSx [%s]", fsType)

			// For Lustre and windows we get everything we need from the filesystem call.  For the other two we need to get volume info
			switch fsType {
			case "LUSTRE":
				mountTargetId = filesystem.LustreConfiguration.MountName
				dnsName = aws.ToString(filesystem.DNSName)
				dataReceiver <- FilesystemObject{
					AWSService:  awsService,
					Region:      r,
					Name:        name,
					DnsName:     dnsName,
					Policy:      "",
					MountTarget: aws.ToString(mountTargetId),
				}
			case "WINDOWS":
				//mountTargetId = filesystem.WindowsConfiguration.
				dnsName = aws.ToString(filesystem.WindowsConfiguration.PreferredFileServerIp)
				//dnsName = *&filesystem.WindowsConfiguration.PreferredFileServerIp
				dataReceiver <- FilesystemObject{
					AWSService:  awsService,
					Region:      r,
					Name:        name,
					DnsName:     dnsName,
					Policy:      "",
					MountTarget: "",
				}
			}

			// For OpenZFS and ONTAP, we need to get volume specific info
			for {
				DescribeVolumes, err := m.FSxClient.DescribeVolumes(
					context.TODO(),
					&fsx.DescribeVolumesInput{
						Filters: []fsxTypes.VolumeFilter{
							{
								Name:   "file-system-id",
								Values: []string{id},
							}},
						NextToken: PaginationMarker2,
					},
					func(o *fsx.Options) {
						o.Region = r
					},
				)
				if err != nil {
					break
				}
				//				awsService := fmt.Sprintf("FSx [%s]", fsType)
				for _, volume := range DescribeVolumes.Volumes {

					switch fsType {
					case "OPENZFS":
						mountTargetId = volume.OpenZFSConfiguration.VolumePath
						dnsName = aws.ToString(filesystem.DNSName)
						dataReceiver <- FilesystemObject{
							AWSService:  awsService,
							Region:      r,
							Name:        name,
							DnsName:     dnsName,
							Policy:      "",
							MountTarget: aws.ToString(mountTargetId),
						}
					case "ONTAP":
						mountTargetId = volume.OntapConfiguration.JunctionPath
						dnsName = fmt.Sprintf("%s.%s.fsx.%s.amazonaws.com", aws.ToString(volume.OntapConfiguration.StorageVirtualMachineId), aws.ToString(volume.FileSystemId), r)
						dataReceiver <- FilesystemObject{
							AWSService:  awsService,
							Region:      r,
							Name:        name,
							DnsName:     dnsName,
							Policy:      "",
							MountTarget: aws.ToString(mountTargetId),
						}

					}

				}
				if DescribeVolumes.NextToken != nil {
					PaginationMarker2 = DescribeVolumes.NextToken
				} else {
					PaginationMarker2 = nil
					break
				}
			}

		}

		// Pagination control. After the last page of output, the for loop exits.
		if DescribeFileSystems.NextToken != nil {
			PaginationMarker = DescribeFileSystems.NextToken
		} else {
			PaginationMarker = nil
			break
		}
	}

}

func (m *FilesystemsModule) getEFSfilesystemPermissions(accessPoint types.AccessPointDescription) (string, string) {
	var path string
	var permissions string

	if accessPoint.AccessPointId != nil && accessPoint.RootDirectory != nil && accessPoint.RootDirectory.CreationInfo != nil {
		path = aws.ToString(accessPoint.RootDirectory.Path)
		permissions = aws.ToString(accessPoint.RootDirectory.CreationInfo.Permissions)
		return path, permissions
	}
	return "", ""

}
