package ssh_config

import (
	"fmt"
	"strings"
	"unicode"
)

type sshParser struct {
	flow          chan token
	config        *Config
	tokensBuffer  []token
	currentTable  []string
	seenTableKeys []string
	// /etc/ssh parser or local parser - used to find the default for relative
	// filepaths in the Include directive
	system bool
	depth  uint8
}

type sshParserStateFn func() sshParserStateFn

// Formats and panics an error message based on a token
func (p *sshParser) raiseErrorf(tok *token, msg string) {
	// TODO this format is ugly
	panic(tok.Position.String() + ": " + msg)
}

func (p *sshParser) raiseError(tok *token, err error) {
	if err == ErrDepthExceeded {
		panic(err)
	}
	// TODO this format is ugly
	panic(tok.Position.String() + ": " + err.Error())
}

func (p *sshParser) run() {
	for state := p.parseStart; state != nil; {
		state = state()
	}
}

func (p *sshParser) peek() *token {
	if len(p.tokensBuffer) != 0 {
		return &(p.tokensBuffer[0])
	}

	tok, ok := <-p.flow
	if !ok {
		return nil
	}
	p.tokensBuffer = append(p.tokensBuffer, tok)
	return &tok
}

func (p *sshParser) getToken() *token {
	if len(p.tokensBuffer) != 0 {
		tok := p.tokensBuffer[0]
		p.tokensBuffer = p.tokensBuffer[1:]
		return &tok
	}
	tok, ok := <-p.flow
	if !ok {
		return nil
	}
	return &tok
}

func (p *sshParser) parseStart() sshParserStateFn {
	tok := p.peek()

	// end of stream, parsing is finished
	if tok == nil {
		return nil
	}

	switch tok.typ {
	case tokenComment, tokenEmptyLine:
		return p.parseComment
	case tokenKey:
		return p.parseKV
	case tokenEOF:
		return nil
	default:
		p.raiseErrorf(tok, fmt.Sprintf("unexpected token %q\n", tok))
	}
	return nil
}

func (p *sshParser) parseKV() sshParserStateFn {
	key := p.getToken()
	hasEquals := false
	val := p.getToken()
	if val.typ == tokenEquals {
		hasEquals = true
		val = p.getToken()
	}
	comment := ""
	tok := p.peek()
	if tok == nil {
		tok = &token{typ: tokenEOF}
	}
	if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
		tok = p.getToken()
		comment = tok.val
	}
	if strings.ToLower(key.val) == "match" {
		return p.parseMatch(val, hasEquals, comment)
	}
	if strings.ToLower(key.val) == "host" {
		strPatterns := strings.Split(val.val, " ")
		patterns := make([]*Pattern, 0)
		for i := range strPatterns {
			if strPatterns[i] == "" {
				continue
			}
			pat, err := NewPattern(strPatterns[i])
			if err != nil {
				p.raiseErrorf(val, fmt.Sprintf("Invalid host pattern: %v", err))
				return nil
			}
			patterns = append(patterns, pat)
		}
		// val.val at this point could be e.g. "example.com       "
		hostval := strings.TrimRightFunc(val.val, unicode.IsSpace)
		spaceBeforeComment := val.val[len(hostval):]
		val.val = hostval
		p.config.Hosts = append(p.config.Hosts, &Host{
			Patterns:           patterns,
			Nodes:              make([]Node, 0),
			EOLComment:         comment,
			spaceBeforeComment: spaceBeforeComment,
			hasEquals:          hasEquals,
		})
		return p.parseStart
	}
	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
	if strings.ToLower(key.val) == "include" {
		inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1)
		if err == ErrDepthExceeded {
			p.raiseError(val, err)
			return nil
		}
		if err != nil {
			p.raiseErrorf(val, fmt.Sprintf("Error parsing Include directive: %v", err))
			return nil
		}
		lastHost.Nodes = append(lastHost.Nodes, inc)
		return p.parseStart
	}
	shortval := strings.TrimRightFunc(val.val, unicode.IsSpace)
	spaceAfterValue := val.val[len(shortval):]
	kv := &KV{
		Key:             key.val,
		Value:           shortval,
		spaceAfterValue: spaceAfterValue,
		Comment:         comment,
		hasEquals:       hasEquals,
		leadingSpace:    key.Position.Col - 1,
		position:        key.Position,
	}
	lastHost.Nodes = append(lastHost.Nodes, kv)
	return p.parseStart
}

func (p *sshParser) parseMatch(val *token, hasEquals bool, comment string) sshParserStateFn {
	// val.val contains everything after "Match ", e.g. "Host *.example.com"
	// or "all".
	trimmed := strings.TrimRightFunc(val.val, unicode.IsSpace)
	spaceBeforeComment := val.val[len(trimmed):]
	fields := strings.Fields(trimmed)
	if len(fields) == 0 {
		p.raiseErrorf(val, "ssh_config: Match directive requires at least one criterion")
		return nil
	}
	criterion := strings.ToLower(fields[0])

	switch criterion {
	case "all":
		// "Match all" is equivalent to "Host *" — matches everything.
		p.config.Hosts = append(p.config.Hosts, &Host{
			Patterns:           []*Pattern{matchAll},
			Nodes:              make([]Node, 0),
			EOLComment:         comment,
			spaceBeforeComment: spaceBeforeComment,
			hasEquals:          hasEquals,
			isMatch:            true,
			matchKeyword:       fields[0], // preserve original case
		})
		return p.parseStart

	case "host":
		patterns := make([]*Pattern, 0)
		for _, s := range fields[1:] {
			if s == "" {
				continue
			}
			pat, err := NewPattern(s)
			if err != nil {
				p.raiseErrorf(val, fmt.Sprintf("Invalid host pattern: %v", err))
				return nil
			}
			patterns = append(patterns, pat)
		}
		if len(patterns) == 0 {
			p.raiseErrorf(val, "ssh_config: Match Host requires at least one pattern")
			return nil
		}
		p.config.Hosts = append(p.config.Hosts, &Host{
			Patterns:           patterns,
			Nodes:              make([]Node, 0),
			EOLComment:         comment,
			spaceBeforeComment: spaceBeforeComment,
			hasEquals:          hasEquals,
			isMatch:            true,
			matchKeyword:       fields[0], // preserve original case
		})
		return p.parseStart

	case "exec":
		// Match Exec runs arbitrary commands. Supporting it would allow
		// untrusted SSH config files to execute code on the parsing
		// machine. Reject it explicitly.
		p.raiseErrorf(val, "ssh_config: Match Exec is not supported")
		return nil

	default:
		p.raiseErrorf(val, fmt.Sprintf("ssh_config: unsupported Match criterion %q", criterion))
		return nil
	}
}

func (p *sshParser) parseComment() sshParserStateFn {
	comment := p.getToken()
	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
	lastHost.Nodes = append(lastHost.Nodes, &Empty{
		Comment: comment.val,
		// account for the "#" as well
		leadingSpace: comment.Position.Col - 2,
		position:     comment.Position,
	})
	return p.parseStart
}

func parseSSH(flow chan token, system bool, depth uint8) *Config {
	// Ensure we consume tokens to completion even if parser exits early
	defer func() {
		for range flow {
		}
	}()

	result := newConfig()
	result.position = Position{1, 1}
	parser := &sshParser{
		flow:          flow,
		config:        result,
		tokensBuffer:  make([]token, 0),
		currentTable:  make([]string, 0),
		seenTableKeys: make([]string, 0),
		system:        system,
		depth:         depth,
	}
	parser.run()
	return result
}
