package tygoja

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

	"golang.org/x/tools/go/packages"
)

// Tygoja is a generator for one or more input packages,
// responsible for linking them together if necessary.
type Tygoja struct {
	conf *Config

	parent               *Tygoja
	implicitPackages     map[string][]string
	generatedTypes       map[string][]string
	generatedPackageDocs map[string]struct{}
}

// New initializes a new Tygoja generator from the specified config.
func New(config Config) *Tygoja {
	config.InitDefaults()

	return &Tygoja{
		conf:                 &config,
		implicitPackages:     map[string][]string{},
		generatedTypes:       map[string][]string{},
		generatedPackageDocs: map[string]struct{}{},
	}
}

// Generate executes the generator and produces the related TS files.
func (g *Tygoja) Generate() (string, error) {
	// extract config packages
	configPackages := make([]string, 0, len(g.conf.Packages))
	for p, types := range g.conf.Packages {
		if len(types) == 0 {
			continue // no typings
		}
		configPackages = append(configPackages, p)
	}

	// load packages info
	pkgs, err := packages.Load(&packages.Config{
		Mode: packages.NeedSyntax | packages.NeedFiles | packages.NeedDeps | packages.NeedImports | packages.NeedTypes,
	}, configPackages...)
	if err != nil {
		return "", err
	}

	var s strings.Builder

	// Heading
	if g.parent == nil {
		s.WriteString("// GENERATED CODE - DO NOT MODIFY BY HAND\n")

		if g.conf.Heading != "" {
			s.WriteString(g.conf.Heading)
		}

		// write base types
		// ---
		s.WriteString("type ")
		s.WriteString(BaseTypeDict)
		s.WriteString(" = { [key:string | number | symbol]: any; }\n")

		s.WriteString("type ")
		s.WriteString(BaseTypeAny)
		s.WriteString(" = any\n")
		// ---
	}

	for i, pkg := range pkgs {
		if len(pkg.Errors) > 0 {
			return "", fmt.Errorf("%+v", pkg.Errors)
		}

		if len(pkg.GoFiles) == 0 {
			return "", fmt.Errorf("no input go files for package index %d", i)
		}

		if len(g.conf.Packages[pkg.ID]) == 0 {
			// ignore the package as it has no typings
			continue
		}

		pkgGen := &PackageGenerator{
			conf:           g.conf,
			pkg:            pkg,
			types:          g.conf.Packages[pkg.ID],
			withPkgDoc:     !g.isPackageDocGenerated(pkg.ID),
			generatedTypes: map[string]struct{}{},
			unknownTypes:   map[string]struct{}{},
			imports:        map[string][]string{},
		}

		code, err := pkgGen.Generate()
		if err != nil {
			return "", err
		}

		g.generatedPackageDocs[pkg.ID] = struct{}{}

		for t := range pkgGen.generatedTypes {
			g.generatedTypes[pkg.ID] = append(g.generatedTypes[pkg.ID], t)
		}

		for t := range pkgGen.unknownTypes {
			parts := strings.Split(t, ".")
			var tPkg string
			var tName string

			if len(parts) == 0 {
				continue
			}

			if len(parts) == 2 {
				// type from external package
				tPkg = parts[0]
				tName = parts[1]
			} else {
				// unexported type from the current package
				tName = parts[0]

				// already mapped for export
				if pkgGen.isTypeAllowed(tName) {
					continue
				}

				tPkg = packageNameFromPath(pkg.ID)

				// add to self import later
				pkgGen.imports[pkg.ID] = []string{tPkg}
			}

			for p, aliases := range pkgGen.imports {
				for _, alias := range aliases {
					if tName != "" && alias == tPkg && !g.isTypeGenerated(p, tName) && !exists(g.implicitPackages[p], tName) {
						if g.implicitPackages[p] == nil {
							g.implicitPackages[p] = []string{}
						}
						g.implicitPackages[p] = append(g.implicitPackages[p], tName)
						break
					}
				}
			}
		}

		s.WriteString(code)
	}

	// recursively try to generate the found unknown types
	if len(g.implicitPackages) > 0 {
		subConfig := *g.conf
		subConfig.Heading = ""
		if (subConfig.TypeMappings) == nil {
			subConfig.TypeMappings = map[string]string{}
		}

		// extract the nonempty package definitions
		subConfig.Packages = make(map[string][]string, len(g.implicitPackages))
		for p, types := range g.implicitPackages {
			if len(types) == 0 {
				continue
			}
			subConfig.Packages[p] = types
		}

		subGenerator := New(subConfig)
		subGenerator.parent = g
		subResult, err := subGenerator.Generate()
		if err != nil {
			return "", err
		}

		s.WriteString(subResult)
	}

	return s.String(), nil
}

func (g *Tygoja) isPackageDocGenerated(pkgId string) bool {
	_, ok := g.generatedPackageDocs[pkgId]
	if ok {
		return true
	}

	if g.parent != nil {
		return g.parent.isPackageDocGenerated(pkgId)
	}

	return false
}

func (g *Tygoja) isTypeGenerated(pkg string, name string) bool {
	if g.parent != nil && g.parent.isTypeGenerated(pkg, name) {
		return true
	}

	if len(g.generatedTypes[pkg]) == 0 {
		return false
	}

	for _, t := range g.generatedTypes[pkg] {
		if t == name {
			return true
		}
	}

	return false
}

// isTypeAllowed checks whether the provided type name is allowed by the generator "types".
func (g *PackageGenerator) isTypeAllowed(name string) bool {
	name = strings.TrimSpace(name)

	if name == "" {
		return false
	}

	for _, t := range g.types {
		if t == name || t == "*" {
			return true
		}
	}

	return false
}

func (g *PackageGenerator) markTypeAsGenerated(t string) {
	g.generatedTypes[t] = struct{}{}
}

var versionRegex = regexp.MustCompile(`^v\d+$`)

// packageNameFromPath extracts and normalizes the imported package identifier.
//
// For example:
//
// "github.com/labstack/echo/v5" -> "echo"
// "github.com/go-ozzo/ozzo-validation/v4" -> "ozzo_validation"
func packageNameFromPath(path string) string {
	name := filepath.Base(strings.Trim(path, `"' `))

	if versionRegex.MatchString(name) {
		name = filepath.Base(filepath.Dir(path))
	}

	return strings.ReplaceAll(name, "-", "_")
}

// exists checks if search exists in list.
func exists[T comparable](list []T, search T) bool {
	for _, v := range list {
		if v == search {
			return true
		}
	}

	return false
}
