// 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.

package engines

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/metamorphosis-dev/kafka/internal/contracts"
)

// QwantImagesEngine searches Qwant Images via the v3 search API.
type QwantImagesEngine struct {
	client *http.Client
}

func (e *QwantImagesEngine) Name() string { return "qwant_images" }

func (e *QwantImagesEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) {
	if e == nil || e.client == nil {
		return contracts.SearchResponse{}, errors.New("qwant_images engine not initialized")
	}
	q := strings.TrimSpace(req.Query)
	if q == "" {
		return contracts.SearchResponse{Query: req.Query}, nil
	}

	args := url.Values{}
	args.Set("q", req.Query)
	args.Set("count", "20")
	args.Set("locale", qwantLocale(req.Language))
	args.Set("safesearch", fmt.Sprintf("%d", req.Safesearch))
	args.Set("offset", fmt.Sprintf("%d", (req.Pageno-1)*20))

	endpoint := "https://api.qwant.com/v3/search/images?" + args.Encode()

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return contracts.SearchResponse{}, err
	}
	httpReq.Header.Set("User-Agent", "kafka/0.1 (compatible; +https://git.ashisgreat.xyz/penal-colony/kafka)")

	resp, err := e.client.Do(httpReq)
	if err != nil {
		return contracts.SearchResponse{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusForbidden {
		return contracts.SearchResponse{
			Query:               req.Query,
			UnresponsiveEngines: [][2]string{{"qwant_images", "captcha_or_js_block"}},
			Results:             []contracts.MainResult{},
			Answers:             []map[string]any{},
			Corrections:         []string{},
			Infoboxes:           []map[string]any{},
			Suggestions:         []string{},
		}, nil
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		io.Copy(io.Discard, io.LimitReader(resp.Body, 16*1024))
		return contracts.SearchResponse{}, fmt.Errorf("qwant_images upstream error: status %d", resp.StatusCode)
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
	if err != nil {
		return contracts.SearchResponse{}, err
	}

	return parseQwantImages(body, req.Query)
}

func parseQwantImages(body []byte, query string) (contracts.SearchResponse, error) {
	var top map[string]any
	if err := json.Unmarshal(body, &top); err != nil {
		return contracts.SearchResponse{}, fmt.Errorf("qwant_images JSON parse error: %w", err)
	}

	status, _ := top["status"].(string)
	if status != "success" {
		return contracts.SearchResponse{
			Query:               query,
			UnresponsiveEngines: [][2]string{{"qwant_images", "api_error"}},
			Results:             []contracts.MainResult{},
			Answers:             []map[string]any{},
			Corrections:         []string{},
			Infoboxes:           []map[string]any{},
			Suggestions:         []string{},
		}, nil
	}

	data, _ := top["data"].(map[string]any)
	result, _ := data["result"].(map[string]any)
	items, _ := result["items"].(map[string]any)
	mainline := items["mainline"]

	rows := toSlice(mainline)
	if len(rows) == 0 {
		return contracts.SearchResponse{
			Query:               query,
			NumberOfResults:    0,
			Results:             []contracts.MainResult{},
			Answers:             []map[string]any{},
			Corrections:         []string{},
			Infoboxes:           []map[string]any{},
			Suggestions:         []string{},
			UnresponsiveEngines: [][2]string{},
		}, nil
	}

	out := make([]contracts.MainResult, 0)
	for _, row := range rows {
		rowMap, ok := row.(map[string]any)
		if !ok {
			continue
		}
		rowType, _ := rowMap["type"].(string)
		if rowType != "images" {
			continue
		}

		rowItems := toSlice(rowMap["items"])
		for _, it := range rowItems {
			itemMap, ok := it.(map[string]any)
			if !ok {
				continue
			}
			title := toString(itemMap["title"])
			resURL := toString(itemMap["url"])
			thumb := toString(itemMap["thumbnail"])
			fullImg := toString(itemMap["media"])
			source := toString(itemMap["source"])

			if resURL == "" && fullImg == "" {
				continue
			}

			// Use the source page URL for the link, full image for thumbnail display.
			linkPtr := resURL
			if linkPtr == "" {
				linkPtr = fullImg
			}
			displayThumb := fullImg
			if displayThumb == "" {
				displayThumb = thumb
			}

			content := source
			if width, ok := itemMap["width"]; ok {
				w := toString(width)
				if h, ok2 := itemMap["height"]; ok2 {
					h2 := toString(h)
					if w != "" && h2 != "" {
						content = w + " × " + h2
						if source != "" {
							content += " — " + source
						}
					}
				}
			}

			out = append(out, contracts.MainResult{
				Template:  "images",
				Title:     title,
				Content:   content,
				URL:       &linkPtr,
				Thumbnail: displayThumb,
				Engine:    "qwant_images",
				Score:     0,
				Category:  "images",
				Engines:   []string{"qwant_images"},
			})
		}
	}

	return contracts.SearchResponse{
		Query:               query,
		NumberOfResults:    len(out),
		Results:             out,
		Answers:             []map[string]any{},
		Corrections:         []string{},
		Infoboxes:           []map[string]any{},
		Suggestions:         []string{},
		UnresponsiveEngines: [][2]string{},
	}, nil
}
