// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

package eval

import (
	"context"
	"fmt"
	"strconv"
	"strings"
)

// phase represents a single step in the evaluation pipeline.
type phase struct {
	name    string
	execute func(ctx context.Context, input string, opts *Options) (string, error)
	enabled func(opts *Options) bool // nil = always run
}

// pipeline is an ordered sequence of evaluation phases.
type pipeline struct {
	phases []phase
}

// execute runs all enabled phases in order on the input string.
func (p *pipeline) execute(ctx context.Context, input string, opts *Options) (string, error) {
	if opts.NoExpansion {
		return input, nil
	}
	value := input
	if opts.EscapeDollar {
		ctx, value = withDollarEscapes(ctx, input)
	}
	for _, ph := range p.phases {
		if ph.enabled != nil && !ph.enabled(opts) {
			continue
		}
		var err error
		value, err = ph.execute(ctx, value, opts)
		if err != nil {
			return "", fmt.Errorf("phase %s: %w", ph.name, err)
		}
	}
	return value, nil
}

// defaultPipeline is the standard evaluation pipeline used by String().
// Phase order: quoted-refs → variables → substitute → shell-expand
var defaultPipeline = &pipeline{
	phases: []phase{
		{
			name:    "quoted-refs",
			execute: expandQuotedRefs,
		},
		{
			name:    "variables",
			execute: expandAllVariables,
		},
		{
			name:    "substitute",
			execute: substitutePhase,
			enabled: func(opts *Options) bool { return opts.Substitute },
		},
		{
			name:    "shell-expand",
			execute: shellExpandPhase,
			enabled: func(opts *Options) bool { return opts.ExpandEnv },
		},
		{
			name: "unescape-dollar",
			execute: func(ctx context.Context, input string, _ *Options) (string, error) {
				return unescapeDollars(ctx, input), nil
			},
			enabled: func(opts *Options) bool { return opts.EscapeDollar },
		},
	},
}

// expandQuotedRefs handles quoted references like "${FOO.bar}" and "${VAR}" within
// double quotes. Resolved values are re-quoted so surrounding JSON stays valid.
func expandQuotedRefs(ctx context.Context, input string, opts *Options) (string, error) {
	r := newResolver(ctx, opts)
	result := reQuotedJSONRef.ReplaceAllStringFunc(input, func(match string) string {
		ref := match[3 : len(match)-2] // Strip leading "$ { and trailing } "

		var val string
		var ok bool

		if dotIdx := strings.Index(ref, "."); dotIdx >= 0 {
			val, ok = r.resolveReference(ctx, ref[:dotIdx], ref[dotIdx:])
		} else {
			val, ok = r.resolve(ref)
		}

		if ok {
			return strconv.Quote(val)
		}
		return match
	})
	return result, nil
}

// expandAllVariables resolves JSON path references, step property references,
// and simple $VAR/${VAR} patterns from all variable sources.
func expandAllVariables(ctx context.Context, input string, opts *Options) (string, error) {
	return expandVariables(ctx, input, opts), nil
}

// substitutePhase runs backtick command substitution.
func substitutePhase(ctx context.Context, input string, _ *Options) (string, error) {
	return substituteCommandsWithContext(ctx, input)
}

// regexExpandEnv performs regex-based variable expansion. When ExpandOS is true,
// os.LookupEnv is available as a fallback; otherwise only scoped entries are used.
func regexExpandEnv(ctx context.Context, input string, opts *Options) string {
	if opts.ExpandOS {
		return ExpandEnvContext(ctx, input)
	}
	return expandEnvScopeOnly(ctx, input)
}

// shellExpandPhase performs shell-style variable expansion.
// When ExpandShell is true (default), uses selective POSIX expansion via mvdan.cc/sh;
// defined variables with POSIX operators are expanded, undefined variables are preserved.
// When ExpandShell is false or POSIX expansion fails, falls back to regex-based expansion.
func shellExpandPhase(ctx context.Context, input string, opts *Options) (string, error) {
	if !opts.ExpandShell {
		return regexExpandEnv(ctx, input, opts), nil
	}
	expanded, err := expandWithShellContext(ctx, input, opts)
	if err != nil {
		return regexExpandEnv(ctx, input, opts), nil
	}
	return expanded, nil
}

// evalStringValue applies variable expansion, substitution, and env expansion to a string.
// Used by StringFields and Object for struct/map field processing.
func evalStringValue(ctx context.Context, value string, opts *Options) (string, error) {
	if opts.EscapeDollar {
		ctx, value = withDollarEscapes(ctx, value)
	}
	value = expandVariables(ctx, value, opts)
	if opts.Substitute {
		var err error
		value, err = substituteCommandsWithContext(ctx, value)
		if err != nil {
			return "", err
		}
	}
	if opts.ExpandEnv {
		value = regexExpandEnv(ctx, value, opts)
	}
	return unescapeDollars(ctx, value), nil
}
