package openai

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"go.uber.org/zap"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
)

func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
		return
	}
	defer r.Body.Close()

	var req chatCompletionRequest
	if err := decodeChatCompletionRequest(json.NewDecoder(r.Body), &req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
		return
	}
	runMeta, workspace, err := parseOpenAIMetadata(req.Metadata)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
		return
	}

	dispatch, ok := s.resolveRouteDispatch(req.Model)
	if !ok {
		writeError(w, http.StatusBadRequest, "invalid_request_error", "model is required")
		return
	}
	if err := validateWorkspaceForRoute(dispatch, workspace); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
		return
	}
	basePrompt := promptFromMessages(req.Messages)
	if strings.TrimSpace(basePrompt) == "" {
		writeError(w, http.StatusBadRequest, "invalid_request_error", "messages are required")
		return
	}
	outputPolicy := s.resolveOutputPolicy(basePrompt)
	messages := req.Messages
	if instruction := strictOutputContractInstruction(outputPolicy); instruction != "" {
		messages = prependSystemMessage(messages, instruction)
	}
	prompt := promptFromMessages(messages)
	input := req.runInput(prompt, messages, outputPolicy.Strict, routeSupportsNativeToolCalls(dispatch))
	s.logger.Info("openai chat completion input",
		zap.String("model", req.Model),
		zap.String("target", dispatch.Target),
		zap.String("adapter", dispatch.Adapter),
		zap.Bool("strict_output", outputPolicy.Strict),
		zap.Bool("strict_stream_buffer", outputPolicy.StreamBuffer),
		zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool),
		zap.Bool("contract_instruction", outputPolicy.ContractInstruction),
		zap.Bool("stream", req.Stream),
		zap.Int("message_count", len(req.Messages)),
		zap.Int("prompt_len", len(prompt)),
		zap.String("prompt_preview", previewString(prompt, 1000)),
		zap.Any("input_keys", mapKeys(input)),
	)

	validation := toolValidationContractForChatRequest(req, outputPolicy)
	metadata := chatRunMetadata(runMeta, req, outputPolicy)
	if validation.enabled {
		metadata = toolValidationAttemptMetadata(metadata, 1, "", "")
	}
	submitReq := chatSubmitRunRequest(dispatch, req, workspace, prompt, input, metadata)
	handle, err := s.service.SubmitRun(r.Context(), submitReq)
	if err != nil {
		writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error())
		return
	}

	if req.Stream {
		s.streamChatCompletion(w, r, req, submitReq, handle, outputPolicy, validation)
		return
	}
	s.completeChatCompletion(w, r, req, submitReq, handle, outputPolicy, validation)
}

func decodeChatCompletionRequest(dec *json.Decoder, req *chatCompletionRequest) error {
	var raw map[string]json.RawMessage
	if err := dec.Decode(&raw); err != nil {
		return fmt.Errorf("invalid JSON request")
	}
	for key := range raw {
		switch key {
		case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store":
		default:
			return fmt.Errorf("%s is not supported for /v1/chat/completions", key)
		}
	}
	normalized, err := json.Marshal(raw)
	if err != nil {
		return fmt.Errorf("invalid JSON request")
	}
	if err := json.Unmarshal(normalized, req); err != nil {
		return fmt.Errorf("invalid /v1/chat/completions request format")
	}
	if req.MaxTokens != nil && *req.MaxTokens <= 0 {
		return fmt.Errorf("max_tokens must be greater than zero")
	}
	if req.MaxCompletionTokens != nil && *req.MaxCompletionTokens <= 0 {
		return fmt.Errorf("max_completion_tokens must be greater than zero")
	}
	if req.Temperature != nil && (*req.Temperature < 0 || *req.Temperature > 2) {
		return fmt.Errorf("temperature must be between 0 and 2")
	}
	if req.TopP != nil && (*req.TopP < 0 || *req.TopP > 1) {
		return fmt.Errorf("top_p must be between 0 and 1")
	}
	return nil
}

func chatRunMetadata(runMeta map[string]string, req chatCompletionRequest, outputPolicy strictOutputPolicy) map[string]string {
	if runMeta == nil {
		runMeta = make(map[string]string)
	}
	runMeta["openai_model"] = req.Model
	runMeta["openai_stream"] = fmt.Sprintf("%t", req.Stream)
	runMeta["strict_output"] = fmt.Sprintf("%t", outputPolicy.Strict)
	return runMeta
}

func routeSupportsNativeToolCalls(dispatch routeDispatch) bool {
	if dispatch.ProviderPool {
		return true
	}
	switch strings.TrimSpace(dispatch.Adapter) {
	case "openai_compat", "vllm", "ollama":
		return true
	default:
		return false
	}
}

func shouldSynthesizeTextToolCalls(dispatch edgeservice.RunDispatch, textToolFallback bool) bool {
	return textToolFallback || strings.TrimSpace(dispatch.Adapter) == "cli"
}

func chatSubmitRunRequest(dispatch routeDispatch, req chatCompletionRequest, workspace, prompt string, input map[string]any, metadata map[string]string) edgeservice.SubmitRunRequest {
	return edgeservice.SubmitRunRequest{
		NodeRef:        dispatch.NodeRef,
		ModelGroupKey:  strings.TrimSpace(req.Model),
		Adapter:        dispatch.Adapter,
		Target:         dispatch.Target,
		SessionID:      dispatch.SessionID,
		Workspace:      workspace,
		Prompt:         prompt,
		Input:          input,
		TimeoutSec:     dispatch.TimeoutSec,
		MaxQueue:       dispatch.MaxQueue,
		QueueTimeoutMS: dispatch.QueueTimeoutMS,
		Metadata:       metadata,
		ProviderPool:   dispatch.ProviderPool,
	}
}

func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, submitReq edgeservice.SubmitRunRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy, validation toolValidationContract) {
	attempt := 1
	for {
		result, err := collectChatCompletionOutput(r.Context(), req, handle, outputPolicy)
		if err != nil {
			s.cancelRunOnHTTPGiveUp(handle.Dispatch(), err)
			handle.Close()
			writeError(w, httpStatusForRunError(err), "run_error", err.Error())
			return
		}
		if err := validateToolCallResponse(validation, result.toolCalls, result.toolCallOrigin); err != nil {
			failedRunID := handle.Dispatch().RunID
			if attempt < maxToolValidationAttempts {
				handle.Close()
				attempt++
				retryReq := submitReq
				retryReq.Metadata = toolValidationAttemptMetadata(submitReq.Metadata, attempt, failedRunID, err.Error())
				s.logger.Warn("openai chat completion tool validation retry",
					zap.String("run_id", failedRunID),
					zap.Int("attempt", attempt),
					zap.String("reason", err.Error()),
				)
				next, submitErr := s.service.SubmitRun(r.Context(), retryReq)
				if submitErr != nil {
					writeError(w, http.StatusBadGateway, "tool_validation_retry_error", submitErr.Error())
					return
				}
				handle = next
				continue
			}
			handle.Close()
			s.logger.Warn("openai chat completion tool validation failed",
				zap.String("run_id", failedRunID),
				zap.Int("attempt", attempt),
				zap.String("reason", err.Error()),
			)
			writeError(w, http.StatusBadGateway, "tool_validation_error", err.Error())
			return
		}
		handle.Close()
		s.logger.Info("openai chat completion output",
			zap.String("run_id", handle.Dispatch().RunID),
			zap.Bool("strict_output", outputPolicy.Strict),
			zap.Bool("strict_stream_buffer", outputPolicy.StreamBuffer),
			zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool),
			zap.Bool("normalized", result.normalized),
			zap.Int("content_len", result.contentLen),
			zap.Int("reasoning_len", result.reasoningLen),
			zap.String("content_preview", previewString(result.message.Content, 1000)),
		)
		created := time.Now().Unix()
		writeJSON(w, http.StatusOK, chatCompletionResponse{
			ID:      "chatcmpl-" + handle.Dispatch().RunID,
			Object:  "chat.completion",
			Created: created,
			Model:   responseModel(req.Model, handle.Dispatch().Target),
			Choices: []chatCompletionChoice{{
				Index:        0,
				Message:      result.message,
				FinishReason: result.finishReason,
			}},
			Usage: result.usage,
		})
		return
	}
}

type chatCompletionOutput struct {
	message        chatMessage
	finishReason   string
	usage          *openAIUsage
	normalized     bool
	contentLen     int
	reasoningLen   int
	toolCalls      []any
	toolCallOrigin string
}

func collectChatCompletionOutput(ctx context.Context, req chatCompletionRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) (chatCompletionOutput, error) {
	text, reasoning, finishReason, nativeToolCalls, usage, textToolFallback, err := collectRunResult(ctx, handle.Stream(), handle.WaitTimeout())
	if err != nil {
		return chatCompletionOutput{}, err
	}
	text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning)
	message := chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning}
	var toolCalls []any
	toolCallOrigin := ""
	if len(nativeToolCalls) > 0 {
		nativeToolCalls = normalizeNativeToolCallsForResponse(nativeToolCalls, req.Tools)
		message.ToolCalls = nativeToolCalls
		toolCalls = nativeToolCalls
		toolCallOrigin = toolCallOriginNative
		finishReason = "tool_calls"
	} else if shouldSynthesizeTextToolCalls(handle.Dispatch(), textToolFallback) {
		cleaned, synthesizedToolCalls := synthesizeToolCallsFromText(text, req.Tools, handle.Dispatch().RunID)
		if len(synthesizedToolCalls) > 0 {
			message.Content = cleaned
			message.ReasoningContent = ""
			message.ToolCalls = synthesizedToolCalls
			toolCalls = synthesizedToolCalls
			toolCallOrigin = toolCallOriginText
			finishReason = "tool_calls"
		}
	}
	return chatCompletionOutput{
		message:        message,
		finishReason:   finishReason,
		usage:          usage,
		normalized:     normalized,
		contentLen:     len(text),
		reasoningLen:   len(reasoning),
		toolCalls:      toolCalls,
		toolCallOrigin: toolCallOrigin,
	}, nil
}

func (s *Server) resolveAdapter() string {
	if s.cfg.Adapter != "" {
		return s.cfg.Adapter
	}
	return "ollama"
}

func (s *Server) resolveTarget(model string) string {
	if s.cfg.Target != "" {
		return s.cfg.Target
	}
	return strings.TrimSpace(model)
}

// routeDispatch holds fully-resolved dispatch parameters for a single request.
type routeDispatch struct {
	NodeRef           string
	Adapter           string
	Target            string
	SessionID         string
	TimeoutSec        int
	MaxQueue          int
	QueueTimeoutMS    int
	WorkspaceRequired bool
	// ProviderPool is true when the request model matched a provider-pool
	// catalog entry. Adapter and Target are empty; the service layer resolves
	// them per-candidate and rewrites Target after admission.
	ProviderPool bool
}

// resolveRoute returns the first catalog entry whose Model matches model.
// Entries with an empty Target are skipped.
func (s *Server) resolveRoute(model string) *config.OpenAIRouteEntry {
	model = strings.TrimSpace(model)
	if model == "" {
		return nil
	}
	for i := range s.cfg.ModelRoutes {
		r := &s.cfg.ModelRoutes[i]
		if strings.TrimSpace(r.Model) == model && r.Target != "" {
			return r
		}
	}
	return nil
}

// findProviderPoolEntry returns the catalog entry matching model, or nil.
func (s *Server) findProviderPoolEntry(model string) *config.ModelCatalogEntry {
	model = strings.TrimSpace(model)
	if model == "" {
		return nil
	}
	modelCatalog := s.modelCatalogSnapshot()
	for i := range modelCatalog {
		if modelCatalog[i].ID == model {
			entry := modelCatalog[i]
			return &entry
		}
	}
	return nil
}

// resolveRouteDispatch returns fully-resolved dispatch params for model.
// Priority: provider-pool catalog → legacy model_routes → single-target fallback.
// Returns (dispatch, true) on success; (zero, false) when no target can be resolved.
func (s *Server) resolveRouteDispatch(model string) (routeDispatch, bool) {
	// Provider-pool catalog takes highest priority.
	if s.findProviderPoolEntry(model) != nil {
		return routeDispatch{
			SessionID:    s.resolveSessionID(),
			TimeoutSec:   s.resolveTimeoutSec(),
			ProviderPool: true,
		}, true
	}

	if route := s.resolveRoute(model); route != nil {
		adapter := route.Adapter
		if adapter == "" {
			adapter = s.resolveAdapter()
		}
		nodeRef := route.NodeRef
		if nodeRef == "" {
			nodeRef = s.cfg.NodeRef
		}
		sessionID := route.SessionID
		if sessionID == "" {
			sessionID = s.resolveSessionID()
		}
		timeoutSec := route.TimeoutSec
		if timeoutSec <= 0 {
			timeoutSec = s.resolveTimeoutSec()
		}
		return routeDispatch{
			NodeRef:           nodeRef,
			Adapter:           adapter,
			Target:            route.Target,
			SessionID:         sessionID,
			TimeoutSec:        timeoutSec,
			MaxQueue:          route.MaxQueue,
			QueueTimeoutMS:    route.QueueTimeoutMS,
			WorkspaceRequired: route.WorkspaceRequired,
		}, true
	}
	target := s.resolveTarget(model)
	if target == "" {
		return routeDispatch{}, false
	}
	return routeDispatch{
		NodeRef:    s.cfg.NodeRef,
		Adapter:    s.resolveAdapter(),
		Target:     target,
		SessionID:  s.resolveSessionID(),
		TimeoutSec: s.resolveTimeoutSec(),
	}, true
}

func (s *Server) resolveSessionID() string {
	if s.cfg.SessionID != "" {
		return s.cfg.SessionID
	}
	return edgeservice.DefaultSessionID
}

func (s *Server) resolveTimeoutSec() int {
	if s.cfg.TimeoutSec > 0 {
		return s.cfg.TimeoutSec
	}
	return edgeservice.DefaultTimeoutSec
}

func (s *Server) resolveStrictOutput() bool {
	return s.cfg.StrictOutput
}

func (s *Server) resolveStrictStreamBuffer() bool {
	return s.cfg.StrictStreamBuffer
}

func (s *Server) resolveOutputPolicy(prompt string) strictOutputPolicy {
	policy := strictOutputPolicy{
		Strict:       s.resolveStrictOutput(),
		StreamBuffer: s.resolveStrictStreamBuffer(),
	}
	if !policy.Strict {
		return policy
	}
	policy.XMLCompletionTool, policy.XMLResultTag = inferXMLCompletionContract(prompt)
	policy.ContractInstruction = policy.XMLCompletionTool != ""
	return policy
}

func promptFromMessages(messages []chatMessage) string {
	var b strings.Builder
	for _, msg := range messages {
		content := strings.TrimSpace(msg.Content)
		if content == "" {
			continue
		}
		role := strings.TrimSpace(msg.Role)
		if role == "" {
			role = "user"
		}
		if b.Len() > 0 {
			b.WriteString("\n")
		}
		b.WriteString(role)
		b.WriteString(": ")
		b.WriteString(content)
	}
	return b.String()
}

func responseModel(requestModel, target string) string {
	if requestModel != "" {
		return requestModel
	}
	return target
}

func httpStatusForRunError(err error) int {
	if errors.Is(err, context.Canceled) {
		return http.StatusRequestTimeout
	}
	return http.StatusBadGateway
}

func validateWorkspaceForRoute(d routeDispatch, workspace string) error {
	if !d.WorkspaceRequired {
		return nil
	}
	if strings.TrimSpace(workspace) == "" {
		return fmt.Errorf("workspace is required for this model route")
	}
	if !filepath.IsAbs(workspace) {
		return fmt.Errorf("workspace must be an absolute path")
	}
	return nil
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, code, message string) {
	writeJSON(w, status, errorResponse{Error: errorBody{Type: code, Message: message}})
}
