package importas

import (
	"fmt"
	"go/ast"
	"go/types"
	"strconv"
	"strings"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/inspector"
)

var config = &Config{
	RequiredAlias: make(map[string]string),
}

var Analyzer = &analysis.Analyzer{
	Name: "importas",
	Doc:  "Enforces consistent import aliases",
	Run:  run,

	Flags: flags(config),

	Requires: []*analysis.Analyzer{inspect.Analyzer},
}

func run(pass *analysis.Pass) (interface{}, error) {
	return runWithConfig(config, pass)
}

func runWithConfig(config *Config, pass *analysis.Pass) (interface{}, error) {
	if err := config.CompileRegexp(); err != nil {
		return nil, err
	}

	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
	inspect.Preorder([]ast.Node{(*ast.ImportSpec)(nil)}, func(n ast.Node) {
		visitImportSpecNode(config, n.(*ast.ImportSpec), pass)
	})

	return nil, nil
}

func visitImportSpecNode(config *Config, node *ast.ImportSpec, pass *analysis.Pass) {
	if !config.DisallowUnaliased && node.Name == nil {
		return
	}

	alias := ""
	if node.Name != nil {
		alias = node.Name.String()
	}

	if alias == "." {
		return // Dot aliases are generally used in tests, so ignore.
	}

	if strings.HasPrefix(alias, "_") {
		return // Used by go test and for auto-includes, not a conflict.
	}

	path, err := strconv.Unquote(node.Path.Value)
	if err != nil {
		pass.Reportf(node.Pos(), "import not quoted")
	}

	if required, exists := config.AliasFor(path); exists && required != alias {
		message := fmt.Sprintf("import %q imported as %q but must be %q according to config", path, alias, required)
		if alias == "" {
			message = fmt.Sprintf("import %q imported without alias but must be with alias %q according to config", path, required)
		}

		pass.Report(analysis.Diagnostic{
			Pos:     node.Pos(),
			End:     node.End(),
			Message: message,
			SuggestedFixes: []analysis.SuggestedFix{{
				Message:   "Use correct alias",
				TextEdits: findEdits(node, pass.TypesInfo.Uses, path, alias, required),
			}},
		})
	}
}

func findEdits(node ast.Node, uses map[*ast.Ident]types.Object, importPath, original, required string) []analysis.TextEdit {
	// Edit the actual import line.
	result := []analysis.TextEdit{{
		Pos:     node.Pos(),
		End:     node.End(),
		NewText: []byte(required + " " + strconv.Quote(importPath)),
	}}

	// Edit all the uses of the alias in the code.
	for use, pkg := range uses {
		pkgName, ok := pkg.(*types.PkgName)
		if !ok {
			// skip identifiers that aren't pointing at a PkgName.
			continue
		}

		if pkgName.Pos() != node.Pos() {
			// skip identifiers pointing to a different import statement.
			continue
		}

		result = append(result, analysis.TextEdit{
			Pos:     use.Pos(),
			End:     use.End(),
			NewText: []byte(required),
		})
	}

	return result
}
