package pinterest

import (
	"encoding/json"
	"fmt"
	"strings"

	"pinscrape-allgo/internal/util"
)

// BuildSearchURL replicates the exact request URL produced by the Python
// implementation (pinscrape/v2.py search/search_descriptions), including the
// double-encoding fix chain. Kept pure for golden tests against Python output.
func BuildSearchURL(baseURL, query string, pageSize int) (url string, sourceURL string) {
	sourceURL = fmt.Sprintf("/search/pins/?q=%s&rs=typed", util.Quote(query))

	payload := buildPayloadString(query, sourceURL, pageSize)

	encoded := util.QuotePlus(strings.ReplaceAll(payload, " ", ""))
	encoded = strings.ReplaceAll(encoded, "%2520", "%20")
	encoded = strings.ReplaceAll(encoded, "%252F", "%2F")
	encoded = strings.ReplaceAll(encoded, "%253F", "%3F")
	encoded = strings.ReplaceAll(encoded, "%252520", "%2520")
	encoded = strings.ReplaceAll(encoded, "%253D", "%3D")
	encoded = strings.ReplaceAll(encoded, "%2526", "%26")

	epoch := util.CurrentEpochMs()
	url = fmt.Sprintf("%s/resource/BaseSearchResource/get/?source_url=%s&data=%s&_=%d",
		baseURL, util.QuotePlus(sourceURL), encoded, epoch)
	return url, sourceURL
}

// buildPayloadString emits the payload JSON with Python json.dumps key order so
// the encoded output is byte-identical to the reference implementation.
func buildPayloadString(query, sourceURL string, pageSize int) string {
	var sb strings.Builder
	sb.WriteString(`{"options": {`)
	sb.WriteString(`"applied_unified_filters": null, `)
	sb.WriteString(`"appliedProductFilters": "---", `)
	sb.WriteString(`"article": null, `)
	sb.WriteString(`"auto_correction_disabled": false, `)
	sb.WriteString(`"corpus": null, `)
	sb.WriteString(`"customized_rerank_type": null, `)
	sb.WriteString(`"domains": null, `)
	sb.WriteString(`"filters": null, `)
	sb.WriteString(`"journey_depth": null, `)
	fmt.Fprintf(&sb, `"page_size": "%d", `, pageSize)
	sb.WriteString(`"price_max": null, `)
	sb.WriteString(`"price_min": null, `)
	sb.WriteString(`"query_pin_sigs": null, `)
	fmt.Fprintf(&sb, `"query": %s, `, jsonString(util.Quote(query)))
	sb.WriteString(`"redux_normalize_feed": true, `)
	sb.WriteString(`"request_params": null, `)
	sb.WriteString(`"rs": "typed", `)
	sb.WriteString(`"scope": "pins", `)
	sb.WriteString(`"selected_one_bar_modules": null, `)
	sb.WriteString(`"source_id": null, `)
	sb.WriteString(`"source_module_id": null, `)
	sb.WriteString(`"seoDrawerEnabled": false, `)
	fmt.Fprintf(&sb, `"source_url": %s, `, jsonString(util.QuotePlus(sourceURL)))
	sb.WriteString(`"top_pin_id": null, `)
	sb.WriteString(`"top_pin_ids": null`)
	sb.WriteString(`}, `)
	sb.WriteString(`"context": {}}`)
	return sb.String()
}

// jsonString escapes a string the way Python json.dumps does for ASCII input.
func jsonString(s string) string {
	b, _ := json.Marshal(s)
	return string(b)
}

type PinImage struct {
	URL string `json:"url"`
}

type PinResult struct {
	ID          string              `json:"id"`
	Title       json.RawMessage     `json:"title"`
	GridTitle   json.RawMessage     `json:"grid_title"`
	SeoAltText  string              `json:"seo_alt_text"`
	Description string              `json:"description"`
	Images      map[string]PinImage `json:"images"`
}

type SearchData struct {
	Results []PinResult `json:"results"`
}

type ResourceResponse struct {
	Data *SearchData `json:"data"`
}

type SearchResponse struct {
	ResourceResponse ResourceResponse `json:"resource_response"`
}

type Result struct {
	Title       string
	ImageURL    string
	Description string
}

type Descriptions struct {
	Images       []Result
	Descriptions []string
}

func ParseSearchResponse(body []byte) (*SearchResponse, error) {
	var resp SearchResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, err
	}
	return &resp, nil
}

func rawToString(raw json.RawMessage, fallback string) string {
	if len(raw) == 0 {
		return fallback
	}
	var s string
	if err := json.Unmarshal(raw, &s); err == nil {
		if s != "" {
			return s
		}
		return fallback
	}
	var obj struct {
		Text   string `json:"text"`
		Format string `json:"format"`
	}
	if err := json.Unmarshal(raw, &obj); err == nil {
		if obj.Text != "" {
			return obj.Text
		}
		if obj.Format != "" {
			return obj.Format
		}
	}
	return fallback
}

// Extract converts a parsed response into images and descriptions in one pass
// (merged search: both outputs come from the same request).
func Extract(resp *SearchResponse, fallbackTitle string) Descriptions {
	var out Descriptions
	if resp == nil || resp.ResourceResponse.Data == nil {
		return out
	}
	for _, r := range resp.ResourceResponse.Data.Results {
		if r.Images == nil {
			continue
		}
		orig, ok := r.Images["orig"]
		if !ok || orig.URL == "" {
			continue
		}
		title := rawToString(r.Title, "")
		if title == "" {
			title = rawToString(r.GridTitle, "")
		}
		if title == "" {
			title = r.SeoAltText
		}
		if title == "" {
			title = fallbackTitle
		}
		desc := r.Description
		out.Images = append(out.Images, Result{Title: title, ImageURL: orig.URL, Description: desc})
		if strings.TrimSpace(desc) != "" {
			out.Descriptions = append(out.Descriptions, strings.TrimSpace(desc))
		}
	}
	return out
}
