// Package nginx provides a recursive descent parser for nginx configuration files.
package nginx

import (
	"fmt"
	"os"
	"path/filepath"
)

// Directive represents a single nginx directive.
type Directive struct {
	Name  string      // directive name (e.g., "server", "listen", "proxy_pass")
	Args  []string    // directive arguments
	Block []Directive // child directives for block directives (e.g., server { ... })
	Line  int         // line number in source file
	File  string      // source file path (for include tracking)
}

// NginxConfig represents a parsed nginx configuration.
type NginxConfig struct {
	Directives []Directive
}

// ParseError represents a parse error with file and line information.
type ParseError struct {
	File    string
	Line    int
	Message string
}

// Error 返回解析错误的字符串表示。
func (e *ParseError) Error() string {
	return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Message)
}

type parser struct {
	input           []byte
	pos             int
	line            int
	file            string
	includeStack    map[string]bool
	depth           int
	extraDirectives []Directive // directives injected by include expansion
}

const maxDepth = 10

// ParseString parses an nginx configuration from a string.
func ParseString(content string) (*NginxConfig, error) {
	p := &parser{
		input:        []byte(content),
		pos:          0,
		line:         1,
		file:         "<string>",
		includeStack: map[string]bool{},
	}
	directives, err := p.parseDirectives()
	if err != nil {
		return nil, err
	}
	return &NginxConfig{Directives: directives}, nil
}

// ParseFile parses an nginx configuration file, handling include directives.
func ParseFile(path string) (*NginxConfig, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return nil, &ParseError{File: path, Line: 1, Message: fmt.Sprintf("resolve absolute path: %v", err)}
	}
	resolved, err := filepath.EvalSymlinks(absPath)
	if err != nil {
		return nil, &ParseError{File: path, Line: 1, Message: fmt.Sprintf("resolve symlinks: %v", err)}
	}
	return parseFileWithStack(resolved, map[string]bool{resolved: true}, 0)
}

func parseFileWithStack(path string, includeStack map[string]bool, depth int) (*NginxConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, &ParseError{File: path, Line: 1, Message: fmt.Sprintf("read file: %v", err)}
	}

	newStack := make(map[string]bool, len(includeStack)+1)
	for k := range includeStack {
		newStack[k] = true
	}
	newStack[path] = true

	p := &parser{
		input:        data,
		pos:          0,
		line:         1,
		file:         path,
		includeStack: newStack,
		depth:        depth,
	}
	directives, err := p.parseDirectives()
	if err != nil {
		return nil, err
	}
	return &NginxConfig{Directives: directives}, nil
}

func (p *parser) errorf(msg string, args ...any) error {
	return &ParseError{File: p.file, Line: p.line, Message: fmt.Sprintf(msg, args...)}
}

func (p *parser) parseDirectives() ([]Directive, error) {
	var directives []Directive
	for {
		p.skipWhitespaceAndComments()
		if p.pos >= len(p.input) {
			break
		}
		if p.input[p.pos] == '}' {
			break
		}
		d, err := p.parseDirective()
		if err != nil {
			return nil, err
		}
		// handleInclude may produce zero directives (glob no match) or
		// multiple directives (include expands to several files).
		if d == nil {
			continue
		}
		directives = append(directives, *d)

		// Drain any extra directives injected by include expansion.
		directives = append(directives, p.extraDirectives...)
		p.extraDirectives = nil
	}
	return directives, nil
}

func (p *parser) parseDirective() (*Directive, error) {
	p.skipWhitespaceAndComments()

	line := p.line
	name, err := p.readToken()
	if err != nil {
		return nil, err
	}
	if name == "" {
		return nil, p.errorf("expected directive name")
	}

	d := &Directive{
		Name: name,
		Line: line,
		File: p.file,
	}

	// Read arguments until ; or {
	for {
		p.skipWhitespaceAndComments()
		if p.pos >= len(p.input) {
			return nil, p.errorf("unexpected end of input, expected ';' or '{'")
		}

		ch := p.input[p.pos]
		if ch == ';' {
			p.pos++
			break
		}
		if ch == '{' {
			p.pos++
			block, err := p.parseDirectives()
			if err != nil {
				return nil, err
			}
			p.skipWhitespaceAndComments()
			if p.pos >= len(p.input) || p.input[p.pos] != '}' {
				return nil, p.errorf("expected '}'")
			}
			p.pos++
			d.Block = block
			break
		}

		arg, err := p.readToken()
		if err != nil {
			return nil, err
		}
		if arg == "" {
			return nil, p.errorf("unexpected character %q", p.input[p.pos])
		}
		d.Args = append(d.Args, arg)
	}

	// Handle include directive: replace with expanded content.
	if d.Name == "include" && len(d.Args) > 0 {
		return p.handleInclude(d.Args[0])
	}

	return d, nil
}

func (p *parser) handleInclude(pattern string) (*Directive, error) {
	if p.depth >= maxDepth {
		return nil, p.errorf("include depth exceeds maximum of %d", maxDepth)
	}

	var fullPattern string
	if filepath.IsAbs(pattern) {
		fullPattern = pattern
	} else {
		baseDir := filepath.Dir(p.file)
		fullPattern = filepath.Join(baseDir, pattern)
	}

	matches, err := filepath.Glob(fullPattern)
	if err != nil {
		return nil, p.errorf("invalid include pattern %q: %v", pattern, err)
	}

	if len(matches) == 0 {
		// If the pattern contains no glob metacharacters, it's a literal
		// file path that should exist. Return an error if it doesn't.
		if !isGlobPattern(pattern) {
			return nil, p.errorf("include file not found: %s", fullPattern)
		}
		// Glob pattern with no matches — silently skip (matches nginx behavior).
		return nil, nil
	}

	var allDirectives []Directive
	for _, match := range matches {
		resolved, err := filepath.EvalSymlinks(match)
		if err != nil {
			return nil, p.errorf("resolve symlinks for %q: %v", match, err)
		}
		if p.includeStack[resolved] {
			return nil, p.errorf("circular include detected: %s", resolved)
		}

		cfg, err := parseFileWithStack(resolved, p.includeStack, p.depth+1)
		if err != nil {
			return nil, err
		}
		allDirectives = append(allDirectives, cfg.Directives...)
	}

	if len(allDirectives) == 0 {
		return nil, nil
	}

	// Return the first directive; stash the rest for parseDirectives to drain.
	p.extraDirectives = allDirectives[1:]
	return &allDirectives[0], nil
}

func (p *parser) skipWhitespaceAndComments() {
	for p.pos < len(p.input) {
		ch := p.input[p.pos]
		if ch == ' ' || ch == '\t' || ch == '\r' {
			p.pos++
			continue
		}
		if ch == '\n' {
			p.pos++
			p.line++
			continue
		}
		if ch == '#' {
			for p.pos < len(p.input) && p.input[p.pos] != '\n' {
				p.pos++
			}
			continue
		}
		break
	}
}

func (p *parser) readToken() (string, error) {
	if p.pos >= len(p.input) {
		return "", nil
	}

	ch := p.input[p.pos]

	if ch == '"' || ch == '\'' {
		token, err := p.readQuotedString(ch)
		if err != nil {
			return "", err
		}
		// Return a special marker for empty quoted strings to distinguish
		// from the empty string returned when encountering special chars.
		if token == "" {
			return `""`, nil
		}
		return token, nil
	}

	if ch == '{' || ch == '}' || ch == ';' {
		return "", nil
	}

	start := p.pos
	for p.pos < len(p.input) {
		ch = p.input[p.pos]
		if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' ||
			ch == '{' || ch == '}' || ch == ';' || ch == '#' ||
			ch == '"' || ch == '\'' {
			break
		}
		p.pos++
	}
	if p.pos == start {
		return "", nil
	}
	return string(p.input[start:p.pos]), nil
}

func (p *parser) readQuotedString(quote byte) (string, error) {
	p.pos++ // skip opening quote
	var buf []byte
	for p.pos < len(p.input) {
		ch := p.input[p.pos]
		if ch == '\\' {
			p.pos++
			if p.pos >= len(p.input) {
				return "", p.errorf("unterminated escape in quoted string")
			}
			buf = append(buf, p.input[p.pos])
			p.pos++
			continue
		}
		if ch == quote {
			p.pos++ // skip closing quote
			return string(buf), nil
		}
		if ch == '\n' {
			p.line++
		}
		buf = append(buf, ch)
		p.pos++
	}
	return "", p.errorf("unterminated quoted string")
}

// isGlobPattern returns true if the path contains glob metacharacters.
func isGlobPattern(path string) bool {
	for i := 0; i < len(path); i++ {
		ch := path[i]
		if ch == '*' || ch == '?' || ch == '[' {
			return true
		}
	}
	return false
}
