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 := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request_error", "invalid JSON request")
		return
	}
	runMeta, inferenceTarget, workspace, err := parseOpenAIMetadata(req.Metadata)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
		return
	}

	dispatch, ok := s.resolveRouteDispatch(req.Model, inferenceTarget)
	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)
	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)),
	)

	handle, err := s.service.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
		NodeRef:    dispatch.NodeRef,
		Adapter:    dispatch.Adapter,
		Target:     dispatch.Target,
		SessionID:  dispatch.SessionID,
		Workspace:  workspace,
		Prompt:     prompt,
		Input:      input,
		TimeoutSec: dispatch.TimeoutSec,
		Metadata:   chatRunMetadata(runMeta, req, outputPolicy),
	})
	if err != nil {
		writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error())
		return
	}
	defer handle.Close()

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

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

func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) {
	text, reasoning, usage, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout())
	if err != nil {
		writeError(w, httpStatusForRunError(err), "run_error", err.Error())
		return
	}
	text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning)
	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", normalized),
		zap.Int("content_len", len(text)),
		zap.Int("reasoning_len", len(reasoning)),
		zap.String("content_preview", previewString(text, 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:      chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning},
			FinishReason: "stop",
		}},
		Usage: usage,
	})
}

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

func (s *Server) resolveTarget(model string) string {
	return s.resolveTargetWithOverride(model, "")
}

func (s *Server) resolveTargetWithOverride(model, override string) string {
	if s.cfg.Target != "" {
		return s.cfg.Target
	}
	if override != "" {
		return strings.TrimSpace(override)
	}
	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
	WorkspaceRequired 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
}

// resolveRouteDispatch returns fully-resolved dispatch params for model.
// Route catalog entries take priority; metadataTarget is only used in the
// legacy fallback path (when no catalog entry matches).
// Returns (dispatch, true) on success; (zero, false) when no target can be resolved.
func (s *Server) resolveRouteDispatch(model, metadataTarget string) (routeDispatch, bool) {
	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,
			WorkspaceRequired: route.WorkspaceRequired,
		}, true
	}
	target := s.resolveTargetWithOverride(model, metadataTarget)
	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}})
}
