package contracts

import (
	"bytes"
	"encoding/json"
)

// MainResult represents one element of SearXNG's `results` array.
//
// SearXNG returns many additional keys beyond what templates use. To keep the
// contract stable for proxying/merging, we preserve all unknown keys in
// `raw` and re-emit them via MarshalJSON.
type MainResult struct {
	raw map[string]any

	// Common fields used by SearXNG templates (RSS uses: title, url, content, pubdate).
	Template string `json:"template"`
	Title    string `json:"title"`
	Content  string `json:"content"`
	URL      *string `json:"url"`
	Pubdate  *string `json:"pubdate"`

	Engine  string  `json:"engine"`
	Score   float64 `json:"score"`
	Category string `json:"category"`
	Priority string `json:"priority"`

	Positions []int    `json:"positions"`
	Engines   []string `json:"engines"`

	// These fields exist in SearXNG's MainResult base; keep them so downstream
	// callers can generate richer output later.
	OpenGroup  bool `json:"open_group"`
	CloseGroup bool `json:"close_group"`

	// parsed_url in SearXNG is emitted as a tuple; we preserve it as-is.
	ParsedURL any `json:"parsed_url"`
}

func (mr *MainResult) UnmarshalJSON(data []byte) error {
	// Preserve the full object.
	dec := json.NewDecoder(bytes.NewReader(data))
	dec.UseNumber()

	var m map[string]any
	if err := dec.Decode(&m); err != nil {
		return err
	}

	mr.raw = m

	// Fill the typed/common fields (best-effort; don't fail if types differ).
	mr.Template = stringOrEmpty(m["template"])
	mr.Title = stringOrEmpty(m["title"])
	mr.Content = stringOrEmpty(m["content"])
	mr.Engine = stringOrEmpty(m["engine"])
	mr.Category = stringOrEmpty(m["category"])
	mr.Priority = stringOrEmpty(m["priority"])

	if s, ok := stringOrNullable(m["url"]); ok {
		mr.URL = &s
	}
	if s, ok := stringOrNullable(m["pubdate"]); ok {
		mr.Pubdate = &s
	}

	mr.Score = floatOrZero(m["score"])

	if v, ok := sliceOfStrings(m["engines"]); ok {
		mr.Engines = v
	}
	if v, ok := sliceOfInts(m["positions"]); ok {
		mr.Positions = v
	}

	if v, ok := boolOrFalse(m["open_group"]); ok {
		mr.OpenGroup = v
	}
	if v, ok := boolOrFalse(m["close_group"]); ok {
		mr.CloseGroup = v
	}

	mr.ParsedURL = m["parsed_url"]

	return nil
}

func (mr MainResult) MarshalJSON() ([]byte, error) {
	// If we came from upstream JSON, preserve all keys exactly.
	if mr.raw != nil {
		return json.Marshal(mr.raw)
	}

	// Otherwise, marshal the known fields.
	m := map[string]any{
		"template": mr.Template,
		"title":    mr.Title,
		"content":  mr.Content,
		"url":      mr.URL,
		"pubdate":  mr.Pubdate,
		"engine":   mr.Engine,
		"score":    mr.Score,
		"category": mr.Category,
		"priority": mr.Priority,
		"positions": mr.Positions,
		"engines":   mr.Engines,
		"open_group": mr.OpenGroup,
		"close_group": mr.CloseGroup,
		"parsed_url": mr.ParsedURL,
	}
	return json.Marshal(m)
}

func stringOrEmpty(v any) string {
	s, _ := v.(string)
	return s
}

func stringOrNullable(v any) (string, bool) {
	if v == nil {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}

func floatOrZero(v any) float64 {
	switch t := v.(type) {
	case float64:
		return t
	case float32:
		return float64(t)
	case int:
		return float64(t)
	case int64:
		return float64(t)
	case json.Number:
		f, _ := t.Float64()
		return f
	default:
		return 0
	}
}

func boolOrFalse(v any) (bool, bool) {
	b, ok := v.(bool)
	if !ok {
		return false, false
	}
	return b, true
}

func sliceOfStrings(v any) ([]string, bool) {
	raw, ok := v.([]any)
	if !ok {
		return nil, false
	}
	out := make([]string, 0, len(raw))
	for _, item := range raw {
		s, ok := item.(string)
		if !ok {
			return nil, false
		}
		out = append(out, s)
	}
	return out, true
}

func sliceOfInts(v any) ([]int, bool) {
	raw, ok := v.([]any)
	if !ok {
		return nil, false
	}
	out := make([]int, 0, len(raw))
	for _, item := range raw {
		switch t := item.(type) {
		case float64:
			out = append(out, int(t))
		case int:
			out = append(out, t)
		case json.Number:
			i64, err := t.Int64()
			if err != nil {
				return nil, false
			}
			out = append(out, int(i64))
		default:
			return nil, false
		}
	}
	return out, true
}

