package internal

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"go/ast"
	"go/types"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/rs/zerolog"
	"github.com/vektra/mockery/v3/config"
	"github.com/vektra/mockery/v3/internal/logging"
	"github.com/vektra/mockery/v3/internal/stackerr"
	"golang.org/x/tools/go/packages"
)

var autoGeneratedRegex = regexp.MustCompile(`^\/\/ Code generated by .*; DO NOT EDIT(\.?)( )*$`)

type Parser struct {
	parserPackages []*types.Package
	conf           packages.Config
	mockeryConfig  config.RootConfig
}

func NewParser(buildTags []string, mockeryConfig config.RootConfig) *Parser {
	var conf packages.Config
	conf.Mode = packages.NeedTypes |
		packages.NeedTypesSizes |
		packages.NeedSyntax |
		packages.NeedTypesInfo |
		packages.NeedImports |
		packages.NeedName |
		packages.NeedFiles |
		packages.NeedCompiledGoFiles

	if len(buildTags) > 0 {
		conf.BuildFlags = []string{"-tags", strings.Join(buildTags, ",")}
	}
	p := &Parser{
		parserPackages: make([]*types.Package, 0),
		conf:           conf,
		mockeryConfig:  mockeryConfig,
	}
	return p
}

func (p *Parser) ParsePackages(ctx context.Context, packageNames []string) ([]*Interface, error) {
	log := zerolog.Ctx(ctx)
	interfaces := []*Interface{}

	packages, err := packages.Load(&p.conf, packageNames...)
	if err != nil {
		return nil, err
	}
	for _, pkg := range packages {
		pkgLog := log.With().Str("package", pkg.PkgPath).Logger()
		pkgCtx := pkgLog.WithContext(ctx)
		pkgConfig, err := p.mockeryConfig.GetPackageConfig(ctx, pkg.PkgPath)
		if err != nil {
			return nil, fmt.Errorf("getting package-level config: %w", err)
		}

		if len(pkg.GoFiles) == 0 {
			continue
		}
		for _, err := range pkg.Errors {
			log.Err(err).Msg("encountered error when loading package")
		}
		if len(pkg.Errors) != 0 {
			return nil, errors.New("error occurred when loading packages")
		}
		for fileIdx, filePath := range pkg.GoFiles {
			filePath = filepath.ToSlash(filePath)
			fileLog := pkgLog.With().Str("file", filePath).Logger()
			fileLog.Debug().Msg("found file")

			if *pkgConfig.Config.IncludeAutoGenerated == false {
				isGenerated, err := isAutoGenerated(filePath)
				if err != nil {
					return nil, fmt.Errorf("determining if file is auto-generated: %w", err)
				}
				if isGenerated {
					fileLog.Debug().Str("docs-url", logging.DocsURL("/include-auto-generated")).Msg("file is auto-generated, skipping.")
					continue
				}
			}

			fileLog.Debug().Msg("file is not auto-generated.")

			fileCtx := fileLog.WithContext(pkgCtx)

			fileSyntax := pkg.Syntax[fileIdx]
			nv := NewNodeVisitor(fileCtx)
			ast.Walk(nv, fileSyntax)

			scope := pkg.Types.Scope()
			for _, declaredInterface := range nv.declaredInterfaces {
				ifaceLog := fileLog.With().Str("interface", declaredInterface.typeSpec.Name.Name).Logger()

				obj := scope.Lookup(declaredInterface.typeSpec.Name.Name)
				if obj == nil {
					log.Debug().Str("identifier-name", declaredInterface.typeSpec.Name.Name).Msg("obj was nil")
					continue
				}

				var typ *types.Named
				var name string
				ttyp := obj.Type()

				if talias, ok := obj.Type().(*types.Alias); ok {
					name = talias.Obj().Name()
					ttyp = types.Unalias(obj.Type())
				}

				typ, ok := ttyp.(*types.Named)
				if !ok {
					ifaceLog.Debug().Msg("interface is not named, skipping")
					continue
				}

				if name == "" {
					name = typ.Obj().Name()
				}

				if typ.Obj().Pkg() == nil {
					continue
				}

				if !types.IsInterface(typ.Underlying()) {
					ifaceLog.Debug().Msg("type is not an interface, skipping")
					continue
				}

				interfaces = append(interfaces, NewInterface(
					name,
					declaredInterface.typeSpec,
					declaredInterface.genDecl,
					filePath,
					fileSyntax,
					pkg,
					// Leave the config nil because we don't yet know if
					// the interface should even be generated in the first
					// place.
					nil,
				))
			}
		}
	}
	return interfaces, nil
}

func isAutoGenerated(pathName string) (bool, error) {
	file, err := os.Open(pathName)
	if err != nil {
		return false, stackerr.NewStackErr(err)
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		text := scanner.Text()
		if autoGeneratedRegex.MatchString(text) {
			return true, nil
		} else if strings.HasPrefix(text, "package ") {
			break
		}
	}
	return false, nil
}
