package semantic

import (
	"context"
	"errors"
	"path/filepath"
	"strings"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/layers"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/tour"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/safediagnostic"
)

type Stage string

const (
	Architecture Stage = "architecture"
	Tour         Stage = "tour"
	Domain       Stage = "domain"
)

type Invocation struct {
	Stage      Stage
	Attempt    int
	Prompt     string
	OutputFile string
	InputFile  string
	Input      []byte
}

type Runner interface {
	Invoke(context.Context, Invocation) ([]byte, error)
}

type Context struct {
	ProjectRoot    string
	ProjectName    string
	Description    string
	Languages      []string
	Frameworks     []string
	OutputLanguage string
	DirectoryTree  string
	README         string
	EntryPoint     string
	PreviousLayers []schema.Layer
}

type Warning struct {
	Stage    Stage
	Category string
	Message  string
}

type Result struct {
	Graph    schema.KnowledgeGraph
	Warnings []Warning
	Ran      bool
}

// Run executes the UA-success path in strict architecture -> tour order. The
// two-attempt retry and deterministic fallback are explicit AgentTeam availability
// extensions; pinned UA itself dispatches each stage once.
func Run(ctx context.Context, graph schema.KnowledgeGraph, input Context, runner Runner) (Result, error) {
	if err := ctx.Err(); err != nil {
		return Result{}, err
	}
	fallback := graph
	fallback.Layers = layers.Generate(fallback, layers.Options{})
	fallback.Tour = tour.Generate(fallback, tour.Options{})
	if err := ctx.Err(); err != nil {
		return Result{}, err
	}
	if runner == nil {
		return Result{Graph: fallback}, nil
	}
	warnings := []Warning{}
	architecturePrompt := BuildArchitecturePrompt(graph, input)
	semanticLayers, ok, err := invokeLayers(ctx, runner, architecturePrompt, graph, &warnings)
	if err != nil {
		return Result{}, err
	}
	if !ok {
		return Result{Graph: fallback, Warnings: warnings, Ran: true}, nil
	}
	withLayers := graph
	withLayers.Layers = semanticLayers
	tourPrompt := BuildTourPrompt(withLayers, input)
	semanticTour, ok, err := invokeTour(ctx, runner, tourPrompt, withLayers, &warnings)
	if err != nil {
		return Result{}, err
	}
	if !ok {
		withLayers.Tour = tour.Generate(withLayers, tour.Options{})
		return Result{Graph: withLayers, Warnings: warnings, Ran: true}, nil
	}
	withLayers.Tour = semanticTour
	return Result{Graph: withLayers, Warnings: warnings, Ran: true}, nil
}

func invokeLayers(ctx context.Context, runner Runner, prompt string, graph schema.KnowledgeGraph, warnings *[]Warning) ([]schema.Layer, bool, error) {
	var failure string
	for attempt := 1; attempt <= 2; attempt++ {
		if err := ctx.Err(); err != nil {
			return nil, false, err
		}
		p := prompt
		if attempt == 2 {
			p += "\n\n## AgentTeam retry context\nThe previous attempt failed validation: " + failure + "\nReturn a corrected result without changing the requested contract."
		}
		data, err := runner.Invoke(ctx, Invocation{Stage: Architecture, Attempt: attempt, Prompt: p, OutputFile: ".ua/intermediate/layers.json"})
		if fatal := contextFailure(ctx, err); fatal != nil {
			return nil, false, fatal
		}
		if err != nil {
			failure = bounded(err.Error())
			continue
		}
		value, err := NormalizeLayers(data, graph)
		if err == nil {
			return value, true, nil
		}
		failure = bounded(err.Error())
	}
	*warnings = append(*warnings, Warning{Stage: Architecture, Category: classifyFailure(failure), Message: failure})
	return nil, false, nil
}

func invokeTour(ctx context.Context, runner Runner, prompt string, graph schema.KnowledgeGraph, warnings *[]Warning) ([]schema.TourStep, bool, error) {
	var failure string
	for attempt := 1; attempt <= 2; attempt++ {
		if err := ctx.Err(); err != nil {
			return nil, false, err
		}
		p := prompt
		if attempt == 2 {
			p += "\n\n## AgentTeam retry context\nThe previous attempt failed validation: " + failure + "\nReturn a corrected result without changing the requested contract."
		}
		data, err := runner.Invoke(ctx, Invocation{Stage: Tour, Attempt: attempt, Prompt: p, OutputFile: ".ua/intermediate/tour.json"})
		if fatal := contextFailure(ctx, err); fatal != nil {
			return nil, false, fatal
		}
		if err != nil {
			failure = bounded(err.Error())
			continue
		}
		value, err := NormalizeTour(data, graph)
		if err == nil {
			return value, true, nil
		}
		failure = bounded(err.Error())
	}
	*warnings = append(*warnings, Warning{Stage: Tour, Category: classifyFailure(failure), Message: failure})
	return nil, false, nil
}

func contextFailure(ctx context.Context, err error) error {
	if ctxErr := ctx.Err(); ctxErr != nil {
		return ctxErr
	}
	if errors.Is(err, context.Canceled) {
		return context.Canceled
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return context.DeadlineExceeded
	}
	return nil
}

func classifyFailure(message string) string {
	lower := strings.ToLower(message)
	switch {
	case strings.Contains(lower, "executable file not found"), strings.Contains(lower, "exec:") && strings.Contains(lower, "not found"):
		return "opencode_not_installed"
	case strings.Contains(lower, "start opencode"), strings.Contains(lower, "connection refused"), strings.Contains(lower, "server startup"):
		return "opencode_startup_failed"
	default:
		return "opencode_model_or_output_failed"
	}
}

func bounded(value string) string {
	return safediagnostic.Sanitize(value, 500)
}

// BoundedMessage keeps dependency diagnostics safe for one-line progress logs.
func BoundedMessage(value string) string { return bounded(value) }

func fileLevelNodes(graph schema.KnowledgeGraph) []schema.GraphNode {
	contained := map[string]struct{}{}
	for _, edge := range graph.Edges {
		if edge.Type == "contains" {
			contained[edge.Target] = struct{}{}
		}
	}
	out := []schema.GraphNode{}
	for _, node := range graph.Nodes {
		if schema.IsFileLevelNode(node) {
			if _, child := contained[node.ID]; !child {
				out = append(out, node)
			}
		}
	}
	return out
}

func DetectEntryPoint(graph schema.KnowledgeGraph) string {
	patterns := []string{
		"src/index.ts", "src/main.ts", "src/App.tsx", "index.js", "main.py",
		"manage.py", "app.py", "wsgi.py", "asgi.py", "run.py", "__main__.py",
		"main.go", "cmd/*/main.go", "src/main.rs", "src/lib.rs",
		"src/main/java/**/Application.java", "Program.cs", "config.ru", "index.php",
	}
	for _, pattern := range patterns {
		for _, node := range fileLevelNodes(graph) {
			path := strings.ReplaceAll(node.FilePath, `\`, "/")
			if entryPointMatch(pattern, path) {
				return path
			}
		}
	}
	return ""
}

func entryPointMatch(pattern, path string) bool {
	if pattern == "src/main/java/**/Application.java" {
		return strings.HasPrefix(path, "src/main/java/") && strings.HasSuffix(path, "/Application.java")
	}
	matched, _ := filepath.Match(pattern, path)
	return matched
}
