package diagnostics

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"

	"github.com/urfave/cli/v2"

	"github.com/ledgerwatch/erigon/turbo/logging"
)

func SetupLogsAccess(ctx *cli.Context, metricsMux *http.ServeMux) {
	dirPath := ctx.String(logging.LogDirPathFlag.Name)
	if dirPath == "" {
		datadir := ctx.String("datadir")
		if datadir != "" {
			dirPath = filepath.Join(datadir, "logs")
		}
	}
	if dirPath == "" {
		return
	}
	metricsMux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		writeLogsList(w, dirPath)
	})
	metricsMux.HandleFunc("/logs/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		writeLogsRead(w, r, dirPath)
	})
}

func writeLogsList(w http.ResponseWriter, dirPath string) {
	entries, err := os.ReadDir(dirPath)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to list directory %s: %v", dirPath, err), http.StatusInternalServerError)
		return
	}

	infos := make([]fs.FileInfo, 0, len(entries))

	for _, entry := range entries {
		fileInfo, err := os.Stat(filepath.Join(dirPath, entry.Name()))
		if err != nil {
			http.Error(w, fmt.Sprintf("Can't stat file %s: %v", entry.Name(), err), http.StatusInternalServerError)
			return
		}
		if fileInfo.IsDir() {
			continue
		}
		infos = append(infos, fileInfo)
	}

	type file struct {
		Name string `json:"name"`
		Size int64  `json:"size"`
	}

	files := make([]file, len(infos))

	for _, fileInfo := range infos {
		files = append(files, file{Name: fileInfo.Name(), Size: fileInfo.Size()})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(files)
}

func writeLogsRead(w http.ResponseWriter, r *http.Request, dirPath string) {
	file := path.Base(r.URL.Path)

	if file == "/" || file == "." {
		http.Error(w, "file is required - specify the name of log file to read", http.StatusBadRequest)
		return
	}

	offset, err := offsetValue(r.URL.Query())

	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	fileInfo, err := os.Stat(filepath.Join(dirPath, file))

	if err != nil {
		http.Error(w, fmt.Sprintf("Can't stat file %s: %v", file, err), http.StatusInternalServerError)
		return
	}

	if fileInfo.IsDir() {
		http.Error(w, fmt.Sprintf("%s is a directory, needs to be a file", file), http.StatusInternalServerError)
		return
	}

	if offset > fileInfo.Size() {
		http.Error(w, fmt.Sprintf("offset %d must not be greater than this file size %d", offset, fileInfo.Size()), http.StatusBadRequest)
		return
	}

	f, err := os.Open(filepath.Join(dirPath, file))

	if err != nil {
		http.Error(w, fmt.Sprintf("Can't opening file %s: %v\n", file, err), http.StatusInternalServerError)
		return
	}

	limit, err := limitValue(r.URL.Query(), fileInfo.Size())

	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	buf := make([]byte, limit)

	if _, err := f.Seek(offset, 0); err != nil {
		http.Error(w, fmt.Sprintf("seek failed for file: %s to %d: %v", file, offset, err), http.StatusInternalServerError)
		return
	}

	var n int
	var readTotal int

	for n, err = f.Read(buf[readTotal:]); err == nil && readTotal < len(buf); n, err = f.Read(buf[readTotal:]) {
		readTotal += n
	}

	if err != nil && !errors.Is(err, io.EOF) {
		http.Error(w, fmt.Sprintf("Reading failed for: %s at %d: %v\n", file, readTotal, err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Length", strconv.FormatInt(int64(readTotal), 10))
	w.Header().Set("X-Offset", strconv.FormatInt(offset, 10))
	w.Header().Set("X-Limit", strconv.FormatInt(limit, 10))
	w.Header().Set("X-Size", strconv.FormatInt(fileInfo.Size(), 10))
	w.Write(buf[:readTotal])
}

func limitValue(values url.Values, def int64) (int64, error) {
	limitStr := values.Get("limit")

	var limit int64
	var err error

	if limitStr == "" {
		limit = def
	} else {
		limit, err = strconv.ParseInt(limitStr, 10, 64)
	}

	if err != nil {
		return 0, fmt.Errorf("limit %s is not a int64 number: %v", limitStr, err)
	}

	return limit, nil
}

func offsetValue(values url.Values) (int64, error) {

	offsetStr := values.Get("offset")

	var offset int64
	var err error

	if offsetStr != "" {
		offset, err = strconv.ParseInt(offsetStr, 10, 64)

		if err != nil {
			return 0, fmt.Errorf("offset %s is not a int64 number: %v", offsetStr, err)
		}
	}

	if offset < 0 {
		return 0, fmt.Errorf("offset %d must be non-negative", offset)
	}

	return offset, nil
}