package main

import (
	"github.com/fsnotify/fsnotify"
	"github.com/go-chi/chi/v5"

	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

type counter struct {
	miss        int64
	bypass      int64
	expired     int64
	stale       int64
	updating    int64
	revalidated int64
	hit         int64
	other       int64

	s3RetryOne   int64
	s3RetryTwo   int64
	s3RetryThree int64
	s3RetryMax   int64
	zeroLength   int64

	mutex *sync.Mutex
}

var watcher *fsnotify.Watcher
var logPath string

func handler(w http.ResponseWriter, r *http.Request, cnt *counter, logger *log.Logger) {
	w.Header().Set("Content-Type", "text/json")
	cnt.mutex.Lock()
	_, err := fmt.Fprintf(w, `[["cache-miss_summ", %d],
["cache-hit_summ", %d],
["cache-bypass_summ", %d],
["cache-expired_summ", %d],
["cache-stale_summ", %d],
["cache-updating_summ", %d],
["cache-revalidated_summ", %d],
["cache-other_summ", %d],
["s3-retry-one_summ", %d],
["s3-retry-two_summ", %d],
["s3-retry-three_summ", %d],
["s3-retry-max_summ", %d],
["zero-length_summ", %d]]
`,
		cnt.miss, cnt.hit, cnt.bypass, cnt.expired, cnt.stale, cnt.updating, cnt.revalidated, cnt.other, cnt.s3RetryOne, cnt.s3RetryTwo, cnt.s3RetryThree, cnt.s3RetryMax, cnt.zeroLength)
	if err != nil {
		logger.Fatal(err)
	}
	cnt.mutex.Unlock()
}

var lineMatcher = `^(?P<remote_addr>[\w\.:]+?)\s+(?P<remote_user>.+?)\s+\[(?P<time_local>.+?)\]\s+"(?P<host>.+?):(?P<port>.+?)"\s+"(?P<request>.+?)"\s+"(?P<request_filename>.+?)"\s+(?P<status>\d+)\s+(?P<bytes_sent>\d+)\s+"(?P<http_referer>.+?)"\s+"(?P<http_user_agent>.+?)"\s+"(?P<http_cookie>.+?)"\s+(?P<upstream_addr>(?:-|((.+:\d+)(\,\ )?){1,}|.+?))\s+(?P<request_time>(?:-|[\d\.]+))\s+(?P<upstream_response_time>(?:-|((\d+\.\d+)(\,\ )?){1,}))\s+(?P<ssl_cipher>.+?)\s+(?P<upstream_cache_status>.+?)\s+"(?P<accept_encoding>.+?)"\s+"(?P<sent_http_x_nginx_request_id>.+?)"\s+"(?P<http_accept_encoding>.+?)"\s+"(?P<http_x_cdn_location>.+?)"$`

func isOrdinaryRequest(reqLine string) bool {
	if !(strings.HasPrefix(reqLine, "GET") || strings.HasPrefix(reqLine, "POST")) {
		return false
	}

	//nolint:S1003
	return strings.Index(reqLine, "/ping") == -1 &&
		//nolint:S1003
		strings.Index(reqLine, "/ok.html") == -1 &&
		//nolint:S1003
		strings.Index(reqLine, "/admin?action") == -1 &&
		//nolint:S1003
		strings.Index(reqLine, "/awacs-balancer-health-check") == -1
}

func parseLog(data string, cnt *counter, logger *log.Logger) {
	reg := regexp.MustCompile(lineMatcher)

	for _, line := range strings.Split(data, "\n") {
		groups := reg.FindAllStringSubmatch(line, -1)

		if len(groups) == 0 {
			continue
		}

		cnt.mutex.Lock()

		switch groups[0][23] {
		case "HIT":
			cnt.hit += 1
		case "MISS":
			cnt.miss += 1
		case "BYPASS":
			cnt.bypass += 1
		case "EXPIRED":
			cnt.expired += 1
		case "STALE":
			cnt.stale += 1
		case "UPDATING":
			cnt.updating += 1
		case "REVALIDATED":
			cnt.revalidated += 1
		default:
			cnt.other += 1
		}

		respStatus, errCode := strconv.Atoi(groups[0][8])
		respSize, err := strconv.Atoi(groups[0][9])

		if errCode == nil && err == nil && respStatus == 200 && respSize == 0 && isOrdinaryRequest(groups[0][6]) {
			cnt.zeroLength += 1
			logger.Printf("Zero length path: %s\n", groups[0][6])
		}

		if groups[0][18] != "-" {
			s3Retry := strings.Count(groups[0][18], ",")

			switch s3Retry {
			case 0:
			case 1:
				cnt.s3RetryOne += 1
			case 2:
				cnt.s3RetryTwo += 1
			case 3:
				cnt.s3RetryThree += 1
			default:
				cnt.s3RetryMax += 1
			}
		}

		cnt.mutex.Unlock()
	}
}

func main() {

	logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)

	port := flag.Int("port", 31337, "HTTP port for stats")

	flag.StringVar(&logPath, "log", "/logs/current-nginx-cache-access-8888.log", "log path")
	flag.Parse()

	watcher, _ = fsnotify.NewWatcher()
	defer watcher.Close()

	err := watcher.Add(logPath)
	if err != nil {
		logger.Fatalf("Could not add file %s", logPath)
	}

	file, err := os.Open(logPath)
	if err != nil {
		logger.Fatalf("Could not open file %s\n", logPath)
	}
	defer file.Close()

	stat, err := file.Stat()
	if err != nil {
		logger.Fatalf("Could not get size file %s\n", logPath)
	}

	offset := stat.Size()
	mutex := &sync.Mutex{}

	var cnt counter
	cnt.mutex = &sync.Mutex{}

	go func() {
		for {
			select {
			case event := <-watcher.Events:
				if event.Op&fsnotify.Write == fsnotify.Write {
					logger.Printf("modified file: %s\n", event.Name)

					file, err := os.Open(logPath)
					if err != nil {
						logger.Fatalf("Could not open file %s\n", logPath)
					}
					defer file.Close()

					mutex.Lock()

					stat, err := file.Stat()
					if err != nil {
						logger.Fatalf("Could not get size file %s\n", logPath)
					}

					newSize := stat.Size()
					if offset > newSize {
						offset = 0
					}

					buff := make([]byte, newSize-offset)
					_, err = file.ReadAt(buff, offset)

					if err != nil {
						logger.Fatalf("Could not read file %s: %s\n", logPath, err)
					}

					offset = newSize

					mutex.Unlock()

					parseLog(string(buff), &cnt, logger)
				} else if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
					for {
						_, err := os.Stat(logPath)

						if !os.IsNotExist(err) {
							break
						}
						time.Sleep(1 * time.Second)
					}

					// Adding new watcher with the same name is not working without removing old one
					err := watcher.Remove(logPath)
					if err != nil {
						logger.Fatalf("Could not remove old watcher %s\n", logPath)
					}
					err = watcher.Add(logPath)
					if err != nil {
						logger.Fatalf("Could not add file %s\n", logPath)
					} else {
						logger.Printf("Found new file %s\n", logPath)
					}
				}
			case err := <-watcher.Errors:
				logger.Printf("inotify error: %s\n", err)
			}
		}
	}()

	r := chi.NewRouter()

	r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
		handler(w, r, &cnt, logger)
	})

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

	logger.Printf("Starting server on port %d. Path to watch %s", *port, logPath)

	logger.Fatal(srv.ListenAndServe())
}
