package clients

import (
	"context"
	"errors"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const (
	defaultProviderRequestTimeout = 10 * time.Minute
	imageProviderRequestTimeout   = 20 * time.Minute
	videoProviderRequestTimeout   = 30 * time.Minute
)

// ProviderRequestTimeout returns the default end-to-end provider request or
// polling budget for a task kind. Explicit platform polling configuration may
// still override these defaults for providers with a documented requirement.
func ProviderRequestTimeout(kind string) time.Duration {
	switch {
	case strings.HasPrefix(strings.TrimSpace(kind), "images."):
		return imageProviderRequestTimeout
	case strings.HasPrefix(strings.TrimSpace(kind), "videos."):
		return videoProviderRequestTimeout
	default:
		return defaultProviderRequestTimeout
	}
}

func transportClientError(err error) *ClientError {
	if err == nil {
		return nil
	}
	var netErr net.Error
	if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &netErr) && netErr.Timeout()) {
		return &ClientError{
			Code:      "timeout",
			Message:   "upstream request timed out: " + err.Error(),
			Retryable: false,
		}
	}
	return &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}

type Request struct {
	Kind                        string
	ModelType                   string
	Model                       string
	Body                        map[string]any
	OriginalBody                map[string]any
	Candidate                   store.RuntimeModelCandidate
	HTTPClient                  *http.Client
	RemoteTaskID                string
	RemoteTaskPayload           map[string]any
	UpstreamIdempotencyKey      string
	OnRemoteTaskSubmitted       func(remoteTaskID string, payload map[string]any) error
	OnRemoteTaskPolled          func(remoteTaskID string, payload map[string]any) error
	OnUpstreamSubmissionStarted func() error
	OnUpstreamResponseReceived  func() error
	OnUpstreamWireResponse      func(*WireResponse) error
	Stream                      bool
	StreamDelta                 StreamDelta
	UpstreamProtocol            string
	PublicResponseID            string
	PublicPreviousResponseID    string
	UpstreamPreviousResponseID  string
	PreviousResponseTurns       []ResponseTurn
}

func applyUpstreamIdempotency(request *http.Request, input Request) {
	if request == nil {
		return
	}
	if key := strings.TrimSpace(input.UpstreamIdempotencyKey); key != "" {
		request.Header.Set("Idempotency-Key", key)
	}
}

// WireResponse preserves the provider-facing response independently from the
// canonical result used by billing, retries, and cross-protocol conversion.
// Headers are filtered before this value leaves the clients package.
type WireResponse struct {
	Protocol   string              `json:"protocol,omitempty"`
	StatusCode int                 `json:"statusCode,omitempty"`
	Headers    map[string][]string `json:"headers,omitempty"`
	Body       map[string]any      `json:"body,omitempty"`
	RawJSON    []byte              `json:"-"`
	Converted  bool                `json:"converted,omitempty"`
}

type ResponseTurn struct {
	Request  map[string]any
	Response map[string]any
	Internal map[string]any
}

type Response struct {
	AttemptID          string
	Result             map[string]any
	RequestID          string
	Usage              Usage
	Progress           []Progress
	Metrics            map[string]any
	ResponseStartedAt  time.Time
	ResponseFinishedAt time.Time
	ResponseDurationMS int64
	UpstreamProtocol   string
	UpstreamEndpoint   string
	UpstreamResponseID string
	PublicResponseID   string
	ParentResponseID   string
	ResponseChainDepth int
	ResponseConverted  bool
	InternalResult     map[string]any
	Wire               *WireResponse
}

type Usage struct {
	InputTokens            int
	OutputTokens           int
	CachedInputTokens      int
	CachedInputTokensKnown bool
	TotalTokens            int
}

type Progress struct {
	Phase    string
	Progress float64
	Message  string
	Payload  map[string]any
}

type StreamDeltaEvent struct {
	Text             string
	ReasoningContent string
	Event            map[string]any
	WireProtocol     string
	WireConverted    bool
	WireStatusCode   int
	WireHeaders      map[string][]string
}

type StreamDelta func(event StreamDeltaEvent) error

type Client interface {
	Run(ctx context.Context, request Request) (Response, error)
}

type VoiceCloneDeleteRequest struct {
	VoiceID    string
	Candidate  store.RuntimeModelCandidate
	HTTPClient *http.Client
}

type VoiceCloneDeleteResponse struct {
	VoiceID    string
	RequestID  string
	Result     map[string]any
	StartedAt  time.Time
	FinishedAt time.Time
	DurationMS int64
}

type VoiceCloneDeleter interface {
	DeleteVoiceClone(ctx context.Context, request VoiceCloneDeleteRequest) (VoiceCloneDeleteResponse, error)
}

type ClientError struct {
	Code               string
	Message            string
	Param              string
	Details            map[string]any
	StatusCode         int
	RequestID          string
	ResponseStartedAt  time.Time
	ResponseFinishedAt time.Time
	ResponseDurationMS int64
	Retryable          bool
	Wire               *WireResponse
}

func ErrorWireResponse(err error) *WireResponse {
	var clientErr *ClientError
	if errors.As(err, &clientErr) {
		return clientErr.Wire
	}
	return nil
}

func ErrorParam(err error) string {
	var clientErr *ClientError
	if errors.As(err, &clientErr) {
		return clientErr.Param
	}
	return ""
}

func ErrorDetails(err error) map[string]any {
	var clientErr *ClientError
	if !errors.As(err, &clientErr) || len(clientErr.Details) == 0 {
		return nil
	}
	details := make(map[string]any, len(clientErr.Details))
	for key, value := range clientErr.Details {
		details[key] = value
	}
	return details
}

func (e *ClientError) Error() string {
	if e.Message != "" {
		return e.Message
	}
	return e.Code
}

func IsRetryable(err error) bool {
	var clientErr *ClientError
	return errors.As(err, &clientErr) && clientErr.Retryable
}

func upstreamModelName(candidate store.RuntimeModelCandidate) string {
	if name := strings.TrimSpace(candidate.ProviderModelName); name != "" {
		return name
	}
	return candidate.ModelName
}

func ErrorCode(err error) string {
	var coded interface{ ErrorCode() string }
	if errors.As(err, &coded) && strings.TrimSpace(coded.ErrorCode()) != "" {
		return strings.TrimSpace(coded.ErrorCode())
	}
	var clientErr *ClientError
	if errors.As(err, &clientErr) && clientErr.Code != "" {
		return clientErr.Code
	}
	return "client_error"
}

type ResponseMetadata struct {
	RequestID          string
	ResponseStartedAt  time.Time
	ResponseFinishedAt time.Time
	ResponseDurationMS int64
	StatusCode         int
}

func ErrorResponseMetadata(err error) ResponseMetadata {
	var clientErr *ClientError
	if errors.As(err, &clientErr) {
		return ResponseMetadata{
			RequestID:          clientErr.RequestID,
			ResponseStartedAt:  clientErr.ResponseStartedAt,
			ResponseFinishedAt: clientErr.ResponseFinishedAt,
			ResponseDurationMS: clientErr.ResponseDurationMS,
			StatusCode:         clientErr.StatusCode,
		}
	}
	return ResponseMetadata{}
}

func annotateResponseError(err error, requestID string, startedAt time.Time, finishedAt time.Time) error {
	var clientErr *ClientError
	if !errors.As(err, &clientErr) {
		return err
	}
	if clientErr.RequestID == "" {
		clientErr.RequestID = requestID
	}
	if clientErr.ResponseStartedAt.IsZero() {
		clientErr.ResponseStartedAt = startedAt
	}
	if clientErr.ResponseFinishedAt.IsZero() {
		clientErr.ResponseFinishedAt = finishedAt
	}
	if clientErr.ResponseDurationMS == 0 {
		clientErr.ResponseDurationMS = responseDurationMS(clientErr.ResponseStartedAt, clientErr.ResponseFinishedAt)
	}
	return err
}

func HTTPRetryable(status int) bool {
	return status == http.StatusTooManyRequests || status == http.StatusRequestTimeout || status >= 500
}

func responseDurationMS(startedAt time.Time, finishedAt time.Time) int64 {
	if startedAt.IsZero() || finishedAt.IsZero() {
		return 0
	}
	duration := finishedAt.Sub(startedAt).Milliseconds()
	if duration < 0 {
		return 0
	}
	if duration == 0 && finishedAt.After(startedAt) {
		return 1
	}
	return duration
}
