package querycodec

import (
	"regexp"
	"strings"
	"text/scanner"
	"time"
)

// TokenType represents the type of a token
type TokenType int

const (
	TokenError TokenType = iota
	TokenEOF
	TokenIdentifier
	TokenKeyword
	TokenString
	TokenNumber
	TokenDateTime
	TokenRelativeDate
	TokenOperator
	TokenPunctuation
)

var soqlDateTimeLiteralPattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d))?`)

// Token represents a lexical token
type Token struct {
	Type    TokenType
	Literal string
	Pos     int
}

// Lexer tokenizes SOQL queries
type Lexer struct {
	s   scanner.Scanner
	buf string
}

func NewLexer(input string) *Lexer {
	l := &Lexer{buf: input}
	l.s.Init(strings.NewReader(input))
	// Disable default string/char scanning to handle SOQL quotes manually
	l.s.Mode = scanner.ScanIdents | scanner.ScanFloats | scanner.ScanInts
	return l
}

func (l *Lexer) NextToken() Token {
	// 1. Skip Whitespace manually to safely Peek
	l.skipWhitespace()

	pos := l.s.Pos().Offset
	ch := l.s.Peek()

	if ch == scanner.EOF {
		return Token{Type: TokenEOF, Literal: "", Pos: pos}
	}

	// 2. Handle Quotes (Strings)
	if ch == '\'' {
		return l.scanString('\'')
	}
	if ch == '"' {
		return l.scanString('"')
	}

	if ch >= '0' && ch <= '9' {
		if tok, ok := l.scanDateTimeLiteral(pos); ok {
			return tok
		}
	}

	// 3. Handle Multi-char Operators (!=, <=, >=)
	if ch == '!' {
		if l.isNext('=') {
			return Token{Type: TokenOperator, Literal: "!=", Pos: pos}
		}
	}
	if ch == '<' || ch == '>' {
		op := string(ch)
		l.s.Next() // consume first char
		if l.s.Peek() == '=' {
			l.s.Next() // consume '='
			return Token{Type: TokenOperator, Literal: op + "=", Pos: pos}
		}
		return Token{Type: TokenOperator, Literal: op, Pos: pos}
	}

	// 4. Scan Token
	tok := l.s.Scan()
	lit := l.s.TokenText()

	// Handle scanner.EOF case if Scan returns it
	if tok == scanner.EOF {
		return Token{Type: TokenEOF, Literal: "", Pos: pos}
	}

	if tok == scanner.Float || tok == scanner.Int {
		return Token{Type: TokenNumber, Literal: lit, Pos: pos}
	}

	if tok == scanner.Ident {
		name := strings.ToUpper(lit)
		if name == "TODAY" || name == "THIS_YEAR" || name == "LAST_N_DAYS" {
			if name == "LAST_N_DAYS" && l.s.Peek() == ':' {
				l.s.Next()
				name += ":"
				for ch := l.s.Peek(); ch >= '0' && ch <= '9'; ch = l.s.Peek() {
					name += string(l.s.Next())
				}
			}
			return Token{Type: TokenRelativeDate, Literal: name, Pos: pos}
		}
		if isKeyword(lit) {
			return Token{Type: TokenKeyword, Literal: strings.ToUpper(lit), Pos: pos}
		}
		if isOperator(lit) {
			return Token{Type: TokenOperator, Literal: strings.ToUpper(lit), Pos: pos}
		}
		// Handle "test@example.com" sticking together if scanner parses it?
		// Default scanner separates at '@'.
		// We rely on Parser to recombine or Lexer to be smarter?
		// For now, let's stick to standard behavior.
		return Token{Type: TokenIdentifier, Literal: lit, Pos: pos}
	}

	// Punctuation or Operators
	if isOperator(lit) {
		return Token{Type: TokenOperator, Literal: lit, Pos: pos}
	}

	return Token{Type: TokenPunctuation, Literal: lit, Pos: pos}
}

func (l *Lexer) scanDateTimeLiteral(pos int) (Token, bool) {
	remaining := l.buf[pos:]
	lit := soqlDateTimeLiteralPattern.FindString(remaining)
	if lit == "" || !isDateTimeLiteralBoundary(remaining[len(lit):]) {
		return Token{}, false
	}

	for i := 0; i < len(lit); i++ {
		l.s.Next()
	}

	if !isValidDateTimeLiteral(lit) {
		return Token{Type: TokenError, Literal: lit, Pos: pos}, true
	}

	return Token{Type: TokenDateTime, Literal: lit, Pos: pos}, true
}

func isValidDateTimeLiteral(lit string) bool {
	if strings.Contains(lit, "T") {
		_, err := time.Parse(time.RFC3339Nano, lit)
		return err == nil
	}
	_, err := time.Parse("2006-01-02", lit)
	return err == nil
}

func isDateTimeLiteralBoundary(remaining string) bool {
	if remaining == "" {
		return true
	}
	switch remaining[0] {
	case ' ', '\t', '\n', '\r', ')', ',', ']':
		return true
	default:
		return false
	}
}

func (l *Lexer) skipWhitespace() {
	for {
		ch := l.s.Peek()
		if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
			l.s.Next() // consume
			continue
		}
		break
	}
}

func (l *Lexer) scanString(quote rune) Token {
	pos := l.s.Position.Offset
	l.s.Next() // consume opening quote
	var sb strings.Builder
	for {
		ch := l.s.Next()
		if ch == quote {
			// Check for double-quote escaping (SQL standard: 'It''s')
			if l.s.Peek() == quote {
				l.s.Next() // consume 2nd quote
				sb.WriteRune(quote)
				continue
			}
			break
		}
		if ch == scanner.EOF {
			break
		}
		if ch == '\\' {
			// Handle backslash escape (SOQL supports this too?)
			// Yes, 'O\'Reilly'
			next := l.s.Next()
			if next != scanner.EOF {
				sb.WriteRune(next)
			}
			continue
		}
		sb.WriteRune(ch)
	}
	return Token{Type: TokenString, Literal: sb.String(), Pos: pos}
}

func (l *Lexer) isNext(expected rune) bool {
	l.s.Next() // consume current
	if l.s.Peek() == expected {
		l.s.Next() // consume expected
		return true
	}
	// Warning: We consumed 'current' (e.g. '!') but not expected.
	// This is slightly destructive if not expected?
	// But for '!' in SOQL, '!x' is invalid anyway unless '!='.
	return false
}

func isKeyword(lit string) bool {
	upper := strings.ToUpper(lit)
	switch upper {
	case "SELECT", "FROM", "WHERE", "LIMIT", "OFFSET", "ORDER", "BY", "ASC", "DESC", "NULL", "TRUE", "FALSE", "GROUP", "HAVING":
		return true
	}
	return false
}

func isOperator(lit string) bool {
	upper := strings.ToUpper(lit)
	switch upper {
	case "AND", "OR", "IN", "LIKE", "NOT", "=", "!=", "<", ">", "<=", ">=":
		return true
	}
	return false
}
