package pkg

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"go/ast"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"text/template"

	"github.com/chigopher/pathlib"
	"github.com/huandu/xstrings"
	"github.com/iancoleman/strcase"
	"github.com/rs/zerolog"

	"github.com/vektra/mockery/v2/pkg/config"
	"github.com/vektra/mockery/v2/pkg/logging"
	"github.com/vektra/mockery/v2/pkg/stackerr"
)

var ErrInfiniteLoop = fmt.Errorf("infinite loop in template variables detected")

// Functions available in the template for manipulating
//
// Since the map and its functions are stateless, it exists as
// a package var rather than being initialized on every call
// in [parseConfigTemplates] and [generator.printTemplate]
var templateFuncMap = template.FuncMap{
	// String inspection and manipulation
	"contains":    strings.Contains,
	"hasPrefix":   strings.HasPrefix,
	"hasSuffix":   strings.HasSuffix,
	"join":        strings.Join,
	"replace":     strings.Replace,
	"replaceAll":  strings.ReplaceAll,
	"split":       strings.Split,
	"splitAfter":  strings.SplitAfter,
	"splitAfterN": strings.SplitAfterN,
	"trim":        strings.Trim,
	"trimLeft":    strings.TrimLeft,
	"trimPrefix":  strings.TrimPrefix,
	"trimRight":   strings.TrimRight,
	"trimSpace":   strings.TrimSpace,
	"trimSuffix":  strings.TrimSuffix,
	"lower":       strings.ToLower,
	"upper":       strings.ToUpper,
	"camelcase":   xstrings.ToCamelCase,
	"snakecase":   xstrings.ToSnakeCase,
	"kebabcase":   xstrings.ToKebabCase,
	"firstLower":  xstrings.FirstRuneToLower,
	"firstUpper":  xstrings.FirstRuneToUpper,

	// Regular expression matching
	"matchString": regexp.MatchString,
	"quoteMeta":   regexp.QuoteMeta,

	// Filepath manipulation
	"base":  filepath.Base,
	"clean": filepath.Clean,
	"dir":   filepath.Dir,

	// Basic access to reading environment variables
	"expandEnv": os.ExpandEnv,
	"getenv":    os.Getenv,
}

type Cleanup func() error

type OutputStreamProvider interface {
	GetWriter(context.Context, *Interface) (io.Writer, error, Cleanup)
}

type StdoutStreamProvider struct{}

func (*StdoutStreamProvider) GetWriter(ctx context.Context, iface *Interface) (io.Writer, error, Cleanup) {
	return os.Stdout, nil, func() error { return nil }
}

type FileOutputStreamProvider struct {
	Config                    config.Config
	BaseDir                   string
	InPackage                 bool
	InPackageSuffix           bool
	TestOnly                  bool
	Case                      string
	KeepTree                  bool
	KeepTreeOriginalDirectory string
	FileName                  string
}

func (p *FileOutputStreamProvider) GetWriter(ctx context.Context, iface *Interface) (io.Writer, error, Cleanup) {
	log := zerolog.Ctx(ctx).With().Str(logging.LogKeyInterface, iface.Name).Logger()
	// ctx = log.WithContext(ctx)

	var path string

	caseName := iface.Name
	if p.Case == "underscore" || p.Case == "snake" {
		caseName = p.underscoreCaseName(caseName)
	}

	if p.KeepTree {
		absOriginalDir, err := filepath.Abs(p.KeepTreeOriginalDirectory)
		if err != nil {
			return nil, err, func() error { return nil }
		}
		relativePath := strings.TrimPrefix(
			filepath.Join(filepath.Dir(iface.FileName), p.filename(caseName)),
			absOriginalDir)

		// as it's not possible to import from internal path, we have to replace it in mocks when KeepTree is used
		relativePath = strings.Replace(relativePath, "/internal/", "/internal_/", -1)

		path = filepath.Join(p.BaseDir, relativePath)
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return nil, err, func() error { return nil }
		}
	} else if p.InPackage {
		path = filepath.Join(filepath.Dir(iface.FileName), p.filename(caseName))
	} else {
		path = filepath.Join(p.BaseDir, p.filename(caseName))
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return nil, err, func() error { return nil }
		}
	}

	log = log.With().Str(logging.LogKeyPath, path).Logger()

	log.Debug().Msgf("creating writer to file")
	f, err := os.Create(path)
	if err != nil {
		return nil, err, func() error { return nil }
	}

	return f, nil, func() error {
		return f.Close()
	}
}

func (p *FileOutputStreamProvider) filename(name string) string {
	if p.FileName != "" {
		return p.FileName
	}

	if p.InPackage && p.TestOnly {
		if p.InPackageSuffix {
			return name + "_mock_test.go"
		}

		return "mock_" + name + "_test.go"
	} else if p.InPackage && !p.KeepTree {
		if p.InPackageSuffix {
			return name + "_mock.go"
		}

		return "mock_" + name + ".go"
	} else if p.TestOnly {
		return name + "_test.go"
	}

	return name + ".go"
}

// shamelessly taken from http://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-camel-caseo
func (*FileOutputStreamProvider) underscoreCaseName(caseName string) string {
	rxp1 := regexp.MustCompile("(.)([A-Z][a-z]+)")
	s1 := rxp1.ReplaceAllString(caseName, "${1}_${2}")
	rxp2 := regexp.MustCompile("([a-z0-9])([A-Z])")
	return strings.ToLower(rxp2.ReplaceAllString(s1, "${1}_${2}"))
}

// parseConfigTemplates parses various templated strings
// in the config struct into their fully defined values. This mutates
// the config object passed.
func parseConfigTemplates(ctx context.Context, c *config.Config, iface *Interface) error {
	log := zerolog.Ctx(ctx)

	mock := "mock"
	if ast.IsExported(iface.Name) {
		mock = "Mock"
	}

	workingDir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("get working directory: %w", err)
	}
	var interfaceDirRelative string
	interfaceDir := pathlib.NewPath(iface.FileName).Parent()
	interfaceDirRelativePath, err := interfaceDir.RelativeToStr(workingDir)
	if errors.Is(err, pathlib.ErrRelativeTo) {
		log.Debug().
			Stringer("interface-dir", interfaceDir).
			Str("working-dir", workingDir).
			Msg("can't make interfaceDir relative to working dir. Setting InterfaceDirRelative to package path.")

		interfaceDirRelative = iface.Pkg.Path()
	} else {
		interfaceDirRelative = interfaceDirRelativePath.String()
	}

	// data is the struct sent to the template parser
	data := struct {
		ConfigDir               string
		InterfaceDir            string
		InterfaceDirRelative    string
		InterfaceFile           string
		InterfaceName           string
		InterfaceNameCamel      string
		InterfaceNameLowerCamel string
		InterfaceNameSnake      string
		InterfaceNameLower      string
		Mock                    string
		MockName                string
		PackageName             string
		PackagePath             string
	}{
		ConfigDir:            filepath.Dir(c.Config),
		InterfaceDir:         filepath.Dir(iface.FileName),
		InterfaceDirRelative: interfaceDirRelative,
		InterfaceFile:        iface.FileName,
		InterfaceName:        iface.Name,
		// Deprecated: All custom case variables of InterfaceName will be removed in the next major version
		// Use the template functions instead
		InterfaceNameCamel:      strcase.ToCamel(iface.Name),
		InterfaceNameLowerCamel: strcase.ToLowerCamel(iface.Name),
		InterfaceNameSnake:      strcase.ToSnake(iface.Name),
		InterfaceNameLower:      strings.ToLower(iface.Name),
		Mock:                    mock,
		MockName:                c.MockName,
		PackageName:             iface.Pkg.Name(),
		PackagePath:             iface.Pkg.Path(),
	}
	// These are the config options that we allow
	// to be parsed by the templater. The keys are
	// just labels we're using for logs/errors
	templateMap := map[string]*string{
		"filename": &c.FileName,
		"dir":      &c.Dir,
		"mockname": &c.MockName,
		"outpkg":   &c.Outpkg,
	}

	changesMade := true
	for i := 0; changesMade; i++ {
		if i >= 20 {
			log.Error().Msg("infinite loop in template variables detected")
			for key, val := range templateMap {
				l := log.With().Str("variable-name", key).Str("variable-value", *val).Logger()
				l.Error().Msg("config variable value")
			}
			return ErrInfiniteLoop
		}
		// Templated variables can refer to other templated variables,
		// so we need to continue parsing the templates until it can't
		// be parsed anymore.
		changesMade = false

		for name, attributePointer := range templateMap {
			oldVal := *attributePointer

			attributeTempl, err := template.New("interface-template").Funcs(templateFuncMap).Parse(*attributePointer)
			if err != nil {
				return fmt.Errorf("failed to parse %s template: %w", name, err)
			}
			var parsedBuffer bytes.Buffer

			if err := attributeTempl.Execute(&parsedBuffer, data); err != nil {
				return fmt.Errorf("failed to execute %s template: %w", name, err)
			}
			*attributePointer = parsedBuffer.String()
			if *attributePointer != oldVal {
				changesMade = true
			}
		}
	}

	return nil
}

// Outputter wraps the Generator struct. It calls the generator
// to create the mock implementations in-memory, then has additional
// logic to determine where the mock should be written to on disk.
type Outputter struct {
	boilerplate string
	config      *config.Config
	dryRun      bool
}

func NewOutputter(
	config *config.Config,
	boilerplate string,
	dryRun bool,
) *Outputter {
	return &Outputter{
		boilerplate: boilerplate,
		config:      config,
		dryRun:      dryRun,
	}
}

func (m *Outputter) Generate(ctx context.Context, iface *Interface) error {
	log := zerolog.Ctx(ctx).With().
		Str(logging.LogKeyInterface, iface.Name).
		Str(logging.LogKeyQualifiedName, iface.QualifiedName).
		Logger()
	ctx = log.WithContext(ctx)

	log.Info().Msg("generating mocks for interface")

	log.Debug().Msg("getting config for interface")
	interfaceConfigs, err := m.config.GetInterfaceConfig(ctx, iface.QualifiedName, iface.Name)
	if err != nil {
		return err
	}

	for _, interfaceConfig := range interfaceConfigs {
		interfaceConfig.LogUnsupportedPackagesConfig(ctx)

		log.Debug().Msg("getting mock generator")

		if err := parseConfigTemplates(ctx, interfaceConfig, iface); err != nil {
			return fmt.Errorf("failed to parse config template: %w", err)
		}
		interfaceConfig.LogDeprecatedConfig(ctx)

		g := GeneratorConfig{
			Boilerplate:          m.boilerplate,
			DisableVersionString: interfaceConfig.DisableVersionString,
			Exported:             interfaceConfig.Exported,
			InPackage:            interfaceConfig.InPackage,
			Issue845Fix:          interfaceConfig.Issue845Fix,
			KeepTree:             interfaceConfig.KeepTree,
			Note:                 interfaceConfig.Note,
			MockBuildTags:        interfaceConfig.MockBuildTags,
			Outpkg:               interfaceConfig.Outpkg,
			PackageNamePrefix:    interfaceConfig.Packageprefix,
			StructName:           interfaceConfig.MockName,
			UnrollVariadic:       interfaceConfig.UnrollVariadic,
			WithExpecter:         interfaceConfig.WithExpecter,
			ReplaceType:          interfaceConfig.ReplaceType,
			ResolveTypeAlias:     interfaceConfig.ResolveTypeAlias,
		}
		generator := NewGenerator(ctx, g, iface, interfaceConfig.Outpkg)

		log.Debug().Msg("generating mock in-memory")
		if err := generator.GenerateAll(ctx); err != nil {
			return err
		}

		// Log where the file would be written to before checking whether to create the directories and files
		outputPath := pathlib.NewPath(interfaceConfig.Dir).Join(interfaceConfig.FileName)
		fileLog := log.With().Stringer(logging.LogKeyFile, outputPath).Logger()
		fileLog.Info().Msg("writing to file")

		if m.dryRun {
			continue
		}

		if err := outputPath.Parent().MkdirAll(); err != nil {
			return stackerr.NewStackErrf(err, "failed to mkdir parents of: %v", outputPath)
		}

		file, err := outputPath.OpenFile(os.O_RDWR | os.O_CREATE | os.O_TRUNC)
		if err != nil {
			return stackerr.NewStackErrf(err, "failed to open output file for mock: %v", outputPath)
		}
		defer file.Close()
		if err := generator.Write(file); err != nil {
			return stackerr.NewStackErrf(err, "failed to write to file")
		}
	}
	return nil
}
