package logging

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

type stackFrame struct {
	File string
	Line int
	Func string
}

var stackFileLinePattern = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)

type sourceFunction struct {
	name               string
	source             string
	startLine, endLine int
}

type sourceFileCache struct {
	size    int64
	modTime time.Time
	items   []sourceFunction
}

var sourceCache = struct {
	sync.RWMutex
	files map[string]sourceFileCache
}{files: make(map[string]sourceFileCache)}

func finalApplicationCaller(stack string) (stackFrame, bool) {
	if stack == "" {
		return stackFrame{}, false
	}
	functionName := ""
	for _, raw := range strings.Split(stack, "\n") {
		line := strings.TrimSpace(raw)
		if line == "" {
			continue
		}
		matches := stackFileLinePattern.FindStringSubmatch(line)
		if matches == nil {
			functionName = line
			continue
		}
		lineNumber, _ := strconv.Atoi(matches[2])
		if skipStackFile(matches[1]) {
			functionName = ""
			continue
		}
		return stackFrame{File: matches[1], Line: lineNumber, Func: functionName}, true
	}
	return stackFrame{}, false
}

func skipStackFile(filename string) bool {
	normalized := strings.ReplaceAll(filename, "\\", "/")
	for _, marker := range []string{
		"/go/pkg/mod/",
		"/go.uber.org/",
		"/gorm.io/",
		"/internal/logging/",
		"/internal/server/handler/",
		"/internal/server/middleware/",
		"/internal/server/router/",
		"/internal/server/httpx/",
	} {
		if strings.Contains(normalized, marker) {
			return true
		}
	}
	return strings.Contains(normalized, "/src/") &&
		(strings.Contains(normalized, "/go/go") || strings.Contains(normalized, "/go/src/"))
}

func functionSourceAt(filename string, line int) (name, source string, startLine, endLine int, err error) {
	filename = filepath.Clean(filename)
	info, err := os.Stat(filename)
	if err != nil {
		return "", "", 0, 0, fmt.Errorf("stat file failed: %w", err)
	}
	sourceCache.RLock()
	cached, found := sourceCache.files[filename]
	sourceCache.RUnlock()
	if found && cached.size == info.Size() && cached.modTime.Equal(info.ModTime()) {
		return sourceFunctionAtLine(cached.items, line, filename)
	}
	content, err := os.ReadFile(filename)
	if err != nil {
		return "", "", 0, 0, fmt.Errorf("read file failed: %w", err)
	}
	files := token.NewFileSet()
	parsed, err := parser.ParseFile(files, filename, content, parser.ParseComments)
	if err != nil {
		return "", "", 0, 0, fmt.Errorf("parse file failed: %w", err)
	}
	sourceCache.Lock()
	sourceCache.files[filename] = sourceFileCache{size: info.Size(), modTime: info.ModTime(), items: allFunctions(parsed, files, content)}
	cached = sourceCache.files[filename]
	sourceCache.Unlock()
	return sourceFunctionAtLine(cached.items, line, filename)
}

func allFunctions(parsed *ast.File, files *token.FileSet, content []byte) []sourceFunction {
	items := make([]sourceFunction, 0)
	ast.Inspect(parsed, func(node ast.Node) bool {
		declaration, ok := node.(*ast.FuncDecl)
		if !ok {
			return true
		}
		start := files.Position(declaration.Pos()).Line
		end := files.Position(declaration.End()).Line
		startOffset := files.Position(declaration.Pos()).Offset
		endOffset := files.Position(declaration.End()).Offset
		if startOffset >= 0 && endOffset <= len(content) && startOffset < endOffset {
			items = append(items, sourceFunction{name: declaration.Name.Name, source: string(content[startOffset:endOffset]), startLine: start, endLine: end})
		}
		return true
	})
	return items
}

func sourceFunctionAtLine(items []sourceFunction, line int, filename string) (string, string, int, int, error) {
	for _, item := range items {
		if line >= item.startLine && line <= item.endLine {
			return item.name, item.source, item.startLine, item.endLine, nil
		}
	}
	return "", "", 0, 0, fmt.Errorf("no function encloses line %d in %s", line, filename)
}
