// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package configs

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/spf13/afero"
)

// ConfigFileSet holds the different types of configuration files found in a directory.
type ConfigFileSet struct {
	Primary         []string // Regular .tf and .tf.json files
	Override        []string // Override files (override.tf or *_override.tf)
	Tests           []string // Test files (.tftest.hcl or .tftest.json)
	Queries         []string // Query files (.tfquery.hcl)
	StateMigrations []string // State migration files (.tfmigrate.hcl)
}

// FileMatcher is an interface for components that can match and process specific file types
// in a Terraform module directory.

type FileMatcher interface {
	// Matches returns true if the given filename should be processed by this matcher
	Matches(name string) bool

	// DirFiles allows the matcher to process files in a directory
	// only relevant to its type.
	DirFiles(dir string, cfg *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics
}

// Option is a functional option type for configuring the parser
type Option func(*parserConfig)

type parserConfig struct {
	matchers      []FileMatcher
	testDirectory string
	fs            afero.Afero
}

// dirFileSet finds Terraform configuration files within directory dir
// and returns a ConfigFileSet containing the found files.
// It uses the given options to determine which types of files to look for
// and how to process them. The returned ConfigFileSet contains the paths
// to the found files, categorized by their type (primary, override, test, query).
func (p *Parser) dirFileSet(dir string, opts ...Option) (ConfigFileSet, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	fileSet := ConfigFileSet{
		Primary:         []string{},
		Override:        []string{},
		Tests:           []string{},
		Queries:         []string{},
		StateMigrations: []string{},
	}

	// Set up the parser configuration
	cfg := &parserConfig{
		// We always match .tf files
		matchers:      []FileMatcher{&moduleFiles{}},
		testDirectory: DefaultTestDirectory,
		fs:            p.fs,
	}
	for _, opt := range opts {
		opt(cfg)
	}

	// Scan and categorize main directory files
	mainDirDiags := p.rootFiles(dir, cfg.matchers, &fileSet)
	diags = append(diags, mainDirDiags...)
	if diags.HasErrors() {
		return fileSet, diags
	}

	// Process matcher-specific files
	for _, matcher := range cfg.matchers {
		matcherDiags := matcher.DirFiles(dir, cfg, &fileSet)
		diags = append(diags, matcherDiags...)
	}

	return fileSet, diags
}

// rootFiles scans the main directory for configuration files
// and categorizes them using the appropriate file matchers.
func (p *Parser) rootFiles(dir string, matchers []FileMatcher, fileSet *ConfigFileSet) hcl.Diagnostics {
	var diags hcl.Diagnostics

	// Read main directory files
	infos, err := p.fs.ReadDir(dir)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Failed to read module directory",
			Detail:   fmt.Sprintf("Module directory %s does not exist or cannot be read.", dir),
		})
		return diags
	}

	for _, info := range infos {
		if info.IsDir() || IsIgnoredFile(info.Name()) {
			continue
		}

		name := info.Name()
		fullPath := filepath.Join(dir, name)

		// Try each matcher to see if it matches
		for _, matcher := range matchers {
			if matcher.Matches(name) {
				switch p := matcher.(type) {
				case *moduleFiles:
					if p.isOverride(name) {
						fileSet.Override = append(fileSet.Override, fullPath)
					} else {
						fileSet.Primary = append(fileSet.Primary, fullPath)
					}
				case *testFiles:
					fileSet.Tests = append(fileSet.Tests, fullPath)
				case *queryFiles:
					fileSet.Queries = append(fileSet.Queries, fullPath)
				case *stateMigrateFiles:
					fileSet.StateMigrations = append(fileSet.StateMigrations, fullPath)
				}
				break // Stop checking other matchers once a match is found
			}
		}
	}

	return diags
}

// MatchTestFiles adds a matcher for Terraform test files (.tftest.hcl and .tftest.json)
func MatchTestFiles(dir string) Option {
	return func(o *parserConfig) {
		o.testDirectory = dir
		o.matchers = append(o.matchers, &testFiles{})
	}
}

// MatchQueryFiles adds a matcher for Terraform query files (.tfquery.hcl and .tfquery.json)
func MatchQueryFiles() Option {
	return func(o *parserConfig) {
		o.matchers = append(o.matchers, &queryFiles{})
	}
}

// MatchStateMigrateFiles adds a matcher for Terraform state migrate files (.tfmigrate.hcl only)
func MatchStateMigrateFiles() Option {
	return func(o *parserConfig) {
		o.matchers = append(o.matchers, &stateMigrateFiles{})
	}
}

// moduleFiles matches regular Terraform configuration files (.tf and .tf.json)
type moduleFiles struct{}

func (m *moduleFiles) Matches(name string) bool {
	ext := fileExt(name)
	if ext != ".tf" && ext != ".tf.json" {
		return false
	}

	return true
}

func (m *moduleFiles) isOverride(name string) bool {
	ext := fileExt(name)
	if ext != ".tf" && ext != ".tf.json" {
		return false
	}

	baseName := name[:len(name)-len(ext)] // strip extension
	isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override")
	return isOverride
}

func (m *moduleFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
	return nil
}

// testFiles matches Terraform test files (.tftest.hcl and .tftest.json)
type testFiles struct{}

func (t *testFiles) Matches(name string) bool {
	return strings.HasSuffix(name, ".tftest.hcl") || strings.HasSuffix(name, ".tftest.json")
}

func (t *testFiles) DirFiles(dir string, opts *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
	var diags hcl.Diagnostics

	testPath := path.Join(dir, opts.testDirectory)
	testInfos, err := opts.fs.ReadDir(testPath)

	if err != nil {
		// Then we couldn't read from the testing directory for some reason.
		if os.IsNotExist(err) {
			// Then this means the testing directory did not exist.
			// We won't actually stop loading the rest of the configuration
			// for this, we will add a warning to explain to the user why
			// test files weren't processed but leave it at that.
			if opts.testDirectory != DefaultTestDirectory {
				// We'll only add the warning if a directory other than the
				// default has been requested. If the user is just loading
				// the default directory then we have no expectation that
				// it should actually exist.
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagWarning,
					Summary:  "Test directory does not exist",
					Detail:   fmt.Sprintf("Requested test directory %s does not exist.", testPath),
				})
			}
		} else {
			// Then there is some other reason we couldn't load. We will
			// treat this as a full error.
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Failed to read test directory",
				Detail:   fmt.Sprintf("Test directory %s could not be read: %v.", testPath, err),
			})

			// We'll also stop loading the rest of the config for this.
			return diags
		}
		return diags
	}

	// Process test files
	for _, info := range testInfos {
		if !t.Matches(info.Name()) {
			continue
		}

		name := info.Name()
		fileSet.Tests = append(fileSet.Tests, filepath.Join(testPath, name))
	}

	return diags
}

// queryFiles matches Terraform query files (.tfquery.hcl and .tfquery.json)
type queryFiles struct{}

func (q *queryFiles) Matches(name string) bool {
	return strings.HasSuffix(name, ".tfquery.hcl") || strings.HasSuffix(name, ".tfquery.json")
}

func (q *queryFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
	return nil
}

// stateMigrateFiles matches Terraform state migrate files (.tfmigrate.hcl only)
type stateMigrateFiles struct{}

var _ FileMatcher = (*stateMigrateFiles)(nil)

func (s *stateMigrateFiles) Matches(name string) bool {
	return strings.HasSuffix(name, ".tfmigrate.hcl")
}

func (s *stateMigrateFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
	// There are no special directories for .tfmigrate.hcl files.
	return nil
}
