package store

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

const (
	storageBinaryPrefixChars      = 16
	storageGenericBase64MinLength = 4096
	storageInvalidBinaryMinLength = 512
	storageJSONSanitizerMaxDepth  = 64
	storagePlaceholderPrefix      = "[GatewayBinary:v1;"
	storageBufferObjectType       = "buffer"
	storageDataURLPrefix          = "data:"
	storageDataURLBase64Marker    = ";base64"
	storageDataURLMaxContentType  = 64
)

var (
	ErrTaskRequestBinaryNotMaterialized = errors.New("task request binary payload was not materialized")
	ErrTaskResultBinaryNotMaterialized  = errors.New("task result binary payload was not materialized")
)

type taskPayloadBinaryError struct {
	target error
	code   string
	count  int
}

func (e *taskPayloadBinaryError) Error() string {
	return fmt.Sprintf("%s: detected %d inline binary value(s)", e.code, e.count)
}

func (e *taskPayloadBinaryError) ErrorCode() string {
	return e.code
}

func (e *taskPayloadBinaryError) Is(target error) bool {
	return target == e.target
}

type storageSanitizeReport struct {
	Value       any
	BinaryCount int
}

// sanitizeJSONForStorage is the final task-domain persistence guard. It always
// returns a detached JSON-compatible value and replaces inline binary payloads
// with a bounded, deterministic placeholder.
func sanitizeJSONForStorage(value any) any {
	return sanitizeJSONForStorageWithReport(value).Value
}

func sanitizeJSONForStorageWithReport(value any) storageSanitizeReport {
	next, count := sanitizeJSONStorageValue(value, nil, 0)
	return storageSanitizeReport{Value: next, BinaryCount: count}
}

func sanitizeJSONStorageValue(value any, path []string, depth int) (any, int) {
	if depth >= storageJSONSanitizerMaxDepth {
		return "[JSON,max-depth]", 0
	}
	switch typed := value.(type) {
	case map[string]any:
		if payload, contentType, ok := storageBufferObjectBytes(typed); ok {
			return storageBinaryPlaceholder(payload, contentType, "buffer"), 1
		}
		next := make(map[string]any, len(typed))
		count := 0
		for key, child := range typed {
			sanitized, childCount := sanitizeJSONStorageValue(child, appendStoragePath(path, key), depth+1)
			next[key] = sanitized
			count += childCount
		}
		return next, count
	case []any:
		if storagePathIsBinary(path) {
			if payload, ok := storageNumberArrayBytes(typed); ok {
				return storageBinaryPlaceholder(payload, "", "buffer"), 1
			}
		}
		next := make([]any, len(typed))
		count := 0
		for index, child := range typed {
			sanitized, childCount := sanitizeJSONStorageValue(child, path, depth+1)
			next[index] = sanitized
			count += childCount
		}
		return next, count
	case []byte:
		return storageBinaryPlaceholder(typed, "", "buffer"), 1
	case string:
		if placeholder, ok := storageStringPlaceholder(typed, path); ok {
			return placeholder, 1
		}
		return typed, 0
	default:
		return value, 0
	}
}

func appendStoragePath(path []string, key string) []string {
	next := make([]string, len(path)+1)
	copy(next, path)
	next[len(path)] = key
	return next
}

func storageStringPlaceholder(value string, path []string) (string, bool) {
	raw := strings.TrimSpace(value)
	if raw == "" || strings.HasPrefix(raw, storagePlaceholderPrefix) {
		return "", false
	}
	encoded, contentType, dataURL := storageBase64StringParts(raw)
	strict := dataURL || storagePathIsBinary(path)
	if !strict && len(encoded) < storageGenericBase64MinLength {
		return "", false
	}
	payload, ok := storageDecodeBase64(encoded)
	if ok {
		encoding := "raw"
		if dataURL {
			encoding = "data-uri"
		}
		return storageBinaryPlaceholder(payload, contentType, encoding), true
	}
	if strict && len(raw) >= storageInvalidBinaryMinLength {
		return storageBinaryPlaceholder([]byte(raw), contentType, "raw"), true
	}
	return "", false
}

func storageBase64StringParts(value string) (encoded string, contentType string, dataURL bool) {
	if !strings.HasPrefix(strings.ToLower(value), storageDataURLPrefix) {
		return value, "", false
	}
	prefix, payload, ok := strings.Cut(value, ",")
	if !ok || !strings.Contains(strings.ToLower(prefix), storageDataURLBase64Marker) {
		return value, "", false
	}
	mediaType := strings.TrimSpace(prefix[len(storageDataURLPrefix):])
	if before, _, found := strings.Cut(mediaType, ";"); found {
		mediaType = before
	}
	if len(mediaType) > storageDataURLMaxContentType || !storageSafeContentType(mediaType) {
		mediaType = ""
	}
	return payload, strings.ToLower(mediaType), true
}

func storageSafeContentType(value string) bool {
	if value == "" {
		return true
	}
	for _, char := range value {
		switch {
		case char >= 'a' && char <= 'z':
		case char >= 'A' && char <= 'Z':
		case char >= '0' && char <= '9':
		case char == '/', char == '.', char == '+', char == '-':
		default:
			return false
		}
	}
	return true
}

func storageDecodeBase64(value string) ([]byte, bool) {
	normalized := removeStorageASCIIWhitespace(value)
	if normalized == "" {
		return nil, false
	}
	for _, encoding := range []*base64.Encoding{
		base64.StdEncoding,
		base64.RawStdEncoding,
		base64.URLEncoding,
		base64.RawURLEncoding,
	} {
		payload, err := encoding.DecodeString(normalized)
		if err != nil || len(payload) == 0 {
			continue
		}
		canonical := encoding.EncodeToString(payload)
		if strings.TrimRight(normalized, "=") == strings.TrimRight(canonical, "=") {
			return payload, true
		}
	}
	return nil, false
}

func removeStorageASCIIWhitespace(value string) string {
	return strings.Map(func(char rune) rune {
		switch char {
		case ' ', '\n', '\r', '\t':
			return -1
		default:
			return char
		}
	}, value)
}

func storagePathIsBinary(path []string) bool {
	if len(path) == 0 {
		return false
	}
	key := normalizeStorageBinaryKey(path[len(path)-1])
	if storageBinaryKey(key) {
		return true
	}
	if len(path) < 2 {
		return false
	}
	parent := normalizeStorageBinaryKey(path[len(path)-2])
	return (parent == "inlinedata" || parent == "binary" || parent == "media") &&
		(key == "data" || key == "content")
}

func normalizeStorageBinaryKey(value string) string {
	return strings.Map(func(char rune) rune {
		switch {
		case char >= 'a' && char <= 'z':
			return char
		case char >= 'A' && char <= 'Z':
			return char + ('a' - 'A')
		case char >= '0' && char <= '9':
			return char
		default:
			return -1
		}
	}, value)
}

func storageBinaryKey(key string) bool {
	return key == "b64" ||
		key == "b64json" ||
		key == "base64" ||
		key == "buffer" ||
		key == "bytes" ||
		strings.Contains(key, "base64") ||
		strings.Contains(key, "buffer") ||
		strings.Contains(key, "binary") ||
		strings.HasSuffix(key, "b64") ||
		strings.HasSuffix(key, "bytes")
}

func storageBufferObjectBytes(value map[string]any) ([]byte, string, bool) {
	kind, _ := value["type"].(string)
	if normalizeStorageBinaryKey(kind) != storageBufferObjectType {
		return nil, "", false
	}
	contentType := firstNonEmpty(
		stringFromAny(value["mime_type"]),
		stringFromAny(value["mimeType"]),
		stringFromAny(value["contentType"]),
	)
	switch data := value["data"].(type) {
	case []byte:
		if len(data) == 0 {
			return nil, "", false
		}
		return append([]byte(nil), data...), contentType, true
	case []any:
		payload, ok := storageNumberArrayBytes(data)
		return payload, contentType, ok
	default:
		return nil, "", false
	}
}

func storageNumberArrayBytes(values []any) ([]byte, bool) {
	if len(values) == 0 {
		return nil, false
	}
	payload := make([]byte, len(values))
	for index, value := range values {
		next, ok := storageByteFromAny(value)
		if !ok {
			return nil, false
		}
		payload[index] = next
	}
	return payload, true
}

func storageByteFromAny(value any) (byte, bool) {
	switch typed := value.(type) {
	case byte:
		return typed, true
	case int:
		if typed >= 0 && typed <= 255 {
			return byte(typed), true
		}
	case int32:
		if typed >= 0 && typed <= 255 {
			return byte(typed), true
		}
	case int64:
		if typed >= 0 && typed <= 255 {
			return byte(typed), true
		}
	case float64:
		asInt := int(typed)
		if typed == float64(asInt) && asInt >= 0 && asInt <= 255 {
			return byte(asInt), true
		}
	case json.Number:
		asInt, err := strconv.ParseInt(string(typed), 10, 16)
		if err == nil && asInt >= 0 && asInt <= 255 {
			return byte(asInt), true
		}
	}
	return 0, false
}

func storageBinaryPlaceholder(payload []byte, contentType string, encoding string) string {
	digest := sha256.Sum256(payload)
	prefix := base64.StdEncoding.EncodeToString(payload)
	if len(prefix) > storageBinaryPrefixChars {
		prefix = prefix[:storageBinaryPrefixChars]
	}
	contentType = strings.ToLower(strings.TrimSpace(contentType))
	if contentType == "" || len(contentType) > 32 || !storageSafeContentType(contentType) {
		contentType = "application/octet-stream"
	}
	switch encoding {
	case "data-uri", "buffer":
	default:
		encoding = "raw"
	}
	return fmt.Sprintf(
		"[GatewayBinary:v1;prefix=%s;sha256=%x;bytes=%d;mime=%s;encoding=%s]",
		prefix,
		digest,
		len(payload),
		contentType,
		encoding,
	)
}
