package openai_compat

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

	"go.uber.org/zap"

	runtime "iop/packages/go/execution"
)

// Execute runs the OpenAI-compatible chat completions stream. It validates the
// request and emits the start event, sends the request with native-tool and
// text-tool fallback retries, then consumes the SSE stream into RuntimeEvents.
func (a *Adapter) Execute(ctx context.Context, spec runtime.ExecutionSpec, sink runtime.EventSink) error {
	exec, err := a.prepareExecution(ctx, spec, sink)
	if err != nil {
		return err
	}
	resp, textToolFallback, err := exec.requestWithFallback()
	if err != nil {
		return err
	}
	return a.consumeStream(ctx, spec, sink, resp, textToolFallback)
}

// chatExecution holds the validated request context shared by the request and
// retry stages so Execute stays a linear orchestration without a goto.
type chatExecution struct {
	adapter *Adapter
	ctx     context.Context
	sink    runtime.EventSink
	runID   string
	reqBody map[string]any
	body    []byte
}

// prepareExecution validates the spec, emits the start event, and builds the
// marshaled request body. The validation order and error text match the prior
// single-function Execute.
func (a *Adapter) prepareExecution(ctx context.Context, spec runtime.ExecutionSpec, sink runtime.EventSink) (*chatExecution, error) {
	if a.endpoint == "" && a.profile == nil {
		return nil, fmt.Errorf("openai_compat adapter: endpoint is required")
	}
	model := strings.TrimSpace(spec.Target)
	if model == "" {
		model = stringInput(spec.Input, "model")
	}
	if model == "" {
		return nil, fmt.Errorf("openai_compat adapter: target/model is required")
	}
	messages := messagesFromInput(spec.Input)
	if len(messages) == 0 {
		return nil, fmt.Errorf("openai_compat adapter: messages are required")
	}

	if err := sink.Emit(ctx, runtime.RuntimeEvent{
		RunID:     spec.RunID,
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	}); err != nil {
		return nil, err
	}

	reqBody, err := a.buildRequestBody(model, messages, spec.Input)
	if err != nil {
		_ = emitError(ctx, sink, spec.RunID, fmt.Sprintf("openai_compat build request body failed: %v", err))
		return nil, fmt.Errorf("openai_compat adapter: build request body: %w", err)
	}
	body, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("openai_compat adapter: marshal request: %w", err)
	}

	a.logger.Info("openai_compat adapter executing",
		zap.String("run_id", spec.RunID),
		zap.String("provider", a.provider),
		zap.String("target", model),
		zap.String("endpoint", a.endpoint),
	)

	return &chatExecution{
		adapter: a,
		ctx:     ctx,
		sink:    sink,
		runID:   spec.RunID,
		reqBody: reqBody,
		body:    body,
	}, nil
}

// requestWithFallback sends the chat completion request and, on a non-2xx
// response, applies the forced-single-tool retry then the text-tool fallback
// retry. It returns the streaming response to consume and whether the text-tool
// fallback was used, or an error after emitting the failure event. The retry
// order and short-circuit on the first 2xx match the prior goto-based flow.
func (e *chatExecution) requestWithFallback() (*http.Response, bool, error) {
	a := e.adapter
	resp, err := a.doChatCompletion(e.ctx, e.body)
	if err != nil {
		_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat request failed: %v", err))
		return nil, false, fmt.Errorf("openai_compat adapter: request: %w", err)
	}
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return resp, false, nil
	}

	msg := readLimited(resp.Body, 4096)
	_ = resp.Body.Close()

	if retryBody, ok := retryBodyWithForcedSingleTool(e.reqBody, msg); ok {
		body, err := json.Marshal(retryBody)
		if err != nil {
			_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat retry marshal failed: %v", err))
			return nil, false, fmt.Errorf("openai_compat adapter: retry marshal: %w", err)
		}
		resp, err = a.doChatCompletion(e.ctx, body)
		if err != nil {
			_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat retry request failed: %v", err))
			return nil, false, fmt.Errorf("openai_compat adapter: retry request: %w", err)
		}
		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return resp, false, nil
		}
		msg = readLimited(resp.Body, 4096)
		_ = resp.Body.Close()
	}

	if retryBody, ok := retryBodyWithTextToolFallback(e.reqBody, msg); ok {
		body, err := json.Marshal(retryBody)
		if err != nil {
			_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat text tool fallback marshal failed: %v", err))
			return nil, false, fmt.Errorf("openai_compat adapter: text tool fallback marshal: %w", err)
		}
		resp, err = a.doChatCompletion(e.ctx, body)
		if err != nil {
			_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat text tool fallback request failed: %v", err))
			return nil, false, fmt.Errorf("openai_compat adapter: text tool fallback request: %w", err)
		}
		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return resp, true, nil
		}
		msg = readLimited(resp.Body, 4096)
		_ = resp.Body.Close()
	}

	if msg == "" {
		msg = resp.Status
	}
	_ = emitError(e.ctx, e.sink, e.runID, fmt.Sprintf("openai_compat returned %s: %s", resp.Status, msg))
	return nil, false, fmt.Errorf("openai_compat adapter: non-2xx response: %s", resp.Status)
}
