package publicerror

import (
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
)

const Version = "v1"

type Error struct {
	Code              string         `json:"code"`
	Message           string         `json:"message"`
	Category          string         `json:"category"`
	Source            string         `json:"source"`
	HTTPStatus        int            `json:"httpStatus"`
	Retryable         bool           `json:"retryable"`
	Action            string         `json:"action"`
	RetryAfterSeconds int            `json:"retryAfterSeconds,omitempty"`
	RequestID         string         `json:"requestId,omitempty"`
	TaskID            string         `json:"taskId,omitempty"`
	Details           map[string]any `json:"details,omitempty"`
	Version           string         `json:"version"`
}

func FromFields(code string, message string, status int, retryable bool) Error {
	originalCode := strings.TrimSpace(code)
	code = strings.ToLower(originalCode)
	message = strings.TrimSpace(message)
	if status <= 0 {
		status = statusFromCode(code)
	}
	if strings.HasPrefix(originalCode, "GATEWAY_") {
		return newError(originalCode, safeRequestMessage(message), "gateway", status, retryable, retryAction(retryable))
	}
	if mapped, ok := mappedError(code, message, status, retryable); ok {
		return mapped
	}
	if sensitiveTransportMessage(message) {
		return newError("upstream_connection_interrupted", "The upstream connection was interrupted before a complete response was received.", "upstream", http.StatusBadGateway, true, "retry")
	}
	if code == "" {
		code = defaultCodeForStatus(status)
	} else {
		code = originalCode
	}
	if message == "" {
		message = defaultMessageForStatus(status)
	}
	category := "gateway"
	action := "none"
	if status == http.StatusTooManyRequests {
		category, action, retryable = "rate_limit", "retry_after", true
	} else if status >= 500 {
		category = "upstream"
		if retryable {
			action = "retry"
		}
	} else if status >= 400 {
		category, action = "request", "fix_request"
	}
	return newError(code, message, category, status, retryable, action)
}

func WithIDs(value Error, requestID string, taskID string) Error {
	value.RequestID = strings.TrimSpace(requestID)
	value.TaskID = strings.TrimSpace(taskID)
	return value
}

func mappedError(code string, message string, status int, retryable bool) (Error, bool) {
	switch code {
	case "bad_request", "invalid_request", "invalid_parameter", "unsupported_kind", "unsupported_operation", "unsupported_response_parameter":
		return newError(code, safeRequestMessage(message), "request", http.StatusBadRequest, false, "fix_request"), true
	case "cancelled", "task_cancelled":
		return newError(code, safeRequestMessage(message), "request", http.StatusConflict, false, "none"), true
	case "validation_in_progress":
		return newError(code, firstNonEmptyMessage(message, "New production tasks are paused while validation is running."), "gateway", http.StatusServiceUnavailable, true, "retry_after"), true
	case "traffic_gate_unavailable":
		return newError(code, "The gateway traffic admission service is temporarily unavailable.", "gateway", http.StatusServiceUnavailable, true, "retry"), true
	case "upstream_route_unavailable":
		return newError(code, "No execution pool can currently reach the upstream service.", "gateway", http.StatusServiceUnavailable, true, "retry_after"), true
	case "response_read_error", "stream_read_error", "network", "connection_reset", "upstream_connection_interrupted", "unexpected_eof", "http2_stream_closed", "request_asset_fetch_failed":
		return newError("upstream_connection_interrupted", "The upstream connection was interrupted before a complete response was received.", "upstream", http.StatusBadGateway, true, "retry"), true
	case "upstream_timeout", "upstream_submission_unknown", "timeout", "context_deadline_exceeded":
		return newError("upstream_timeout", "The upstream service did not respond in time. The gateway does not resubmit after submission begins.", "upstream", http.StatusGatewayTimeout, true, "retry_status"), true
	case "gateway_rate_limited":
		return newError("gateway_rate_limited", "The gateway rate limit was reached.", "rate_limit", http.StatusTooManyRequests, true, "retry_after"), true
	case "rate_limit", "upstream_rate_limited", "too_many_requests":
		return newError("upstream_rate_limited", "The upstream service rate limit was reached.", "rate_limit", http.StatusTooManyRequests, true, "retry_after"), true
	case "upstream_unavailable", "upstream_overloaded", "service_unavailable", "bad_gateway", "server_error", "provider_failed":
		if status > 0 && status < 500 {
			break
		}
		return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
	case "invalid_upstream_result":
		return newError("invalid_upstream_result", "The upstream service returned an invalid media result.", "upstream", http.StatusBadGateway, false, "contact_support"), true
	case "upload_invalid_response", "invalid_response", "response_too_large", "invalid_upstream_response", "upstream_invalid_response":
		return newError("upstream_invalid_response", "The upstream service returned an invalid or incomplete response.", "upstream", http.StatusBadGateway, true, "retry"), true
	case "invalid_api_key", "authentication_error", "auth_failed", "missing_credentials", "upstream_auth_failed":
		return newError("upstream_auth_failed", "The upstream service rejected the configured credentials.", "upstream", http.StatusBadGateway, false, "contact_support"), true
	case "storage_write_failed", "storage_config_invalid", "storage_auth_failed", "upload_config_failed", "upload_no_channel", "upload_network", "upload_read_failed", "upload_failed", "local_result_storage_unavailable", "local_static_store_failed", "request_asset_upload_failed":
		return newError("storage_write_failed", "The media asset could not be written to object storage.", "storage", http.StatusServiceUnavailable, true, "retry"), true
	case "storage_read_failed", "upload_source_fetch_failed", "upload_source_read_failed":
		return newError("storage_read_failed", "The media asset could not be read from object storage.", "storage", http.StatusServiceUnavailable, true, "retry"), true
	case "result_url_unavailable":
		return newError("result_url_unavailable", "The generated result URL is temporarily unavailable.", "storage", http.StatusServiceUnavailable, true, "retry"), true
	case "result_materialization_required":
		return newError("result_materialization_required", "The generated result is being migrated to URL storage.", "storage", http.StatusServiceUnavailable, true, "retry"), true
	case "response_format_too_large":
		return newError("response_format_too_large", "The generated media is too large for a synchronous Base64 response. Use the task result URL instead.", "request", http.StatusRequestEntityTooLarge, false, "use_result_url"), true
	case "response_format_capacity_timeout":
		return newError("response_format_capacity_timeout", "Synchronous Base64 response capacity is temporarily unavailable.", "gateway", http.StatusServiceUnavailable, true, "retry"), true
	case "result_response_too_large":
		return newError("result_response_too_large", "The generated result response exceeded the URL-only size limit.", "gateway", http.StatusInternalServerError, false, "contact_support"), true
	case "binary_result_expired", "result_expired":
		return newError("result_expired", "The generated result has expired and must be submitted again.", "storage", http.StatusGone, false, "resubmit"), true
	case "result_unavailable", "request_asset_expired":
		return newError("result_unavailable", "The generated result is no longer available and must be submitted again.", "storage", http.StatusGone, false, "resubmit"), true
	case "binary_result_corrupted", "result_binary_not_materialized":
		return newError("result_corrupted", "The stored result failed an integrity check.", "storage", http.StatusInternalServerError, false, "contact_support"), true
	}
	if status == http.StatusTooManyRequests {
		return newError("upstream_rate_limited", "The upstream service rate limit was reached.", "rate_limit", status, true, "retry_after"), true
	}
	if (status == http.StatusUnauthorized || status == http.StatusForbidden) && (strings.HasPrefix(code, "http_") || strings.Contains(code, "provider")) {
		return newError("upstream_auth_failed", "The upstream service rejected the configured credentials.", "upstream", http.StatusBadGateway, false, "contact_support"), true
	}
	if strings.HasPrefix(code, "http_") && status >= 500 {
		return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
	}
	if strings.HasPrefix(code, "http_") || (code == "provider_failed" && status > 0 && status < 500) {
		return upstreamHTTPError(status, originalUpstreamErrorDetail(code, message, status)), true
	}
	if status >= 500 && (strings.Contains(code, "upstream") || strings.Contains(code, "provider")) {
		return newError("upstream_unavailable", "The upstream service is temporarily unavailable.", "upstream", http.StatusServiceUnavailable, true, "retry"), true
	}
	if status >= 500 {
		return newError("gateway_error", defaultMessageForStatus(status), "gateway", status, retryable, retryAction(retryable)), true
	}
	return Error{}, false
}

func safeRequestMessage(message string) string {
	message = strings.TrimSpace(message)
	if message == "" || sensitiveTransportMessage(message) {
		return "The request parameters are invalid."
	}
	return message
}

func firstNonEmptyMessage(message string, fallback string) string {
	if message = strings.TrimSpace(message); message != "" && !sensitiveTransportMessage(message) {
		return message
	}
	return fallback
}

func retryAction(retryable bool) string {
	if retryable {
		return "retry"
	}
	return "contact_support"
}

func newError(code string, message string, category string, status int, retryable bool, action string) Error {
	return Error{Code: code, Message: message, Category: category, Source: errorSource(code, category), HTTPStatus: status, Retryable: retryable, Action: action, Version: Version}
}

func upstreamHTTPError(status int, details map[string]any) Error {
	if status < 400 || status >= 500 {
		status = http.StatusBadRequest
	}
	var value Error
	switch status {
	case http.StatusBadRequest:
		value = newError("upstream_invalid_request", "The upstream service rejected the request parameters.", "upstream", status, false, "fix_request")
	case http.StatusNotFound:
		value = newError("upstream_not_found", "The upstream service could not find the requested resource.", "upstream", status, false, "contact_support")
	case http.StatusMethodNotAllowed:
		value = newError("upstream_method_not_allowed", "The upstream service did not accept the configured request method.", "upstream", status, false, "contact_support")
	case http.StatusConflict:
		value = newError("upstream_conflict", "The upstream service reported a request conflict.", "upstream", status, false, "fix_request")
	case http.StatusRequestEntityTooLarge:
		value = newError("upstream_payload_too_large", "The upstream service rejected the request because its payload was too large.", "upstream", status, false, "fix_request")
	case http.StatusUnsupportedMediaType:
		value = newError("upstream_unsupported_media_type", "The upstream service rejected the request media type.", "upstream", status, false, "fix_request")
	case http.StatusUnprocessableEntity:
		value = newError("upstream_unprocessable_request", "The upstream service could not process the request parameters.", "upstream", status, false, "fix_request")
	default:
		value = newError("upstream_request_rejected", "The upstream service rejected the request.", "upstream", status, false, "fix_request")
	}
	if message := originalUpstreamMessage(details); message != "" {
		value.Message = message
	}
	value.Details = details
	return value
}

func originalUpstreamMessage(details map[string]any) string {
	upstream, _ := details["upstreamError"].(map[string]any)
	message, _ := upstream["message"].(string)
	return strings.TrimSpace(message)
}

func originalUpstreamErrorDetail(code string, message string, status int) map[string]any {
	if status != http.StatusBadRequest && status != http.StatusUnprocessableEntity {
		return nil
	}
	message = strings.TrimSpace(strings.ToValidUTF8(message, ""))
	if message == "" || sensitiveTransportMessage(message) || json.Valid([]byte(message)) {
		return nil
	}
	lower := strings.ToLower(message)
	for _, marker := range []string{"bearer ", "password=", "secret=", "api_key=", "apikey=", "access_token=", "refresh_token=", "sk-"} {
		if strings.Contains(lower, marker) {
			return nil
		}
	}
	runes := []rune(message)
	if len(runes) > 2048 {
		message = string(runes[:2048])
	}
	return map[string]any{
		"upstreamError": map[string]any{
			"code":       strings.TrimSpace(code),
			"message":    message,
			"statusCode": status,
		},
	}
}

func errorSource(code string, category string) string {
	code = strings.ToLower(strings.TrimSpace(code))
	switch {
	case strings.HasPrefix(code, "upstream_"):
		return "upstream"
	case strings.HasPrefix(code, "gateway_"), category == "gateway", category == "storage":
		return "gateway"
	case category == "request":
		return "client"
	default:
		return "gateway"
	}
}

func sensitiveTransportMessage(message string) bool {
	value := strings.ToLower(message)
	for _, marker := range []string{"read tcp ", "write tcp ", "dial tcp ", "connection reset by peer", "unexpected eof", "http2:", "stream error", "context deadline exceeded"} {
		if strings.Contains(value, marker) {
			return true
		}
	}
	return false
}

func statusFromCode(code string) int {
	if strings.HasPrefix(code, "http_") {
		if status, err := strconv.Atoi(strings.TrimPrefix(code, "http_")); err == nil && status >= 400 && status <= 599 {
			return status
		}
	}
	switch code {
	case "gateway_rate_limited", "rate_limit", "upstream_rate_limited", "too_many_requests":
		return http.StatusTooManyRequests
	case "upstream_timeout", "timeout", "context_deadline_exceeded":
		return http.StatusGatewayTimeout
	case "binary_result_expired", "result_expired", "result_unavailable", "request_asset_expired":
		return http.StatusGone
	case "storage_write_failed", "storage_read_failed", "local_result_storage_unavailable":
		return http.StatusServiceUnavailable
	default:
		return http.StatusBadGateway
	}
}

func defaultCodeForStatus(status int) string {
	switch status {
	case http.StatusBadRequest:
		return "invalid_request"
	case http.StatusUnauthorized:
		return "unauthorized"
	case http.StatusForbidden:
		return "forbidden"
	case http.StatusNotFound:
		return "not_found"
	case http.StatusConflict:
		return "conflict"
	case http.StatusTooManyRequests:
		return "rate_limited"
	default:
		if status >= 500 {
			return "gateway_error"
		}
		return "request_failed"
	}
}

func defaultMessageForStatus(status int) string {
	if value := http.StatusText(status); value != "" {
		return value
	}
	return "Request failed."
}
