package main

import (
	"bytes"
	"context"

	//"context"
	"flag"
	"fmt"
	"net/http"
	"net/http/pprof"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"

	//"sync"
	"time"

	"github.com/go-chi/chi/v5"
	zp "go.uber.org/zap"
	"go.uber.org/zap/zapcore"

	rlParser "a.yandex-team.ru/balancer/production/yastatic/yastatic_lberrors/parser"
	"a.yandex-team.ru/kikimr/public/sdk/go/persqueue"
	"a.yandex-team.ru/kikimr/public/sdk/go/persqueue/log/corelogadapter"
	"a.yandex-team.ru/kikimr/public/sdk/go/ydb"

	//"a.yandex-team.ru/kikimr/public/sdk/go/ydb"
	"a.yandex-team.ru/library/go/core/log"
	"a.yandex-team.ru/library/go/core/log/zap"
)

const (
	dateFormat = "2006-01-02T15:04:05.999999-0700"
	// 86400 - one day
	timeBucketsDevider = 60
	timeBucketsCount   = 86400 / timeBucketsDevider
)

var (
	endpoint  = flag.String("endpoint", "sas.logbroker.yandex.net", "")
	token     = flag.String("token", os.Getenv("LOGBROKER_TOKEN"), "LB OAuth token, default LOGBROKER_TOKEN env variable")
	topic     = flag.String("topic", "l7_balancer_yastatic/l7-balancer-yastatic-access-log", "Topic path")
	consumer  = flag.String("consumer", "l7_balancer_yastatic/errors-parser", "Consumer id")
	verbose   = flag.Bool("verbose", false, "verbosity")
	httpPort  = flag.Int("port", 8080, "HTTP Port")
	yasmItype = flag.String("itype", "balancer", "service itype")
	yasmCtype = flag.String("ctype", "prod", "service ctype")
	yasmPrj   = flag.String("prj", "l7-balancer-yastatic", "service prj")
)

var (
	Log = zap.Must(zp.Config{
		Level:            zp.NewAtomicLevelAt(zp.InfoLevel),
		Encoding:         "console",
		OutputPaths:      []string{"stdout"},
		ErrorOutputPaths: []string{"stderr"},
		EncoderConfig: zapcore.EncoderConfig{
			MessageKey:     "msg",
			LevelKey:       "level",
			TimeKey:        "ts",
			CallerKey:      "caller",
			EncodeLevel:    zapcore.CapitalColorLevelEncoder,
			EncodeTime:     zapcore.ISO8601TimeEncoder,
			EncodeDuration: zapcore.StringDurationEncoder,
			EncodeCaller:   zapcore.ShortCallerEncoder,
		},
	})
)

// URL idx = 5
// Host idx = 9
// Code idx = 11
var logLineRegexp = regexp.MustCompile(`ip_port=([^\t]+)\ttimestamp=([^\t]+)\tquery="((GET\s+|POST\s+)([^\t]+)(\sHTTP/[\d\.]{1,3}))"\twork_time=([^\t]+)\treferer=([^\t]+)\thost="([^\t]+)"\tworkflow=(.*\s([\d]{3})[\]]+)`)

func calcPart(date *time.Time) int {
	return (date.Hour()*3600 + date.Minute()*60 + date.Second()) / timeBucketsDevider
}

type StatStorage [timeBucketsCount]StatSuperHeap

func DataParser(data []byte, errorsDataChan chan rlParser.ParserRecord, succDataChan chan rlParser.ParserRecord) {
	err, succ := rlParser.DataMatcher(data)
	for _, i := range err {
		errorsDataChan <- i
	}

	for _, i := range succ {
		succDataChan <- i
	}
}

func parseMessage(data []byte, errorsDataChan chan rlParser.ParserRecord, succDataChan chan rlParser.ParserRecord) {
	for _, line := range bytes.Split(bytes.Trim(data, "\n"), []byte("\n")) {
		message := string(line)
		groups := logLineRegexp.FindAllStringSubmatch(message, -1)

		if len(groups) > 0 && len(groups[0]) >= 12 {
			code, err := strconv.Atoi(groups[0][11])
			if err == nil {
				rawURL := groups[0][5]
				u, err := url.Parse(rawURL)
				if err != nil {
					if *verbose {
						Log.Error("Error", log.Any("Incorrect url", rawURL), log.Error(err))
					}
					return
				}

				rawDate := groups[0][2]
				recordDate, err := time.Parse(dateFormat, rawDate)
				if err != nil {
					if *verbose {
						Log.Error("Error", log.Any("Incorrect date", rawDate), log.Error(err))
					}

					return
				}

				reqPath := u.EscapedPath()
				if *verbose {
					host := groups[0][9]
					Log.Info("Message", log.Any("Hr", recordDate.Hour()), log.Any("Min", recordDate.Minute()), log.Any("URL", reqPath), log.Any("Host", host), log.Any("Code", code))
				}

				if reqPath != "/ok.html" && reqPath != "/ping" && reqPath != "/l7ping" && !strings.HasPrefix("/awacs-balancer-health-check", reqPath) {
					if code != 200 && code != 304 && code != 206 && code != 301 && code != 302 {
						errorsDataChan <- rlParser.ParserRecord{Date: recordDate, URL: reqPath, Code: code}
					} else {
						succDataChan <- rlParser.ParserRecord{Date: recordDate, URL: reqPath, Code: code}
					}
				}
			} else if *verbose {
				Log.Error("Error converting status code", log.Error(err))
			}
		}
	}
}

func dataProcessor(records chan rlParser.ParserRecord, storage *StatStorage) {
	for record := range records {
		part := calcPart(&record.Date)

		storage[part].Push(record.URL, record.Code, 1)
	}
}

func dataCleaner(storage *StatStorage) {
	for {
		currDate := time.Now()
		currPart := calcPart(&currDate)

		// make new storage items to mark old for garbage collection
		for i := 5; i < 11; i++ {
			idx := currPart - i
			if idx < 0 {
				idx += timeBucketsCount
			}

			storage[idx] = *CreateStatSuperHeap()
		}

		time.Sleep(5 * time.Minute)
	}
}

func main() {
	flag.Parse()
	wg := &sync.WaitGroup{}

	var graphTopStorage GraphTopStorage
	graphtop, err := NewGraphTop(*yasmItype, *yasmCtype, *yasmPrj)
	if err != nil {
		Log.Fatal("Unable to create YASM client", log.Error(err))
	}

	wg.Add(1)
	go func() {
		defer wg.Done()
		if err = graphtop.Serve(&graphTopStorage); err != nil {
			Log.Fatal("Unable to start YASM client", log.Error(err))
		}
	}()

	var errorsStatStorage StatStorage
	var succStatStorage StatStorage
	for i := 0; i < timeBucketsCount; i++ {
		errorsStatStorage[i] = *CreateStatSuperHeap()
		succStatStorage[i] = *CreateStatSuperHeap()
	}

	errorsDataChan := make(chan rlParser.ParserRecord, 100)
	succDataChan := make(chan rlParser.ParserRecord, 100)

	wg.Add(1)
	go func() {
		defer wg.Done()
		dataProcessor(errorsDataChan, &errorsStatStorage)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		dataProcessor(succDataChan, &succStatStorage)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		dataCleaner(&errorsStatStorage)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		dataCleaner(&succStatStorage)
	}()

	localHostName, err := os.Hostname()
	if err != nil {
		Log.Fatal("Unable to get hostname", log.Error(err))
	}

	c := persqueue.NewReader(persqueue.ReaderOptions{
		Endpoint:              *endpoint,
		Credentials:           ydb.AuthTokenCredentials{AuthToken: *token},
		Consumer:              *consumer,
		Logger:                corelogadapter.New(Log),
		Topics:                []persqueue.TopicInfo{{Topic: *topic}},
		MaxReadSize:           5 * (1 << 20), // 5 MB
		MaxReadMessagesCount:  50,
		DecompressionDisabled: true,
		ReadOnlyLocal:         true,
		ReadTimestamp:         time.Now(),
		RetryOnFailure:        true,
	})

	ctx := context.Background()
	if _, err := c.Start(ctx); err != nil {
		Log.Fatal("Unable to start reader", log.Error(err))
	}

	/* ========================= Init HTTP Server ====================== */
	r := chi.NewRouter()

	r.Get("/unistat", func(w http.ResponseWriter, r *http.Request) {
		UnistatHandler(w, c)
	})

	r.Get("/debug/pprof/cmdline", pprof.Cmdline)
	r.Get("/debug/pprof/profile", pprof.Profile)
	r.Get("/debug/pprof/symbol", pprof.Symbol)
	r.Get("/debug/pprof/trace", pprof.Trace)
	r.Get("/debug/pprof/*", pprof.Index)
	r.Get("/debug/buckets/errors*", func(w http.ResponseWriter, r *http.Request) {
		BucketsHandler(w, &errorsStatStorage)
	})
	r.Get("/debug/buckets/succ*", func(w http.ResponseWriter, r *http.Request) {
		BucketsHandler(w, &succStatStorage)
	})

	r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
		DataHandler(w, r, &errorsStatStorage, &succStatStorage, &graphTopStorage, localHostName)
	})

	srv := &http.Server{
		Handler:      r,
		Addr:         fmt.Sprintf(":%d", *httpPort),
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}

	Log.Info("Starting HTTP server", log.Any("port", *httpPort))

	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := srv.ListenAndServe(); err != nil {
			Log.Fatal("Unable to start HTTP server", log.Error(err))
		}
	}()

	raw := make(chan persqueue.ReadMessage)
	decompressed := persqueue.Decompress(raw)

	for msg := range c.C() {
		switch v := msg.(type) {
		case *persqueue.Data:
			for _, b := range v.Batches() {
				if *verbose {
					Log.Info("Received batch",
						log.Any("topic", b.Topic),
						log.Any("partition", b.Partition),
						log.Any("messages", len(b.Messages)))
				}

				for _, m := range b.Messages {
					if *verbose {
						Log.Info("Received message", log.Any("seqNo", m.SeqNo))
					}

					raw <- m
					dec := <-decompressed

					if dec.Err == nil {
						DataParser(dec.Msg.Data, errorsDataChan, succDataChan)
						//parseMessage(dec.Msg.Data, errorsDataChan, succDataChan)
					} else {
						Log.Error("Error decoding data", log.Any("error", dec.Err))
					}
				}
			}
			v.Commit()

		default:
		}
	}

	<-c.Closed()
	if err := c.Err(); err != nil {
		Log.Fatal("Reader error", log.Error(err))
	}

	close(errorsDataChan)
	close(succDataChan)
	wg.Wait()
}
