/*
 * ChatCLI - Command Line Interface for LLM interaction
 * Copyright (c) 2024 Edilson Freitas
 * License: Apache-2.0
 */
package cli

import (
	"context"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/diillson/chatcli/i18n"
	"github.com/diillson/chatcli/llm/client"
	"github.com/diillson/chatcli/models"
	"go.uber.org/zap"
)

// Options representa as flags suportadas pelo binário
type Options struct {
	// Geral
	Version bool // --version | -v
	Help    bool // --help | -h

	// Modo one-shot
	Prompt         string        // -p | --prompt
	Provider       string        // --provider
	Model          string        // --model
	Timeout        time.Duration // --timeout
	NoAnim         bool          // --no-anim
	Raw            bool          // --raw
	PromptFlagUsed bool          // indica se -p/--prompt foi passado explicitamente
	AgentAutoExec  bool          // --agent-auto-exec
	MaxTokens      int           // --max-tokens
	Realm          string        // --realm
	AgentID        string        // --agent-id
}

// HandleOneShotOrFatal executa o modo one-shot se solicitado (flag -p usada ou stdin presente).
// - Em caso de erro, imprime mensagem em Markdown (stderr) e faz logger.Fatal (sem fallback).
// - Retorna true se o one-shot foi tratado (com sucesso ou erro fatal). Retorna false se não foi acionado.
func (cli *ChatCLI) HandleOneShotOrFatal(ctx context.Context, opts *Options) bool {
	if !opts.PromptFlagUsed && !HasStdin() {
		return false
	}

	// Aplica overrides de provider/model
	if err := cli.ApplyOverrides(ctx, cli.manager, opts.Provider, opts.Model); err != nil {
		fmt.Fprintln(os.Stderr, i18n.T("manager.error_provider_not_supported", opts.Provider)+"\n\n"+i18n.T("oneshot.details_label")+":\n```\n"+err.Error()+"\n```")
		cli.logger.Fatal("Erro ao aplicar provider/model via flags", zap.Error(err))
	}

	// Monta input a partir de -p e/ou stdin
	input := strings.TrimSpace(opts.Prompt)
	if HasStdin() {
		b, _ := io.ReadAll(os.Stdin)
		stdinText := strings.TrimSpace(string(b))
		if input == "" {
			input = stdinText
		} else if stdinText != "" {
			input = input + "\n" + stdinText
		}
	}

	if cli.Client == nil {
		fmt.Fprintln(os.Stderr, i18n.T("oneshot.error.no_provider"))
		cli.logger.Fatal("One-shot acionado sem provedor LLM configurado")
	}

	if strings.TrimSpace(input) == "" {
		fmt.Fprintln(os.Stderr, i18n.T("oneshot.error.empty_input"))
		cli.logger.Fatal("One-shot acionado sem input (prompt vazio e sem stdin)")
	}

	ctxOne, cancelOne := context.WithTimeout(ctx, opts.Timeout)
	defer cancelOne()

	if strings.HasPrefix(input, "/agent ") || strings.HasPrefix(input, "/run ") {
		if err := cli.RunAgentOnce(ctxOne, input, opts.AgentAutoExec); err != nil {
			fmt.Fprintln(os.Stderr, i18n.T("oneshot.error.agent_failed")+"\n\n"+i18n.T("oneshot.details_label")+":\n```\n"+err.Error()+"\n```")
			cli.logger.Fatal("Erro no modo agente one-shot", zap.Error(err))
		}
	} else if strings.HasPrefix(input, "/coder ") {
		// coder one-shot (mesma experiência do modo /coder interativo)
		if err := cli.RunCoderOnce(ctxOne, input); err != nil {
			fmt.Fprintln(os.Stderr, i18n.T("oneshot.error.coder_failed")+"\n\n"+i18n.T("oneshot.details_label")+":\n```\n"+err.Error()+"\n```")
			cli.logger.Fatal("Erro no modo coder one-shot", zap.Error(err))
		}
	} else {
		if err := cli.RunOnce(ctxOne, input, opts.NoAnim, opts.Raw); err != nil {
			fmt.Fprintln(os.Stderr, i18n.T("oneshot.error.run_failed")+"\n\n"+i18n.T("oneshot.details_label")+":\n```\n"+err.Error()+"\n```")
			cli.logger.Fatal("Erro no modo one-shot", zap.Error(err))
		}
	}

	return true
}

// Detecta se há dados no stdin (pipe/arquivo ao invés de TTY).
func HasStdin() bool {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return false
	}
	// Se não for dispositivo de caractere (tty), então veio de pipe/arquivo
	return (fi.Mode() & os.ModeCharDevice) == 0
}

// PreprocessArgs normaliza o caso de -p/--prompt sem valor, convertendo para -p= / --prompt=
// Ex.: echo "msg" | chatcli -p  -> trata como prompt vazio + stdin (não quebra o flag parser)
func PreprocessArgs(args []string) []string {
	out := make([]string, 0, len(args))
	for i := 0; i < len(args); i++ {
		a := args[i]
		if a == "-p" || a == "--prompt" {
			// Se existir próximo arg e não começar com "-", mantém normal (valor presente).
			if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") && args[i+1] != "" {
				out = append(out, a)
				continue
			}
			// Sem valor explícito: força formato com "=" (string vazia)
			if a == "-p" {
				out = append(out, "-p=")
			} else {
				out = append(out, "--prompt=")
			}
			continue
		}
		out = append(out, a)
	}
	return out
}

func (cli *ChatCLI) RunOnce(ctx context.Context, input string, disableAnimation bool, rawOutput bool) error {
	userInput, additionalContext, images := cli.processSpecialCommands(ctx, input)
	images, visionDesc := cli.gateImagesForModel(ctx, images)
	additionalContext += visionDesc

	cli.history = append(cli.history, models.Message{
		Role:    "user",
		Content: userInput + additionalContext,
		Images:  images,
	})

	// Compact history if over budget. In one-shot mode we keep the status
	// output on stderr so it doesn't pollute the stdout result (which may
	// be piped into other tools).
	cfg := DefaultCompactConfig(cli.Provider, cli.Model)
	if cli.historyCompactor.NeedsCompaction(cli.history, cfg) {
		cli.historyCompactor.SetStatusCallback(func(stage CompactStage, msg string) {
			fmt.Fprintf(os.Stderr, "  %s\n", msg)
		})
		if compacted, compactErr := cli.historyCompactor.Compact(ctx, cli.history, cli.Client, cfg); compactErr == nil {
			cli.history = compacted
		}
		cli.historyCompactor.SetStatusCallback(nil)
	}

	if !disableAnimation {
		cli.animation.ShowThinkingAnimation(cli.Client.GetModelName())
	}

	effectiveMaxTokens := cli.getMaxTokensForCurrentLLM()
	aiResponse, err := cli.Client.SendPrompt(ctx, userInput+additionalContext, cli.history, effectiveMaxTokens)
	// Auto-retry on OAuth token expiration (401)
	if cli.refreshClientOnAuthError(err) {
		aiResponse, err = cli.Client.SendPrompt(ctx, userInput+additionalContext, cli.history, effectiveMaxTokens)
	}

	if !disableAnimation {
		cli.animation.StopThinkingAnimation()
	}

	if err != nil {
		return err
	}

	// Track cost for one-shot mode — prefer real API usage
	if cli.costTracker != nil {
		usage := client.GetUsageOrEstimate(cli.Client, len(userInput+additionalContext), len(aiResponse))
		cli.costTracker.RecordRealUsage(cli.Provider, cli.Model, usage)
	}

	if rawOutput {
		fmt.Println(aiResponse) // Imprime texto limpo
	} else {
		rendered := cli.renderMarkdown(aiResponse) // Imprime com cores ANSI
		fmt.Println(rendered)
	}
	return nil
}

// NewFlagSet cria um FlagSet isolado e as Options para parsing
func NewFlagSet() (*flag.FlagSet, *Options) {
	fs := flag.NewFlagSet("chatcli", flag.ContinueOnError)
	opts := &Options{}

	fs.BoolVar(&opts.Version, "version", false, "Mostra versão e sai")
	fs.BoolVar(&opts.Version, "v", false, "Mostra versão e sai (alias)")
	fs.BoolVar(&opts.Help, "help", false, "Mostra ajuda e sai")
	fs.BoolVar(&opts.Help, "h", false, "Mostra ajuda e sai (alias)")

	fs.StringVar(&opts.Prompt, "p", "", "Prompt a executar uma única vez (modo não interativo) - (alias)")
	fs.StringVar(&opts.Prompt, "prompt", "", "Prompt a executar uma única vez (modo não interativo)")
	fs.StringVar(&opts.Provider, "provider", "", "Override do provider (OPENAI, OPENAI_ASSISTANT, CLAUDEAI, BEDROCK, GOOGLEAI, XAI, ZAI, MINIMAX, MOONSHOT, STACKSPOT, OLLAMA, COPILOT, GITHUB_MODELS, OPENROUTER)")
	fs.StringVar(&opts.Model, "model", "", "Override do modelo(LLM)")
	fs.DurationVar(&opts.Timeout, "timeout", 5*time.Minute, "Timeout da chamada one-shot")
	fs.IntVar(&opts.MaxTokens, "max-tokens", 0, "Override do máximo de tokens para a resposta")
	fs.BoolVar(&opts.NoAnim, "no-anim", false, "Desabilita animações no modo one-shot")
	fs.BoolVar(&opts.Raw, "raw", false, "Desabilita formatação markdown/ANSI no output (útil para CI/CD)")
	fs.BoolVar(&opts.AgentAutoExec, "agent-auto-exec", false, "No modo agente one-shot, executa o primeiro comando sugerido automaticamente se for seguro.")

	fs.StringVar(&opts.Realm, "realm", "", "Override do realm (apenas para StackSpot)")
	fs.StringVar(&opts.AgentID, "agent-id", "", "Override do Agent ID (apenas para StackSpot)")

	return fs, opts
}

// Parse analisa os args, valida e retorna Options
func Parse(args []string) (*Options, error) {
	fs, opts := NewFlagSet()
	if err := fs.Parse(args); err != nil {
		return nil, err
	}

	// Validações simples
	if opts.Timeout <= 0 {
		return nil, fmt.Errorf("timeout inválido: deve ser > 0")
	}

	// Detectar se a flag -p/--prompt foi usada explicitamente
	used := false
	fs.Visit(func(f *flag.Flag) {
		if f.Name == "p" || f.Name == "prompt" {
			used = true
		}
	})
	opts.PromptFlagUsed = used

	return opts, nil
}
