package graphql

import (
	"context"
	"encoding/base64"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/vektah/gqlparser/v2/ast"
	"github.com/vektah/gqlparser/v2/parser"
)

type gqlVariablesContextKey struct{}

type gqlConnectionArgs struct {
	After     string
	Before    string
	First     *int
	Last      *int
	Skip      int
	ItemTypes []string
	Since     *time.Time
}

func gqlVariables(ctx context.Context) map[string]any {
	variables, _ := ctx.Value(gqlVariablesContextKey{}).(map[string]any)
	return variables
}

func connectionArgsForField(ctx context.Context, query, parentName, fieldName string) gqlConnectionArgs {
	doc, err := parser.ParseQuery(&ast.Source{Input: query})
	if err != nil || doc == nil {
		return gqlConnectionArgs{}
	}
	variables := gqlVariables(ctx)
	var found *ast.Field
	var visit func(ast.SelectionSet, []string)
	visit = func(selections ast.SelectionSet, path []string) {
		if found != nil {
			return
		}
		for _, selection := range selections {
			switch node := selection.(type) {
			case *ast.Field:
				if node.Name == fieldName && (parentName == "" || containsString(path, parentName)) {
					found = node
					return
				}
				visit(node.SelectionSet, append(path, node.Name))
			case *ast.InlineFragment:
				visit(node.SelectionSet, path)
			case *ast.FragmentSpread:
				if fragment := doc.Fragments.ForName(node.Name); fragment != nil {
					visit(fragment.SelectionSet, path)
				}
			}
		}
	}
	for _, operation := range doc.Operations {
		visit(operation.SelectionSet, nil)
	}
	if found == nil {
		return gqlConnectionArgs{}
	}
	args := gqlConnectionArgs{}
	for _, argument := range found.Arguments {
		value, valueErr := argument.Value.Value(variables)
		if valueErr != nil {
			continue
		}
		switch argument.Name {
		case "after":
			args.After, _ = value.(string)
		case "before":
			args.Before, _ = value.(string)
		case "first":
			args.First = gqlIntPointer(value)
		case "last":
			args.Last = gqlIntPointer(value)
		case "skip":
			if parsed := gqlIntPointer(value); parsed != nil {
				args.Skip = *parsed
			}
		case "itemTypes":
			args.ItemTypes = gqlStrings(value)
		case "since":
			if raw, ok := value.(string); ok {
				if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
					args.Since = &parsed
				}
			}
		}
	}
	return args
}

func containsString(values []string, wanted string) bool {
	for _, value := range values {
		if value == wanted {
			return true
		}
	}
	return false
}

func gqlIntPointer(value any) *int {
	var parsed int
	switch typed := value.(type) {
	case int:
		parsed = typed
	case int64:
		parsed = int(typed)
	case float64:
		parsed = int(typed)
	default:
		return nil
	}
	if parsed < 0 {
		parsed = 0
	}
	return &parsed
}

func gqlStrings(value any) []string {
	switch typed := value.(type) {
	case []string:
		return typed
	case []any:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			if text, ok := item.(string); ok {
				result = append(result, text)
			}
		}
		return result
	default:
		return nil
	}
}

func gqlConnWithArgs(nodes []any, args gqlConnectionArgs) map[string]any {
	total := len(nodes)
	start, end := 0, total
	if index, ok := decodeConnectionCursor(args.After); ok && index+1 > start {
		start = min(index+1, total)
	}
	if index, ok := decodeConnectionCursor(args.Before); ok && index < end {
		end = max(start, index)
	}
	if args.Skip > 0 {
		start = min(end, start+args.Skip)
	}
	if args.First != nil && end-start > *args.First {
		end = start + *args.First
	}
	if args.Last != nil && end-start > *args.Last {
		start = end - *args.Last
	}
	pageNodes := append([]any(nil), nodes[start:end]...)
	edges := make([]any, len(pageNodes))
	for i, node := range pageNodes {
		edges[i] = map[string]any{"cursor": encodeConnectionCursor(start + i), "node": node}
	}
	var startCursor, endCursor any
	if len(pageNodes) > 0 {
		startCursor = encodeConnectionCursor(start)
		endCursor = encodeConnectionCursor(end - 1)
	}
	return map[string]any{
		"totalCount": total,
		"nodes":      pageNodes,
		"edges":      edges,
		"pageInfo": map[string]any{
			"hasNextPage": end < total, "hasPreviousPage": start > 0,
			"endCursor": endCursor, "startCursor": startCursor,
		},
	}
}

func encodeConnectionCursor(index int) string {
	return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("arrayconnection:%d", index)))
}

func decodeConnectionCursor(cursor string) (int, bool) {
	decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(cursor))
	if err != nil {
		return 0, false
	}
	const prefix = "arrayconnection:"
	if !strings.HasPrefix(string(decoded), prefix) {
		return 0, false
	}
	index, err := strconv.Atoi(strings.TrimPrefix(string(decoded), prefix))
	return index, err == nil && index >= 0
}
