package compiler

import (
	"bytes"
	"fmt"
	"strings"

	"c65gm/internal/utils"

	"go.starlark.net/lib/math"
	"go.starlark.net/starlark"
)

// executeScript runs a Starlark script and returns the output lines.
// If isLibrary is true, the script is executed at top level (no _main wrapper)
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
	// Join script lines
	scriptText := strings.Join(scriptLines, "\n")

	// Expand |varname| -> actual variable names
	scriptText = expandVariables(scriptText, ctx)

	var finalScript string
	if isLibrary {
		// LIBRARY: execute at top level so defs become globals
		finalScript = scriptText
	} else {
		// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
		finalScript = "def _main():\n"
		for _, line := range strings.Split(scriptText, "\n") {
			finalScript += "    " + line + "\n"
		}
		finalScript += "_main()\n"
	}

	// Capture print output
	var output bytes.Buffer
	thread := &starlark.Thread{
		Print: func(_ *starlark.Thread, msg string) {
			output.WriteString(msg)
			output.WriteString("\n")
		},
	}

	// Set execution limit (prevent infinite loops)
	thread.SetMaxExecutionSteps(1000000) // 1M steps

	// Build predeclared: math module + library globals
	predeclared := starlark.StringDict{
		"math": math.Module,
	}
	for k, v := range ctx.ScriptLibraryGlobals {
		predeclared[k] = v
	}

	// Execute
	globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
	if err != nil {
		return nil, err
	}

	// For LIBRARY: persist new globals (functions, variables defined at top level)
	if isLibrary {
		for k, v := range globals {
			ctx.ScriptLibraryGlobals[k] = v
		}
	}

	// Split output into lines for assembly
	outputStr := output.String()
	if outputStr == "" {
		return []string{}, nil
	}
	lines := strings.Split(strings.TrimRight(outputStr, "\n"), "\n")
	return lines, nil
}

// expandVariables replaces |varname| with expanded variable names from symbol table
func expandVariables(text string, ctx *CompilerContext) string {
	result := text
	for {
		start := strings.IndexByte(result, '|')
		if start == -1 {
			break
		}
		end := strings.IndexByte(result[start+1:], '|')
		if end == -1 {
			break // unclosed, let script fail
		}
		end += start + 1

		varName := result[start+1 : end]
		expandedName := ctx.SymbolTable.ExpandName(varName, ctx.CurrentScope())
		result = result[:start] + expandedName + result[end+1:]
	}
	return result
}

// ExecuteMacro executes a named macro with the given arguments and returns output lines
func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]string, error) {
	// Look up the macro
	macro, ok := ctx.ScriptMacros[macroName]
	if !ok {
		return nil, fmt.Errorf("undefined macro: %s", macroName)
	}

	// Check argument count
	if len(args) != len(macro.Params) {
		return nil, fmt.Errorf("macro %s expects %d arguments, got %d", macroName, len(macro.Params), len(args))
	}

	// Evaluate arguments and build parameter bindings
	paramBindings := make(starlark.StringDict)
	for i, arg := range args {
		val, err := evaluateMacroArg(arg, ctx)
		if err != nil {
			return nil, fmt.Errorf("error evaluating argument %d for macro %s: %w", i+1, macroName, err)
		}
		paramBindings[macro.Params[i]] = val
	}

	// Build the script: wrap macro body in a function with parameters bound
	scriptText := strings.Join(macro.Body, "\n")

	// Wrap in function for control flow support
	finalScript := "def _macro():\n"
	for _, line := range strings.Split(scriptText, "\n") {
		finalScript += "    " + line + "\n"
	}
	finalScript += "_macro()\n"

	// Capture print output
	var output bytes.Buffer
	thread := &starlark.Thread{
		Print: func(_ *starlark.Thread, msg string) {
			output.WriteString(msg)
			output.WriteString("\n")
		},
	}

	// Set execution limit
	thread.SetMaxExecutionSteps(1000000)

	// Build predeclared: math + library globals + parameter bindings
	predeclared := starlark.StringDict{
		"math": math.Module,
	}
	for k, v := range ctx.ScriptLibraryGlobals {
		predeclared[k] = v
	}
	for k, v := range paramBindings {
		predeclared[k] = v
	}

	// Execute
	_, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
	if err != nil {
		return nil, err
	}

	// Split output into lines
	outputStr := output.String()
	if outputStr == "" {
		return []string{}, nil
	}

	// Expand |varname| -> actual variable names in the OUTPUT
	// This happens at call site, so local variables are resolved using caller's scope
	outputStr = expandVariables(outputStr, ctx)

	return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
}

// evaluateMacroArg evaluates a macro argument, returning either an int or string Starlark value
func evaluateMacroArg(arg string, ctx *CompilerContext) (starlark.Value, error) {
	arg = strings.TrimSpace(arg)

	// Create lookup function for constants
	lookup := ctx.SymbolTable.ConstantLookupFunc(nil)

	// Try to evaluate as expression (number, constant, arithmetic)
	val, err := utils.EvaluateExpression(arg, lookup)
	if err == nil {
		// Successfully evaluated as integer
		return starlark.MakeInt64(val), nil
	}

	// If it's a valid identifier, treat as label (string)
	if utils.ValidateIdentifier(arg) {
		return starlark.String(arg), nil
	}

	// Otherwise, error
	return nil, fmt.Errorf("invalid macro argument: %s (not a valid expression or identifier)", arg)
}

// ParseMacroInvocation parses "@name(arg1, arg2, ...)" and returns name and args
func ParseMacroInvocation(invocation string) (string, []string, error) {
	invocation = strings.TrimSpace(invocation)

	// Must start with @
	if !strings.HasPrefix(invocation, "@") {
		return "", nil, fmt.Errorf("macro invocation must start with @")
	}
	rest := invocation[1:]

	// Find opening paren
	parenStart := strings.IndexByte(rest, '(')
	if parenStart == -1 {
		return "", nil, fmt.Errorf("macro invocation missing '(': %s", invocation)
	}

	// Find closing paren
	parenEnd := strings.LastIndexByte(rest, ')')
	if parenEnd == -1 || parenEnd < parenStart {
		return "", nil, fmt.Errorf("macro invocation missing ')': %s", invocation)
	}

	name := strings.TrimSpace(rest[:parenStart])
	if name == "" {
		return "", nil, fmt.Errorf("macro name is empty")
	}

	// Parse arguments (handle nested parens for expressions)
	argStr := strings.TrimSpace(rest[parenStart+1 : parenEnd])
	args, err := splitMacroArgs(argStr)
	if err != nil {
		return "", nil, err
	}

	return name, args, nil
}

// splitMacroArgs splits comma-separated arguments, respecting parentheses
func splitMacroArgs(argStr string) ([]string, error) {
	if argStr == "" {
		return []string{}, nil
	}

	var args []string
	var current strings.Builder
	parenDepth := 0

	for _, ch := range argStr {
		if ch == '(' {
			parenDepth++
			current.WriteRune(ch)
		} else if ch == ')' {
			parenDepth--
			if parenDepth < 0 {
				return nil, fmt.Errorf("unbalanced parentheses in macro arguments")
			}
			current.WriteRune(ch)
		} else if ch == ',' && parenDepth == 0 {
			args = append(args, strings.TrimSpace(current.String()))
			current.Reset()
		} else {
			current.WriteRune(ch)
		}
	}

	if parenDepth != 0 {
		return nil, fmt.Errorf("unbalanced parentheses in macro arguments")
	}

	// Add final argument
	if current.Len() > 0 {
		args = append(args, strings.TrimSpace(current.String()))
	}

	return args, nil
}
