package main

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

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

type commandRunners struct {
	graph  graphbuilder.CommandRunner
	wiki   codewiki.CommandRunner
	domain graphbuilder.CommandRunner
}

func main() {
	runners := commandRunners{
		graph:  graphbuilder.RunCommand,
		wiki:   codewiki.RunCommand,
		domain: graphbuilder.RunCommand,
	}
	if err := run(context.Background(), os.Args[1:], os.Stdout, os.Stderr, runners); err != nil {
		if errors.Is(err, flag.ErrHelp) {
			return
		}
		fmt.Fprintf(os.Stderr, "agentteam-insight: %v\n", err)
		os.Exit(1)
	}
}

func run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer, runners commandRunners) error {
	if len(args) == 0 {
		printUsage(stderr)
		return fmt.Errorf("subcommand is required: graph, domain, or wiki")
	}
	subcommand := args[0]
	switch subcommand {
	case "graph":
		return runGraph(ctx, args[1:], stdout, stderr, runners.graph)
	case "wiki":
		return runWiki(ctx, args[1:], stdout, stderr, runners.wiki)
	case "domain":
		return runDomain(ctx, args[1:], stdout, stderr, runners.domain)
	case "-h", "--help", "help":
		printUsage(stderr)
		return flag.ErrHelp
	default:
		printUsage(stderr)
		return fmt.Errorf("unknown subcommand %q", subcommand)
	}
}

func printUsage(stderr io.Writer) {
	fmt.Fprintln(stderr, "Usage:")
	fmt.Fprintln(stderr, "  agentteam-insight graph --input <input-repo> [--output <output-root>] [--mode full|incremental|auto] [--jobs N] [--output-language LANG|--language LANG] [--auto-update|--no-auto-update]")
	fmt.Fprintln(stderr, "  agentteam-insight domain --input <input-repo> [--output <output-root>] [--output-language LANG] [--model <provider/model>]")
	fmt.Fprintln(stderr, "  agentteam-insight wiki --input <input-repo-or-analysis-repo> [--input <input-repo-or-analysis-repo> ...] --output <wiki-repo> [--jobs N] [--isolate-opencode-runtime] [--output-language LANG]")
}

func runDomain(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer, runner graphbuilder.CommandRunner) error {
	options, err := parseDomainOptions(args, stderr)
	if err != nil {
		return err
	}
	options.ProgressWriter = stderr
	if runner == nil {
		runner = graphbuilder.RunCommand
	}
	result, err := graphbuilder.RunDomain(ctx, options, runner)
	if err != nil {
		return err
	}
	fmt.Fprintf(stdout, "generated domain graph: nodes=%d edges=%d; graph: %s\n", result.Nodes, result.Edges, result.GraphPath)
	return nil
}

func parseDomainOptions(args []string, stderr io.Writer) (graphbuilder.Options, error) {
	flags := flag.NewFlagSet("agentteam-insight domain", flag.ContinueOnError)
	flags.SetOutput(stderr)
	inputRepo := flags.String("input", "", "path to the repository whose existing knowledge graph is used")
	outputRoot := flags.String("output", "", "project root containing .agentteam-insight/knowledge-graph.json; defaults to --input")
	openCodeBinary := flags.String("opencode", "", "opencode binary path or name; defaults to opencode from PATH")
	openCodeAgent := flags.String("opencode-agent", "", "optional OpenCode agent name")
	openCodeModel := flags.String("model", "", "optional OpenCode model in provider/model format")
	outputLanguage := flags.String("output-language", "", "optional natural language for generated domain graph text")
	taskTimeout := flags.Duration("task-timeout", 30*time.Minute, "maximum duration for the OpenCode task; 0 disables the timeout")
	var openCodeExtraArgs repeatedStringFlag
	flags.Var(&openCodeExtraArgs, "opencode-arg", "extra argument passed to opencode run; repeatable")
	flags.Usage = func() {
		fmt.Fprintln(stderr, "Usage: agentteam-insight domain --input <input-repo> [--output <output-root>] [--output-language LANG] [--model <provider/model>]")
		flags.PrintDefaults()
	}
	if err := flags.Parse(args); err != nil {
		return graphbuilder.Options{}, err
	}
	if flags.NArg() > 0 {
		return graphbuilder.Options{}, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
	}
	if strings.TrimSpace(*inputRepo) == "" {
		return graphbuilder.Options{}, fmt.Errorf("--input is required")
	}
	if *taskTimeout < 0 {
		return graphbuilder.Options{}, fmt.Errorf("--task-timeout must be >= 0")
	}
	return graphbuilder.Options{
		InputRepo: *inputRepo, OutputRoot: *outputRoot,
		OpenCodeBinary: *openCodeBinary, OpenCodeAgent: *openCodeAgent, OpenCodeModel: *openCodeModel,
		OutputLanguage: *outputLanguage, TaskTimeout: *taskTimeout,
		OpenCodeExtraArgs: append([]string(nil), openCodeExtraArgs...),
	}, nil
}

func runWiki(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer, runner codewiki.CommandRunner) error {
	options, err := parseWikiOptions(args, stderr)
	if err != nil {
		return err
	}
	options.ProgressWriter = stderr
	if runner == nil {
		runner = codewiki.RunCommand
	}
	result, err := codewiki.Generate(ctx, options, runner)
	if err != nil {
		return err
	}
	fmt.Fprintf(stdout, "generated code wiki with %d OpenCode tasks; wiki: %s\n", result.TasksRun, result.WikiDir)
	return nil
}

func parseWikiOptions(args []string, stderr io.Writer) (codewiki.Options, error) {
	flags := flag.NewFlagSet("agentteam-insight wiki", flag.ContinueOnError)
	flags.SetOutput(stderr)

	var inputRoots repeatedStringFlag
	flags.Var(&inputRoots, "input", "path to source repository or code-analysis artifact repository; repeatable")
	outputRoot := flags.String("output", "", "path to the repository that receives generated wiki pages")
	jobs := flags.Int("jobs", 0, "maximum number of concurrent OpenCode wiki page workers; defaults to 1, or 4 with --isolate-opencode-runtime")
	openCodeBinary := flags.String("opencode", "", "opencode binary path or name; defaults to opencode from PATH")
	openCodeAgent := flags.String("opencode-agent", "", "optional OpenCode agent name")
	openCodeModel := flags.String("model", "", "optional OpenCode model in provider/model format")
	outputLanguage := flags.String("output-language", "", "optional natural language for generated wiki content")
	isolateOpenCodeRuntime := flags.Bool("isolate-opencode-runtime", false, "run each OpenCode task with isolated XDG data/state directories")
	taskTimeout := flags.Duration("task-timeout", 30*time.Minute, "maximum duration for the OpenCode wiki task; 0 disables the timeout")
	var openCodeExtraArgs repeatedStringFlag
	flags.Var(&openCodeExtraArgs, "opencode-arg", "extra argument passed to opencode run; repeatable")
	flags.Usage = func() {
		fmt.Fprintln(stderr, "Usage: agentteam-insight wiki --input <input-repo-or-analysis-repo> [--input <input-repo-or-analysis-repo> ...] --output <wiki-repo> [--jobs N] [--isolate-opencode-runtime] [--output-language LANG]")
		flags.PrintDefaults()
	}

	if err := flags.Parse(args); err != nil {
		return codewiki.Options{}, err
	}
	if flags.NArg() > 0 {
		return codewiki.Options{}, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
	}
	if len(inputRoots) == 0 {
		return codewiki.Options{}, fmt.Errorf("--input is required")
	}
	if strings.TrimSpace(*outputRoot) == "" {
		return codewiki.Options{}, fmt.Errorf("--output is required")
	}

	return codewiki.Options{
		InputRoots:             append([]string(nil), inputRoots...),
		OutputRoot:             *outputRoot,
		OpenCodeBinary:         *openCodeBinary,
		OpenCodeAgent:          *openCodeAgent,
		OpenCodeModel:          *openCodeModel,
		OutputLanguage:         *outputLanguage,
		OpenCodeExtraArgs:      append([]string(nil), openCodeExtraArgs...),
		IsolateOpenCodeRuntime: *isolateOpenCodeRuntime,
		Jobs:                   *jobs,
		TaskTimeout:            *taskTimeout,
	}, nil
}

func runGraph(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer, runner graphbuilder.CommandRunner) error {
	options, err := parseGraphOptions(args, stderr)
	if err != nil {
		return err
	}
	options.ProgressWriter = stderr
	options.PromptReader = os.Stdin
	if runner == nil {
		runner = graphbuilder.RunCommand
	}
	result, err := graphbuilder.Run(ctx, options, runner)
	if err != nil {
		return err
	}
	fmt.Fprintf(stdout, "graph builder %s in %s mode: analyzed %d files, nodes=%d edges=%d layers=%d tour=%d; graph: %s\n", result.Action, result.Mode, result.AnalyzedFiles, result.Nodes, result.Edges, result.Layers, result.TourSteps, result.GraphPath)
	return nil
}

func parseGraphOptions(args []string, stderr io.Writer) (graphbuilder.Options, error) {
	flags := flag.NewFlagSet("agentteam-insight graph", flag.ContinueOnError)
	flags.SetOutput(stderr)

	inputRepo := flags.String("input", "", "path to the repository to build an insight graph from")
	outputRoot := flags.String("output", "", "path to the project root that receives graph artifacts; defaults to --input")
	mode := flags.String("mode", string(graphbuilder.ModeAuto), "analysis mode: full, incremental, or auto")
	llmMode := flags.String("llm", string(graphbuilder.LLMModeAuto), "LLM graph enrichment: auto or off")
	jobs := flags.Int("jobs", 0, "maximum number of concurrent file-analyzer workers; defaults to 5 in LLM auto mode")
	openCodeBinary := flags.String("opencode", "", "opencode binary path or name; defaults to opencode from PATH")
	openCodeAgent := flags.String("opencode-agent", "", "optional OpenCode agent name for later agent enhancement stages")
	openCodeModel := flags.String("model", "", "optional OpenCode model in provider/model format")
	outputLanguage := flags.String("output-language", "", "optional natural language for generated graph text")
	language := flags.String("language", "", "alias for --output-language")
	autoUpdate := flags.Bool("auto-update", false, "persist autoUpdate=true in graph builder config; analysis still proceeds")
	noAutoUpdate := flags.Bool("no-auto-update", false, "persist autoUpdate=false in graph builder config; analysis still proceeds")
	noPrompt := flags.Bool("no-prompt", false, "auto-accept graph ignore confirmation")
	review := flags.Bool("review", false, "enable later LLM graph-reviewer stage")
	isolateOpenCodeRuntime := flags.Bool("isolate-opencode-runtime", false, "run each OpenCode task with isolated XDG data/state directories; automatic for LLM auto file-analyzer batches")
	taskTimeout := flags.Duration("task-timeout", 30*time.Minute, "maximum duration for each OpenCode task; 0 disables the timeout")
	benchmarkPath := flags.String("benchmark", "", "write a bounded JSON timing report to this path")
	var openCodeExtraArgs repeatedStringFlag
	flags.Var(&openCodeExtraArgs, "opencode-arg", "extra argument passed to opencode run; repeatable")
	flags.Usage = func() {
		fmt.Fprintln(stderr, "Usage: agentteam-insight graph --input <input-repo> [--output <output-root>] [--mode full|incremental|auto] [--jobs N] [--output-language LANG|--language LANG] [--auto-update|--no-auto-update]")
		flags.PrintDefaults()
	}

	if err := flags.Parse(args); err != nil {
		return graphbuilder.Options{}, err
	}
	if flags.NArg() > 0 {
		return graphbuilder.Options{}, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
	}
	if strings.TrimSpace(*inputRepo) == "" {
		return graphbuilder.Options{}, fmt.Errorf("--input is required")
	}
	graphMode := graphbuilder.Mode(strings.TrimSpace(*mode))
	switch graphMode {
	case graphbuilder.ModeFull, graphbuilder.ModeIncremental, graphbuilder.ModeAuto:
	default:
		return graphbuilder.Options{}, fmt.Errorf("--mode must be one of full, incremental, or auto")
	}
	graphLLMMode := graphbuilder.LLMMode(strings.TrimSpace(*llmMode))
	switch graphLLMMode {
	case graphbuilder.LLMModeAuto, graphbuilder.LLMModeOff:
	default:
		return graphbuilder.Options{}, fmt.Errorf("--llm must be one of auto or off")
	}
	if *autoUpdate && *noAutoUpdate {
		return graphbuilder.Options{}, fmt.Errorf("--auto-update and --no-auto-update are mutually exclusive")
	}
	resolvedLanguage := *outputLanguage
	if strings.TrimSpace(*language) != "" {
		if strings.TrimSpace(resolvedLanguage) != "" && strings.TrimSpace(resolvedLanguage) != strings.TrimSpace(*language) {
			return graphbuilder.Options{}, fmt.Errorf("--language and --output-language must not conflict")
		}
		resolvedLanguage = *language
	}
	var autoUpdateValue *bool
	if *autoUpdate || *noAutoUpdate {
		value := *autoUpdate
		autoUpdateValue = &value
	}
	return graphbuilder.Options{
		InputRepo:              *inputRepo,
		OutputRoot:             *outputRoot,
		Mode:                   graphMode,
		LLMMode:                graphLLMMode,
		Jobs:                   *jobs,
		OpenCodeBinary:         *openCodeBinary,
		OpenCodeAgent:          *openCodeAgent,
		OpenCodeModel:          *openCodeModel,
		OutputLanguage:         resolvedLanguage,
		AutoUpdate:             autoUpdateValue,
		NoPrompt:               *noPrompt,
		OpenCodeExtraArgs:      append([]string(nil), openCodeExtraArgs...),
		Review:                 *review,
		IsolateOpenCodeRuntime: *isolateOpenCodeRuntime,
		TaskTimeout:            *taskTimeout,
		BenchmarkPath:          *benchmarkPath,
	}, nil
}

type repeatedStringFlag []string

func (flag *repeatedStringFlag) String() string {
	return strings.Join(*flag, ",")
}

func (flag *repeatedStringFlag) Set(value string) error {
	*flag = append(*flag, value)
	return nil
}
