// kafka — a privacy-respecting metasearch engine
// Copyright (C) 2026-present metamorphosis-dev
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package contracts

import (
	"bytes"
	"encoding/json"
)

// MainResult represents one element of the `results` array.
// Unknown keys are preserved in `raw` and re-emitted via MarshalJSON.
type MainResult struct {
	raw map[string]any

	Template  string `json:"template"`
	Title     string `json:"title"`
	Content   string `json:"content"`
	URL       *string `json:"url"`
	Pubdate   *string `json:"pubdate"`
	Thumbnail string  `json:"thumbnail"`

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

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

	OpenGroup  bool `json:"open_group"`
	CloseGroup bool `json:"close_group"`

	ParsedURL any `json:"parsed_url"`
}

func (mr *MainResult) UnmarshalJSON(data []byte) error {
	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

	mr.Template = stringOrEmpty(m["template"])
	mr.Title = stringOrEmpty(m["title"])
	mr.Content = stringOrEmpty(m["content"])
	mr.Engine = stringOrEmpty(m["engine"])
	mr.Thumbnail = stringOrEmpty(m["thumbnail"])
	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 mr.raw != nil {
		return json.Marshal(mr.raw)
	}

	m := map[string]any{
		"template":  mr.Template,
		"title":     mr.Title,
		"content":   mr.Content,
		"url":       mr.URL,
		"pubdate":   mr.Pubdate,
		"thumbnail": mr.Thumbnail,
		"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
}

