package parser

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"path"
	"strings"

	"github.com/zclconf/go-cty/cty"

	"github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/resolvers"
	"github.com/aquasecurity/trivy/pkg/iac/terraform"
	"github.com/aquasecurity/trivy/pkg/log"
)

type ModuleDefinition struct {
	Name       string
	Path       string
	FileSystem fs.FS
	Definition *terraform.Block
	Parser     *Parser
	External   bool
}

func (d *ModuleDefinition) inputVars() map[string]cty.Value {
	inputs := d.Definition.NullableValues().AsValueMap()
	if inputs == nil {
		return make(map[string]cty.Value)
	}
	return inputs
}

// loadModules reads all module blocks and loads them
func (e *evaluator) loadModules(ctx context.Context) []*ModuleDefinition {
	var moduleDefinitions []*ModuleDefinition

	expanded := e.expandBlocks(e.blocks.OfType("module"))

	for _, moduleBlock := range expanded {
		if moduleBlock.Label() == "" {
			continue
		}
		moduleDefinition, err := e.loadModule(ctx, moduleBlock)
		if err != nil {
			rng := moduleBlock.GetMetadata().Range()
			e.logger.Error("Failed to load module. Maybe try 'terraform init'?",
				log.String("loc", rng.String()), log.Err(err))
			continue
		}

		e.logger.Debug("Loaded module", log.String("name", moduleDefinition.Name), log.FilePath(moduleDefinition.Path))
		moduleDefinitions = append(moduleDefinitions, moduleDefinition)
	}

	return moduleDefinitions
}

// takes in a module "x" {} block and loads resources etc. into e.moduleBlocks - additionally returns variables to add to ["module.x.*"] variables
func (e *evaluator) loadModule(ctx context.Context, b *terraform.Block) (*ModuleDefinition, error) {
	if b.Label() == "" {
		return nil, errors.New("module without label")
	}

	sourceVal := b.GetAttribute("source").Value()
	if !sourceVal.IsKnown() || sourceVal.Type() != cty.String {
		return nil, errors.New("source is not a string")
	}

	source := sourceVal.AsString()
	if source == "" {
		return nil, errors.New("source is empty")
	}

	if e.moduleMetadata != nil {
		// if we have module metadata we can parse all the modules as they'll be cached locally!
		def, err := e.loadModuleFromTerraformCache(ctx, b, source)
		if err == nil {
			e.logger.Debug("Using module from Terraform cache .terraform/modules",
				log.String("source", source))
			return def, nil
		}

		e.logger.Debug("Failed to load module from .terraform cache",
			log.String("module_source", source),
			log.Err(err),
		)
	}

	// we don't have the module installed via 'terraform init' so we need to grab it...
	return e.loadExternalModule(ctx, b, source)
}

func (e *evaluator) loadModuleFromTerraformCache(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) {
	var modulePath string
	moduleKey := b.ModuleKey()
	for _, module := range e.moduleMetadata.Modules {
		if module.Key == moduleKey {
			modulePath = path.Clean(path.Join(e.projectRootPath, module.Dir))
			break
		}
	}
	if modulePath == "" {
		return nil, fmt.Errorf("resolve module with key %q from .terraform/modules", moduleKey)
	}

	if strings.HasPrefix(source, ".") {
		source = ""
	}

	e.logger.Debug("Module resolved using modules.json",
		log.String("block", b.FullName()),
		log.String("source", source),
		log.String("modulePath", modulePath),
	)
	moduleParser := e.parentParser.newModuleParser(e.filesystem, source, modulePath, b.Label(), b)
	if err := moduleParser.ParseFS(ctx, modulePath); err != nil {
		return nil, fmt.Errorf("parse module filesystem: %w", err)
	}

	return &ModuleDefinition{
		Name:       b.Label(),
		Path:       modulePath,
		Definition: b,
		Parser:     moduleParser,
		FileSystem: e.filesystem,
	}, nil
}

func (e *evaluator) loadExternalModule(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) {

	e.logger.Debug("Locating non-initialized module", log.String("source", source))

	version := b.GetAttribute("version").AsStringValueOrDefault("", b).Value()
	opt := resolvers.Options{
		Source:          source,
		OriginalSource:  source,
		Version:         version,
		OriginalVersion: version,
		WorkingDir:      e.projectRootPath,
		Name:            b.FullName(),
		ModulePath:      e.modulePath,
		Logger:          e.logger.With(log.Prefix("module resolver")),
		AllowDownloads:  e.allowDownloads,
		SkipCache:       e.skipCachedModules,
	}

	filesystem, prefix, downloadPath, err := resolveModule(ctx, e.filesystem, opt)
	if err != nil {
		return nil, err
	}
	prefix = path.Join(e.parentParser.moduleSource, prefix)
	e.logger.Debug("Module resolved",
		log.String("block", b.FullName()),
		log.String("source", source),
		log.String("prefix", prefix),
		log.FilePath(downloadPath),
	)
	moduleParser := e.parentParser.newModuleParser(filesystem, prefix, downloadPath, b.Label(), b)
	if err := moduleParser.ParseFS(ctx, downloadPath); err != nil {
		return nil, err
	}
	return &ModuleDefinition{
		Name:       b.Label(),
		Path:       downloadPath,
		Definition: b,
		Parser:     moduleParser,
		FileSystem: filesystem,
		External:   true,
	}, nil
}
