package cmd

import (
	"context"
	"fmt"
	"go/build"
	"math"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"github.com/pingcap-inc/alioth/extractor/go/analyzer"
	"github.com/pingcap-inc/alioth/extractor/go/compiler"
	logextractor "github.com/pingcap-inc/alioth/extractor/go/log"
	"github.com/pingcap-inc/alioth/pkg/util"
	"github.com/pingcap-inc/alioth/pkg/worker"
	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
	"github.com/pingcap-inc/alioth/storage"
	"github.com/pingcap/log"
	"github.com/spf13/cobra"
	"go.uber.org/zap"
)

var (
	defaultConcurrency = int(math.Floor(float64(runtime.NumCPU()) * float64(0.75)))
	Codebase           string
	FlterConfig        string
	Output             string
	LogLevel           string
	IgnoreDir          string
	ExtractConcurrency = defaultConcurrency
	rule               *logpattern_go_proto.LogPatternRule
)

func NewExtractCmd() *cobra.Command {
	cmdExtract := &cobra.Command{
		Use:          "extract",
		Short:        "Extract logs pattern and reference code information from codebase and compilation",
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			// handle filter config
			if FlterConfig != "" {
				rule = &logpattern_go_proto.LogPatternRule{}
				// read from config file
				if err := util.StrictDecodeFile(FlterConfig, rule); err != nil {
					return err
				}
			} else {
				// set default config, log_level = ["error"]
				rule = &logpattern_go_proto.LogPatternRule{
					LogLevel: []string{"error"},
				}
			}

			if !Exists(Codebase) {
				return fmt.Errorf("code %s doesn't exists", Codebase)
			}

			if Output == "" {
				absOutput, err := filepath.Abs(Codebase)
				if err != nil {
					return err
				}

				Output = fmt.Sprintf("./%s.logpattern", filepath.Base(absOutput))
			}

			var ignoreDirs []string
			if len(IgnoreDir) > 0 {
				ignoreDirs = strings.Split(IgnoreDir, ",")
			}

			if err := util.InitLogger(LogLevel, ""); err != nil {
				log.Fatal("Failed to initialize log", zap.Error(err))
			}

			store, err := storage.NewStore(Output)
			if err != nil {
				log.Fatal("open log pattern store failed", zap.Error(err))
			}

			ExtractLogPattern(store, Codebase, ignoreDirs, rule)
			return nil
		},
	}

	cmdExtract.Flags().StringVar(&Codebase, "codebase", "./", "Source codebase directory for extracting log information")
	cmdExtract.Flags().StringVar(&FlterConfig, "filter", "", "the log filter rule config file using json format, if no config file, default set logLevel = error")
	cmdExtract.Flags().StringVar(&Output, "output", "", "the output file that stores the extracted log pattern and reference code information(default \"./${codebase-dirname}.logpattern\")")
	cmdExtract.Flags().StringVar(&LogLevel, "log-level", "info", "log level: debug, info, warn, error, fatal")
	cmdExtract.Flags().StringVar(&IgnoreDir, "ignore-dirs", "", "ignore some directories in the codebase (relative path to codebase)")
	cmdExtract.Flags().IntVar(&ExtractConcurrency, "concurrency", defaultConcurrency, "compile package and analyze code concurrency")

	return cmdExtract
}

func ExtractLogPattern(store *storage.LogPatternStore, codebase string, ignoreDirs []string, rule *logpattern_go_proto.LogPatternRule) {
	err := store.WriteLogPatternRule(context.Background(), rule)
	if err != nil {
		log.Fatal("save log pattern rule into log patern store failed", zap.Any("pattern rule", rule), zap.Error(err))
	}

	poolWorker := worker.NewPool(ExtractConcurrency, "extract")
	filter := logextractor.NewFilter(rule)
	builder := &logextractor.Builder{}

	path, err := filepath.Abs(codebase)
	if err != nil {
		log.Fatal("compute absolute path failed", zap.String("codebase", codebase), zap.Error(err))
	}
	repo, err := builder.Build(build.Default, path, ignoreDirs, poolWorker)
	if err != nil {
		log.Fatal("build codebase failed", zap.String("codebase", codebase), zap.Error(err))
	}

	hash, tag := repo.GetGitVersion()
	log.Info("build preparation successed", zap.String("codebase", codebase), zap.String("git hash", hash), zap.String("git tag", tag))

	ai := analyzer.NewAstAnalyzer(filter.Filter)
	output := ai.SetupOutput()

	logPatternCount := 0
	done := sync.WaitGroup{}
	done.Add(1)
	go func() {
		for {
			lp, ok := <-output
			if !ok {
				break
			}

			pattern := lp.(*logpattern_go_proto.LogPattern)
			pattern.Pos.PackageInfo.Hash = hash
			pattern.Pos.PackageInfo.Tag = tag
			logPatternCount++

			err := store.WriteLogPattern(context.Background(), pattern)
			if err != nil {
				log.Error("write log pattern failed", zap.Any("log pattern", lp), zap.Error(err))
			}

		}
		done.Done()
	}()

	wg := sync.WaitGroup{}
	err = repo.ForEach(func(pkg *compiler.PackageCompilation) error {
		wg.Add(1)
		go func() {
			wk := poolWorker.Apply()
			pkg.ForEach(func(file *compiler.FileCompilation, helper *analyzer.AstHelper) {
				err := file.RunAnalysis(ai, helper)
				if err != nil {
					log.Fatal("analyze file failed", zap.Any("file", file.PackagePath), zap.Error(err))
				}
			})
			poolWorker.Recycle(wk)
			wg.Done()
		}()

		return nil
	})

	wg.Wait()
	ai.MarkDone()
	if err != nil {
		log.Fatal("codebase analyze failed", zap.String("codebase", codebase), zap.Error(err))
	}
	done.Wait()

	log.Info("build successed", zap.String("codebase", codebase), zap.String("git hash", hash), zap.String("git tag", tag), zap.Int("log pattern count", logPatternCount))
}

func Exists(path string) bool {
	_, err := os.Stat(path)
	if err != nil {
		if os.IsExist(err) {
			return true
		}
		return false
	}
	return true
}
