package openai

import (
	"encoding/json"
	"fmt"
	"path"
	"path/filepath"
	"reflect"
	"sort"
	"strings"

	"iop/packages/go/config"
)

const (
	modeDirect = config.ModeDirect
	modeLight  = config.ModeLight
)

const (
	reasonDirectNoReservedControls = "direct_no_reserved_controls"
	reasonLightExactPrepare        = "light_exact_prepare"
	reasonLightExactPair           = "light_exact_pair"
	reasonMalformedPartialPair     = "malformed_partial_pair"
	reasonMalformedMixedCalls      = "malformed_mixed_calls"
	reasonMalformedDuplicateCalls  = "malformed_duplicate_calls"
	reasonMalformedWrongPath       = "malformed_wrong_path"
	reasonMalformedControlRole     = "malformed_control_role"
	reasonMalformedConflictingPath = "malformed_conflicting_path"
	reasonModeDisabled             = "mode_disabled"
	reasonUnhealthyRoute           = "unhealthy_route"
	reasonArtifactRequired         = "artifact_required"
)

type reservedPaths struct {
	RequestID  string
	JobDir     string // e.g. ".iop/job/<request_id>"
	PlanPath   string // e.g. ".iop/job/<request_id>/plan.md"
	ReviewPath string // e.g. ".iop/job/<request_id>/review.md"
}

func newReservedPaths(requestID string) reservedPaths {
	cleanID := strings.TrimSpace(requestID)
	jobDir := ".iop/job/" + cleanID
	return reservedPaths{
		RequestID:  cleanID,
		JobDir:     jobDir,
		PlanPath:   jobDir + "/plan.md",
		ReviewPath: jobDir + "/review.md",
	}
}

type normalizedToolCall struct {
	ID             string         `json:"id"`
	ProviderCallID string         `json:"provider_call_id,omitempty"`
	Name           string         `json:"name"`
	Arguments      map[string]any `json:"arguments,omitempty"`
	RawArgs        string         `json:"raw_args,omitempty"`
	Path           string         `json:"path,omitempty"`
}

type normalizedStageDeltaKind string

const (
	normalizedStageDeltaText      normalizedStageDeltaKind = "text"
	normalizedStageDeltaReasoning normalizedStageDeltaKind = "reasoning"
	normalizedStageDeltaTool      normalizedStageDeltaKind = "tool"
)

// normalizedStageDelta preserves provider-independent delta order after the
// selected provider decoder has done its work. Caller codecs consume this
// shape and never parse the selected provider wire again.
type normalizedStageDelta struct {
	Kind      normalizedStageDeltaKind
	Text      string
	ToolID    string
	ToolName  string
	Arguments string
}

type normalizedStageOutput struct {
	ResponseID            string                 `json:"response_id,omitempty"`
	Created               int64                  `json:"created,omitempty"`
	Content               string                 `json:"content,omitempty"`
	Reasoning             string                 `json:"reasoning,omitempty"`
	ReasoningSignature    string                 `json:"reasoning_signature,omitempty"`
	ToolCalls             []normalizedToolCall   `json:"tool_calls,omitempty"`
	TerminalReason        string                 `json:"terminal_reason,omitempty"`
	Usage                 json.RawMessage        `json:"usage,omitempty"`
	OpenAIUsage           *openAIUsage           `json:"-"`
	Deltas                []normalizedStageDelta `json:"-"`
	ProgressivelyReleased bool                   `json:"-"`
	CallerStageOnly       bool                   `json:"-"`
}

// hotPathSelectorGate is immutable evidence from the single provider-pool
// admission that produced output. Classification never substitutes a caller
// flag or re-resolves mutable catalog state for these facts.
type hotPathSelectorGate struct {
	PresetID            string
	SelectorModel       string
	ModelGroupKey       string
	ProviderID          string
	RunID               string
	NodeID              string
	ExecutionPath       string
	ProfileDriver       string
	ProfileOperation    string
	ProfileCapabilities []string
	Healthy             bool
	CapabilitySatisfied bool
}

type hotPathDecision struct {
	Mode         string               `json:"mode"`
	Reason       string               `json:"reason"`
	PrepareCall  *normalizedToolCall  `json:"prepare_call,omitempty"`
	PairCalls    []normalizedToolCall `json:"pair_calls,omitempty"`
	GeneralCalls []normalizedToolCall `json:"general_calls,omitempty"`
}

func classifyHotPathOutput(preset config.ExecutionPreset, issuedPaths reservedPaths, output normalizedStageOutput, gate hotPathSelectorGate) (hotPathDecision, error) {
	if !gate.Healthy || !gate.CapabilitySatisfied || gate.PresetID != preset.ID || gate.SelectorModel != preset.Selector.Model ||
		strings.TrimSpace(gate.ModelGroupKey) == "" || strings.TrimSpace(gate.ProviderID) == "" ||
		strings.TrimSpace(gate.RunID) == "" || strings.TrimSpace(gate.NodeID) == "" ||
		strings.TrimSpace(gate.ExecutionPath) == "" || strings.TrimSpace(gate.ProfileDriver) == "" {
		return hotPathDecision{Reason: reasonUnhealthyRoute}, fmt.Errorf("route capability or health gate check failed (%s)", reasonUnhealthyRoute)
	}

	var prepareCalls []normalizedToolCall
	var planCalls []normalizedToolCall
	var reviewCalls []normalizedToolCall
	var wrongPathCalls []normalizedToolCall
	var generalCalls []normalizedToolCall

	for _, tc := range output.ToolCalls {
		control, err := classifyReservedControlCall(preset, issuedPaths, tc)
		if err != nil {
			return hotPathDecision{Reason: control.reason}, err
		}
		switch control.kind {
		case "":
			generalCalls = append(generalCalls, tc)
		case "prepare":
			prepareCalls = append(prepareCalls, tc)
		case "plan":
			planCalls = append(planCalls, tc)
		case "review":
			reviewCalls = append(reviewCalls, tc)
		default:
			wrongPathCalls = append(wrongPathCalls, tc)
		}
	}

	if len(wrongPathCalls) > 0 {
		return hotPathDecision{Reason: reasonMalformedWrongPath}, fmt.Errorf("malformed output: tool call targets wrong or invalid reserved path (%s)", reasonMalformedWrongPath)
	}

	reservedCount := len(prepareCalls) + len(planCalls) + len(reviewCalls)

	// Mode Direct Candidate
	if reservedCount == 0 {
		if !isModeAllowed(preset, modeDirect) {
			return hotPathDecision{Reason: reasonModeDisabled}, fmt.Errorf("mode %q is disabled for preset %q (%s)", modeDirect, preset.ID, reasonModeDisabled)
		}
		return hotPathDecision{
			Mode:         modeDirect,
			Reason:       reasonDirectNoReservedControls,
			GeneralCalls: generalCalls,
		}, nil
	}

	// Mode Light Candidate
	if !isModeAllowed(preset, modeLight) {
		return hotPathDecision{Reason: reasonModeDisabled}, fmt.Errorf("mode %q is disabled for preset %q (%s)", modeLight, preset.ID, reasonModeDisabled)
	}

	if len(generalCalls) > 0 {
		return hotPathDecision{Reason: reasonMalformedMixedCalls}, fmt.Errorf("malformed output: mixed reserved controls and general tool calls (%s)", reasonMalformedMixedCalls)
	}

	if len(prepareCalls) > 1 || len(planCalls) > 1 || len(reviewCalls) > 1 {
		return hotPathDecision{Reason: reasonMalformedDuplicateCalls}, fmt.Errorf("malformed output: duplicate reserved control calls (%s)", reasonMalformedDuplicateCalls)
	}

	// Exact Prepare
	if len(prepareCalls) == 1 && len(planCalls) == 0 && len(reviewCalls) == 0 {
		prep := prepareCalls[0]
		return hotPathDecision{
			Mode:        modeLight,
			Reason:      reasonLightExactPrepare,
			PrepareCall: &prep,
		}, nil
	}

	// Exact Pair
	if len(prepareCalls) == 0 && len(planCalls) == 1 && len(reviewCalls) == 1 {
		return hotPathDecision{
			Mode:      modeLight,
			Reason:    reasonLightExactPair,
			PairCalls: []normalizedToolCall{planCalls[0], reviewCalls[0]},
		}, nil
	}

	return hotPathDecision{Reason: reasonMalformedPartialPair}, fmt.Errorf("malformed output: partial reserved control pair (%s)", reasonMalformedPartialPair)
}

type reservedControlClassification struct {
	kind   string
	reason string
}

func classifyReservedControlCall(preset config.ExecutionPreset, issued reservedPaths, tc normalizedToolCall) (reservedControlClassification, error) {
	sources := reservedPathSourcesFromToolCall(tc)
	paths := reservedPathsFromToolCall(tc)
	if len(sources) == 0 {
		return reservedControlClassification{}, nil
	}
	if len(sources) != 1 || len(paths) != 1 {
		return reservedControlClassification{reason: reasonMalformedConflictingPath}, fmt.Errorf("malformed output: conflicting reserved path sources (%s)", reasonMalformedConflictingPath)
	}
	observed := paths[0]

	type roleMatch struct {
		role string
		path string
	}
	var matches []roleMatch
	for _, alternative := range preset.WorkspaceTools {
		for _, role := range []string{"prepare", "write"} {
			op, ok := alternative.Operations[role]
			if !ok || strings.TrimSpace(op.ToolName) != strings.TrimSpace(tc.Name) {
				continue
			}
			mappedPath, ok := mappedControlPath(tc, op)
			if !ok {
				continue
			}
			matches = append(matches, roleMatch{role: role, path: mappedPath})
		}
	}
	if len(matches) == 0 {
		return reservedControlClassification{reason: reasonMalformedControlRole}, fmt.Errorf("malformed output: reserved path used by a non-canonical control role (%s)", reasonMalformedControlRole)
	}

	cleanJobDir := cleanRelativePath(issued.JobDir)
	cleanPlan := cleanRelativePath(issued.PlanPath)
	cleanReview := cleanRelativePath(issued.ReviewPath)
	for _, match := range matches {
		if match.path != observed {
			continue
		}
		switch {
		case match.role == "prepare" && observed == cleanJobDir:
			return reservedControlClassification{kind: "prepare"}, nil
		case match.role == "write" && observed == cleanPlan:
			return reservedControlClassification{kind: "plan"}, nil
		case match.role == "write" && observed == cleanReview:
			return reservedControlClassification{kind: "review"}, nil
		}
	}
	if observed != cleanJobDir && observed != cleanPlan && observed != cleanReview {
		return reservedControlClassification{reason: reasonMalformedWrongPath}, fmt.Errorf("malformed output: tool call targets wrong or invalid reserved path (%s)", reasonMalformedWrongPath)
	}
	for _, match := range matches {
		if match.path != observed {
			return reservedControlClassification{reason: reasonMalformedWrongPath}, fmt.Errorf("malformed output: mapped control path must equal the complete issued path (%s)", reasonMalformedWrongPath)
		}
	}
	return reservedControlClassification{reason: reasonMalformedControlRole}, fmt.Errorf("malformed output: canonical control role does not match reserved path (%s)", reasonMalformedControlRole)
}

func mappedControlPath(tc normalizedToolCall, op config.ExecutionWorkspaceOperation) (string, bool) {
	mapped, ok := op.ArgumentMap["path"].(string)
	if !ok || strings.TrimSpace(mapped) == "" {
		return "", false
	}
	value, ok := lookupMappedArgument(tc.Arguments, mapped)
	if !ok && tc.RawArgs != "" {
		var args map[string]any
		decoder := json.NewDecoder(strings.NewReader(tc.RawArgs))
		decoder.UseNumber()
		if decoder.Decode(&args) == nil {
			value, ok = lookupMappedArgument(args, mapped)
		}
	}
	if !ok {
		return "", false
	}
	text, ok := value.(string)
	if !ok {
		return "", false
	}
	mappedPath := cleanRelativePath(text)
	if mappedPath == "" || mappedPath == "." {
		return "", false
	}
	return mappedPath, true
}

func lookupMappedArgument(arguments map[string]any, mapped string) (any, bool) {
	if arguments == nil {
		return nil, false
	}
	parts := strings.Split(mapped, ".")
	var current any = arguments
	for _, part := range parts {
		object, ok := current.(map[string]any)
		if !ok {
			return nil, false
		}
		current, ok = object[part]
		if !ok {
			return nil, false
		}
	}
	return current, true
}

func reservedPathsFromToolCall(tc normalizedToolCall) []string {
	set := make(map[string]struct{})
	for _, item := range reservedPathSourcesFromToolCall(tc) {
		set[item] = struct{}{}
	}
	paths := make([]string, 0, len(set))
	for item := range set {
		paths = append(paths, item)
	}
	sort.Strings(paths)
	return paths
}

// reservedPathSourcesFromToolCall preserves each independently supplied
// reserved-path occurrence. RawArgs normally serializes Arguments for normalized
// provider calls, so an equivalent decoded copy is not counted twice. A raw
// argument that differs from the decoded argument is still an independent source
// and must be rejected if it contains a reserved path.
func reservedPathSourcesFromToolCall(tc normalizedToolCall) []string {
	var paths []string
	add := func(value string) {
		paths = append(paths, reservedPathsFromString(value)...)
	}
	add(tc.Path)
	if tc.Arguments != nil {
		collectReservedStrings(tc.Arguments, add)
		if tc.RawArgs == "" {
			return paths
		}
		var decoded map[string]any
		decoder := json.NewDecoder(strings.NewReader(tc.RawArgs))
		decoder.UseNumber()
		if decoder.Decode(&decoded) == nil && decoded != nil {
			if !reflect.DeepEqual(decoded, tc.Arguments) {
				collectReservedStrings(decoded, add)
			}
			return paths
		}
		add(tc.RawArgs)
		return paths
	}
	if tc.RawArgs == "" {
		return paths
	}
	var decoded any
	decoder := json.NewDecoder(strings.NewReader(tc.RawArgs))
	decoder.UseNumber()
	if decoder.Decode(&decoded) == nil {
		collectReservedStrings(decoded, add)
	} else {
		add(tc.RawArgs)
	}
	return paths
}

func collectReservedStrings(value any, add func(string)) {
	switch typed := value.(type) {
	case string:
		add(typed)
	case map[string]any:
		for _, item := range typed {
			collectReservedStrings(item, add)
		}
	case []any:
		for _, item := range typed {
			collectReservedStrings(item, add)
		}
	}
}

func reservedPathsFromString(value string) []string {
	normalized := strings.ReplaceAll(value, `\/`, "/")
	normalized = filepath.ToSlash(normalized)
	var paths []string
	for search := normalized; ; {
		idx := strings.Index(search, ".iop/job")
		if idx < 0 {
			break
		}
		candidate := search[idx:]
		end := len(candidate)
		for i, ch := range candidate {
			if ch == ' ' || ch == '\t' || ch == '\n' || ch == '"' || ch == '\'' || ch == '`' || ch == ';' || ch == ',' || ch == '}' || ch == ']' || ch == ')' {
				end = i
				break
			}
		}
		paths = append(paths, cleanRelativePath(candidate[:end]))
		advance := idx + len(".iop/job")
		if advance >= len(search) {
			break
		}
		search = search[advance:]
	}
	return paths
}

func isModeAllowed(preset config.ExecutionPreset, mode string) bool {
	for _, m := range preset.AllowedModes {
		if m == mode {
			return true
		}
	}
	return false
}

func extractPathFromToolCall(tc normalizedToolCall) string {
	paths := reservedPathsFromToolCall(tc)
	if len(paths) == 1 {
		return paths[0]
	}
	return ""
}

func extractIopJobPath(s string) string {
	idx := strings.Index(s, ".iop/job/")
	if idx < 0 {
		return ""
	}
	sub := s[idx:]
	for i, ch := range sub {
		if ch == ' ' || ch == '\t' || ch == '\n' || ch == '"' || ch == '\'' || ch == '`' || ch == ';' {
			return sub[:i]
		}
	}
	return sub
}

func cleanRelativePath(p string) string {
	p = strings.TrimSpace(p)
	p = filepath.ToSlash(p)
	p = path.Clean(p)
	p = strings.TrimPrefix(p, "./")
	p = strings.TrimSuffix(p, "/")
	return p
}
