package llm

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"github.com/enmanuel/agents/internal/config"
	coretypes "github.com/enmanuel/agents/pkg/llm"
)

const (
	defaultClaudeBinary  = "claude"
	defaultClaudeTimeout = 5 * time.Minute
)

// claudeJSONOutput represents the JSON output from `claude -p --output-format json`.
type claudeJSONOutput struct {
	Type         string           `json:"type"`
	Subtype      string           `json:"subtype"`
	CostUSD      float64          `json:"cost_usd"`
	IsError      bool             `json:"is_error"`
	Duration     float64          `json:"duration_api_ms"`
	NumTurns     int              `json:"num_turns"`
	Result       string           `json:"result"`
	SessionID    string           `json:"session_id"`
	TotalCost    float64          `json:"total_cost_usd"`
	Usage        claudeUsage      `json:"usage"`
	ContentBlock []claudeContent  `json:"content"`
}

type claudeUsage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

type claudeContent struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

// NewClaudeCodeComplete creates a CompleteFunc that executes `claude -p` as a subprocess.
func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes.CompleteFunc {
	binary := cfg.Binary
	if binary == "" {
		binary = defaultClaudeBinary
	}

	timeout := cfg.Timeout
	if timeout <= 0 {
		timeout = defaultClaudeTimeout
	}

	// Resolve working directory once at init time.
	workDir := resolveWorkDir(cfg.WorkingDir, log)

	return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
		ctx, cancel := context.WithTimeout(ctx, timeout)
		defer cancel()

		args := buildClaudeArgs(cfg, req)

		prompt := flattenMessages(req.Messages)

		log.Debug("claude_code_exec",
			"binary", binary,
			"args", strings.Join(args, " "),
			"prompt_len", len(prompt),
			"working_dir", workDir,
		)

		cmd := exec.CommandContext(ctx, binary, args...)
		if workDir != "" {
			cmd.Dir = workDir
		}
		// Build clean env: inherit parent but remove ANTHROPIC_API_KEY
		// so claude uses its own OAuth auth instead of a potentially invalid key.
		cmd.Env = filterEnv(os.Environ(), "ANTHROPIC_API_KEY")
		cmd.Stdin = strings.NewReader(prompt)

		// Create a new process group so we can kill claude + all its children.
		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
		// Override the default cancel behavior: kill the entire process group
		// instead of just the main process, preventing orphaned child processes.
		cmd.Cancel = func() error {
			if cmd.Process != nil {
				pgid := cmd.Process.Pid
				log.Info("killing claude-code process group", "pgid", pgid)
				// Negative PID = kill entire process group
				return syscall.Kill(-pgid, syscall.SIGKILL)
			}
			return nil
		}

		var stdout, stderr bytes.Buffer
		cmd.Stdout = &stdout
		cmd.Stderr = &stderr

		start := time.Now()
		err := cmd.Run()
		elapsed := time.Since(start)

		// Ensure the process group is fully dead after Run returns,
		// even if cmd.Run() returned without triggering Cancel (normal exit).
		if cmd.Process != nil {
			_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
		}

		log.Debug("claude_code_done",
			"elapsed_ms", elapsed.Milliseconds(),
			"stdout_len", stdout.Len(),
			"stderr_len", stderr.Len(),
			"exit_err", err,
		)

		return parseClaudeOutput(stdout.Bytes(), stderr.Bytes(), err, elapsed, log)
	}
}

// resolveWorkDir determines the working directory for the claude subprocess.
// If configured is empty, it creates a temporary directory to avoid inheriting the launcher's CWD.
// If configured is non-empty, it ensures the directory exists.
func resolveWorkDir(configured string, log *slog.Logger) string {
	if configured == "" {
		tmp, err := os.MkdirTemp("", "claude-agent-*")
		if err != nil {
			log.Error("claude-code: failed to create temp working dir", "err", err)
			return "" // Fall through — cmd.Dir will remain empty (inherits CWD).
		}
		log.Warn("claude-code working_dir is empty, using temporary directory",
			"dir", tmp,
		)
		return tmp
	}

	// Ensure configured directory exists.
	if err := os.MkdirAll(configured, 0o755); err != nil {
		log.Error("claude-code: failed to create working dir", "dir", configured, "err", err)
	}
	return configured
}

// buildClaudeArgs constructs the CLI arguments for claude -p.
func buildClaudeArgs(cfg config.ClaudeCodeCfg, req coretypes.CompletionRequest) []string {
	args := []string{"--print", "--output-format", "json"}

	if req.SystemPrompt != "" {
		args = append(args, "--system-prompt", req.SystemPrompt)
	}

	if cfg.DisableTools {
		args = append(args, "--tools", "")
	} else {
		if len(cfg.AllowedTools) > 0 {
			args = append(args, "--allowedTools")
			args = append(args, cfg.AllowedTools...)
		}

		if len(cfg.DisallowedTools) > 0 {
			args = append(args, "--disallowedTools")
			args = append(args, cfg.DisallowedTools...)
		}
	}

	if cfg.PermissionMode != "" {
		args = append(args, "--permission-mode", cfg.PermissionMode)
	}

	if cfg.Model != "" {
		args = append(args, "--model", cfg.Model)
	}

	if cfg.FallbackModel != "" {
		args = append(args, "--fallback-model", cfg.FallbackModel)
	}

	if cfg.SessionID != "" {
		args = append(args, "--session-id", cfg.SessionID)
	}

	for _, dir := range cfg.AddDirs {
		args = append(args, "--add-dir", dir)
	}

	return args
}

// flattenMessages converts a conversation history into a single text prompt for stdin.
func flattenMessages(msgs []coretypes.Message) string {
	var b strings.Builder
	for _, m := range msgs {
		switch m.Role {
		case coretypes.RoleUser:
			fmt.Fprintf(&b, "User: %s\n\n", m.Content)
		case coretypes.RoleAssistant:
			fmt.Fprintf(&b, "Assistant: %s\n\n", m.Content)
		case coretypes.RoleTool:
			fmt.Fprintf(&b, "Tool result: %s\n\n", m.Content)
		}
	}
	return b.String()
}

// parseClaudeOutput parses the JSON output from `claude -p --output-format json`.
func parseClaudeOutput(
	stdout, stderr []byte,
	execErr error,
	elapsed time.Duration,
	log *slog.Logger,
) (coretypes.CompletionResponse, error) {
	// If the process failed and there's no stdout, report the error
	if execErr != nil && len(stdout) == 0 {
		errMsg := string(stderr)
		if errMsg == "" {
			errMsg = execErr.Error()
		}
		return coretypes.CompletionResponse{}, fmt.Errorf("claude-code process failed: %s", errMsg)
	}

	// Parse JSON output
	var output claudeJSONOutput
	if err := json.Unmarshal(stdout, &output); err != nil {
		// Fall back to treating stdout as plain text
		log.Warn("claude_code_json_parse_failed", "err", err, "stdout_len", len(stdout))
		return coretypes.CompletionResponse{
			Content:      strings.TrimSpace(string(stdout)),
			FinishReason: "stop",
		}, nil
	}

	if output.IsError {
		return coretypes.CompletionResponse{}, fmt.Errorf("claude-code error: %s", output.Result)
	}

	// Extract text from result field or content blocks
	content := output.Result
	if content == "" && len(output.ContentBlock) > 0 {
		var parts []string
		for _, block := range output.ContentBlock {
			if block.Type == "text" && block.Text != "" {
				parts = append(parts, block.Text)
			}
		}
		content = strings.Join(parts, "\n")
	}

	finishReason := "stop"
	if execErr != nil {
		finishReason = "error"
	}

	log.Info("claude_code_response",
		"content_len", len(content),
		"input_tokens", output.Usage.InputTokens,
		"output_tokens", output.Usage.OutputTokens,
		"num_turns", output.NumTurns,
		"cost_usd", output.TotalCost,
		"elapsed_ms", elapsed.Milliseconds(),
	)

	return coretypes.CompletionResponse{
		Content: content,
		Usage: coretypes.TokenUsage{
			InputTokens:  output.Usage.InputTokens,
			OutputTokens: output.Usage.OutputTokens,
			TotalTokens:  output.Usage.InputTokens + output.Usage.OutputTokens,
		},
		FinishReason: finishReason,
	}, nil
}

// filterEnv returns a copy of environ with the named keys removed.
func filterEnv(environ []string, keys ...string) []string {
	out := make([]string, 0, len(environ))
	for _, e := range environ {
		skip := false
		for _, k := range keys {
			if strings.HasPrefix(e, k+"=") {
				skip = true
				break
			}
		}
		if !skip {
			out = append(out, e)
		}
	}
	return out
}
