package pgx

import (
	"context"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"unicode/utf8"
)

// NamedArgs can be used as the first argument to a query method. It will replace every '@' named placeholder with a '$'
// ordinal placeholder and construct the appropriate arguments.
//
// For example, the following two queries are equivalent:
//
//	conn.Query(ctx, "select * from widgets where foo = @foo and bar = @bar", pgx.NamedArgs{"foo": 1, "bar": 2})
//	conn.Query(ctx, "select * from widgets where foo = $1 and bar = $2", 1, 2)
//
// Named placeholders are case sensitive and must start with a letter or underscore. Subsequent characters can be
// letters, numbers, or underscores.

type NamedArgs map[string]any

// RewriteQuery implements the QueryRewriter interface.
func (na NamedArgs) RewriteQuery(ctx context.Context, conn *Conn, sql string, args []any) (newSQL string, newArgs []any, err error) {
	return rewriteQuery(na, sql, false)
}

// StrictNamedArgs can be used in the same way as NamedArgs, but provided arguments are also checked to include all
// named arguments that the sql query uses, and no extra arguments.
type StrictNamedArgs map[string]any

// RewriteQuery implements the QueryRewriter interface.
func (sna StrictNamedArgs) RewriteQuery(ctx context.Context, conn *Conn, sql string, args []any) (newSQL string, newArgs []any, err error) {
	return rewriteQuery(sna, sql, true)
}

type errorQueryRewriter struct {
	err error
}

func (r errorQueryRewriter) RewriteQuery(ctx context.Context, conn *Conn, sql string, args []any) (newSQL string, newArgs []any, err error) {
	return "", nil, r.err
}

// StructArgs converts exported fields of a struct into a QueryRewriter so it can
// be used as the first argument to a query method (e.g. "where id=@id").
//
// Field names are taken from the `db` struct tag if present. Tag values may
// include comma-separated options (e.g. `db:"id,omitempty"`). A `db:"-"` field is
// ignored. If no `db` tag is present, the Go field name is used.
//
// sa may be a struct or a pointer to a struct.
func StructArgs(sa any) QueryRewriter {
	args, err := structArgs(sa)
	if err != nil {
		return errorQueryRewriter{err: err}
	}
	return NamedArgs(args)
}

// StrictStructArgs is like StructArgs but uses StrictNamedArgs rewriting
// semantics (i.e. errors if the SQL query references missing arguments or if
// extra arguments are provided).
func StrictStructArgs(sa any) QueryRewriter {
	args, err := structArgs(sa)
	if err != nil {
		return errorQueryRewriter{err: err}
	}
	return StrictNamedArgs(args)
}

func structArgs(sa any) (map[string]any, error) {
	if sa == nil {
		return nil, fmt.Errorf("StructArgs requires a struct or pointer to struct, got nil")
	}

	v := reflect.ValueOf(sa)
	t := v.Type()

	if t.Kind() == reflect.Pointer {
		if v.IsNil() {
			return nil, fmt.Errorf("StructArgs requires a non-nil pointer to struct")
		}
		v = v.Elem()
		t = v.Type()
	}

	if t.Kind() != reflect.Struct {
		return nil, fmt.Errorf("StructArgs requires a struct or pointer to struct, got %s", t)
	}

	out := make(map[string]any, t.NumField())
	for i := 0; i < t.NumField(); i++ {
		sf := t.Field(i)

		// Ignore unexported fields.
		if sf.PkgPath != "" {
			continue
		}

		key, ok, err := dbTagKey(sf)
		if err != nil {
			return nil, err
		}
		if !ok {
			continue
		}

		if _, exists := out[key]; exists {
			return nil, fmt.Errorf("duplicate StructArgs key %q", key)
		}

		out[key] = v.Field(i).Interface()
	}

	return out, nil
}

// dbTagKey derives the named-argument key for a struct field. Tag parsing matches
// RowToStructByName* in rows.go (structTagKey, Lookup, comma options, db:"-").
// Anonymous embedded structs are skipped without flattening (unlike row scanning).
func dbTagKey(sf reflect.StructField) (key string, ok bool, err error) {
	if sf.Anonymous {
		ft := sf.Type
		if ft.Kind() == reflect.Pointer {
			ft = ft.Elem()
		}
		if ft.Kind() == reflect.Struct {
			return "", false, nil
		}
	}

	dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
	if dbTagPresent {
		dbTag, _, _ = strings.Cut(dbTag, ",")
	}
	if dbTag == "-" {
		return "", false, nil
	}
	if dbTagPresent {
		if dbTag == "" {
			return "", false, fmt.Errorf("field %s has empty `%s` tag", sf.Name, structTagKey)
		}
		return dbTag, true, nil
	}

	return sf.Name, true, nil
}

type namedArg string

type sqlLexer struct {
	src     string
	start   int
	pos     int
	nested  int // multiline comment nesting level.
	stateFn stateFn
	parts   []any

	nameToOrdinal map[namedArg]int
}

type stateFn func(*sqlLexer) stateFn

func rewriteQuery(na map[string]any, sql string, isStrict bool) (newSQL string, newArgs []any, err error) {
	l := &sqlLexer{
		src:           sql,
		stateFn:       rawState,
		nameToOrdinal: make(map[namedArg]int, len(na)),
	}

	for l.stateFn != nil {
		l.stateFn = l.stateFn(l)
	}

	sb := strings.Builder{}
	for _, p := range l.parts {
		switch p := p.(type) {
		case string:
			sb.WriteString(p)
		case namedArg:
			sb.WriteRune('$')
			sb.WriteString(strconv.Itoa(l.nameToOrdinal[p]))
		}
	}

	newArgs = make([]any, len(l.nameToOrdinal))
	for name, ordinal := range l.nameToOrdinal {
		var found bool
		newArgs[ordinal-1], found = na[string(name)]
		if isStrict && !found {
			return "", nil, fmt.Errorf("argument %s found in sql query but not present in StrictNamedArgs", name)
		}
	}

	if isStrict {
		for name := range na {
			if _, found := l.nameToOrdinal[namedArg(name)]; !found {
				return "", nil, fmt.Errorf("argument %s of StrictNamedArgs not found in sql query", name)
			}
		}
	}

	return sb.String(), newArgs, nil
}

func rawState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case 'e', 'E':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune == '\'' {
				l.pos += width
				return escapeStringState
			}
		case '\'':
			return singleQuoteState
		case '"':
			return doubleQuoteState
		case '@':
			nextRune, _ := utf8.DecodeRuneInString(l.src[l.pos:])
			if isLetter(nextRune) || nextRune == '_' {
				if l.pos-l.start > 0 {
					l.parts = append(l.parts, l.src[l.start:l.pos-width])
				}
				l.start = l.pos
				return namedArgState
			}
		case '-':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune == '-' {
				l.pos += width
				return oneLineCommentState
			}
		case '/':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune == '*' {
				l.pos += width
				return multilineCommentState
			}
		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}

func isLetter(r rune) bool {
	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}

func namedArgState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		if r == utf8.RuneError {
			if l.pos-l.start > 0 {
				na := namedArg(l.src[l.start:l.pos])
				if _, found := l.nameToOrdinal[na]; !found {
					l.nameToOrdinal[na] = len(l.nameToOrdinal) + 1
				}
				l.parts = append(l.parts, na)
				l.start = l.pos
			}
			return nil
		} else if !(isLetter(r) || (r >= '0' && r <= '9') || r == '_') {
			l.pos -= width
			na := namedArg(l.src[l.start:l.pos])
			if _, found := l.nameToOrdinal[na]; !found {
				l.nameToOrdinal[na] = len(l.nameToOrdinal) + 1
			}
			l.parts = append(l.parts, na)
			l.start = l.pos
			return rawState
		}
	}
}

func singleQuoteState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case '\'':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune != '\'' {
				return rawState
			}
			l.pos += width
		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}

func doubleQuoteState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case '"':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune != '"' {
				return rawState
			}
			l.pos += width
		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}

func escapeStringState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case '\\':
			_, width = utf8.DecodeRuneInString(l.src[l.pos:])
			l.pos += width
		case '\'':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune != '\'' {
				return rawState
			}
			l.pos += width
		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}

func oneLineCommentState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case '\\':
			_, width = utf8.DecodeRuneInString(l.src[l.pos:])
			l.pos += width
		case '\n', '\r':
			return rawState
		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}

func multilineCommentState(l *sqlLexer) stateFn {
	for {
		r, width := utf8.DecodeRuneInString(l.src[l.pos:])
		l.pos += width

		switch r {
		case '/':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune == '*' {
				l.pos += width
				l.nested++
			}
		case '*':
			nextRune, width := utf8.DecodeRuneInString(l.src[l.pos:])
			if nextRune != '/' {
				continue
			}

			l.pos += width
			if l.nested == 0 {
				return rawState
			}
			l.nested--

		case utf8.RuneError:
			if l.pos-l.start > 0 {
				l.parts = append(l.parts, l.src[l.start:l.pos])
				l.start = l.pos
			}
			return nil
		}
	}
}
