package graphbuilder

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/semantic"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/opencoderuntime"
)

func prepareSemanticInvocation(runnerOptions Options, layoutRoot string, invocation semantic.Invocation) (workDir, outputPath string, env []string, err error) {
	workDir = filepath.Join(layoutRoot, "semantic-workspace")
	if err = os.MkdirAll(workDir, 0o755); err != nil {
		return "", "", nil, fmt.Errorf("create semantic workdir: %w", err)
	}
	if err = writeOpenCodeTaskConfig(workDir); err != nil {
		return "", "", nil, err
	}
	if err = writeSemanticInput(workDir, invocation); err != nil {
		return "", "", nil, err
	}
	outputPath = filepath.Join(workDir, filepath.FromSlash(invocation.OutputFile))
	if err = os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
		return "", "", nil, fmt.Errorf("create semantic output directory: %w", err)
	}
	removeErr := os.Remove(outputPath)
	if removeErr != nil && !os.IsNotExist(removeErr) {
		return "", "", nil, fmt.Errorf("reset semantic output: %w", removeErr)
	}
	err = nil
	if runnerOptions.IsolateOpenCodeRuntime {
		env, err = opencoderuntime.Prepare(workDir)
	}
	return workDir, outputPath, env, err
}

func writeSemanticInput(workDir string, invocation semantic.Invocation) error {
	if invocation.InputFile == "" && len(invocation.Input) == 0 {
		return nil
	}
	if invocation.InputFile == "" {
		return fmt.Errorf("semantic input file is required")
	}
	path := filepath.Join(workDir, filepath.FromSlash(invocation.InputFile))
	relative, err := filepath.Rel(workDir, path)
	if err != nil || relative == ".." || filepath.IsAbs(relative) || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
		return fmt.Errorf("semantic input file must stay inside the workdir")
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create semantic input directory: %w", err)
	}
	if err := os.WriteFile(path, invocation.Input, 0o600); err != nil {
		return fmt.Errorf("write semantic input: %w", err)
	}
	return nil
}

func (runner opencodeFileAnalyzerRunner) Invoke(ctx context.Context, invocation semantic.Invocation) ([]byte, error) {
	workDir, outputPath, env, err := prepareSemanticInvocation(runner.options, runner.layout.TmpRoot, invocation)
	if err != nil {
		return nil, err
	}
	intent := fmt.Sprintf("graph-builder-%s:%d", invocation.Stage, invocation.Attempt)
	args := []string{"run", "--title", intent}
	// The semantic prompts embed the pinned UA agent definitions, so they must
	// not inherit a file-analyzer agent selected for the batch phase.
	if runner.options.OpenCodeAgent != "" && runner.options.OpenCodeAgent != "file-analyzer" {
		args = append(args, "--agent", runner.options.OpenCodeAgent)
	}
	if runner.options.OpenCodeModel != "" {
		args = append(args, "--model", runner.options.OpenCodeModel)
	}
	args = append(args, runner.options.OpenCodeExtraArgs...)
	args = append(args, "--dir", workDir, "--", invocation.Prompt)
	started := time.Now()
	result, runErr := runCommandWithTaskTimeout(ctx, runner.options.TaskTimeout, Command{Dir: workDir, Name: runner.options.OpenCodeBinary, Args: args, Env: env}, runner.runner)
	if runErr != nil {
		runner.options.benchmark.semanticCall(benchmarkSemanticCall{Stage: string(invocation.Stage), Attempt: invocation.Attempt, InputBytes: len(invocation.Prompt) + len(invocation.Input), DurationMS: elapsedMS(started)})
		return nil, formatCommandError(runner.options.OpenCodeBinary, result, runErr)
	}
	output, outputErr := semanticOutput(outputPath, result.Stdout)
	runner.options.benchmark.semanticCall(benchmarkSemanticCall{
		Stage: string(invocation.Stage), Attempt: invocation.Attempt, InputBytes: len(invocation.Prompt) + len(invocation.Input), OutputBytes: len(output), DurationMS: elapsedMS(started), Success: outputErr == nil,
	})
	return output, outputErr
}

func semanticOutput(outputPath string, fallback []byte) ([]byte, error) {
	if data, err := os.ReadFile(outputPath); err == nil && len(bytes.TrimSpace(data)) > 0 {
		return data, nil
	}
	return fallback, nil
}
