package search

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"encoding/xml"
	"strconv"
	"strings"
)

func WriteSearchResponse(w http.ResponseWriter, format OutputFormat, resp SearchResponse) error {
	switch format {
	case FormatJSON:
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		return json.NewEncoder(w).Encode(resp)
	case FormatCSV:
		w.Header().Set("Content-Type", "text/csv; charset=utf-8")
		if err := writeCSV(w, resp); err != nil {
			return err
		}
		return nil
	case FormatRSS:
		w.Header().Set("Content-Type", "text/xml; charset=utf-8")
		if err := writeRSS(w, resp); err != nil {
			return err
		}
		return nil
	case FormatHTML:
		w.WriteHeader(http.StatusNotImplemented)
		_, _ = w.Write([]byte("format=html not implemented yet"))
		return nil
	default:
		return fmt.Errorf("unsupported format: %s", format)
	}
}

// csvRowHeader matches the SearXNG CSV writer key order.
var csvRowHeader = []string{"title", "url", "content", "host", "engine", "score", "type"}

func writeCSV(w http.ResponseWriter, resp SearchResponse) error {
	cw := csv.NewWriter(w)
	defer cw.Flush()

	if err := cw.Write(csvRowHeader); err != nil {
		return err
	}

	for _, r := range resp.Results {
		urlStr := ""
		if r.URL != nil {
			urlStr = *r.URL
		}
		host := hostFromURL(urlStr)
		scoreStr := strconv.FormatFloat(r.Score, 'f', -1, 64)
		row := []string{
			r.Title,
			urlStr,
			r.Content,
			host,
			r.Engine,
			scoreStr,
			"result",
		}
		if err := cw.Write(row); err != nil {
			return err
		}
	}

	for _, ans := range resp.Answers {
		title := asString(ans["title"])
		urlStr := asString(ans["url"])
		content := asString(ans["content"])
		engine := asString(ans["engine"])
		scoreStr := scoreString(ans["score"])
		host := hostFromURL(urlStr)

		row := []string{
			title,
			urlStr,
			content,
			host,
			engine,
			scoreStr,
			"answer",
		}
		if err := cw.Write(row); err != nil {
			return err
		}
	}

	for _, s := range resp.Suggestions {
		row := []string{s, "", "", "", "", "", "suggestion"}
		if err := cw.Write(row); err != nil {
			return err
		}
	}

	for _, c := range resp.Corrections {
		row := []string{c, "", "", "", "", "", "correction"}
		if err := cw.Write(row); err != nil {
			return err
		}
	}

	return nil
}

func writeRSS(w http.ResponseWriter, resp SearchResponse) error {
	q := resp.Query
	escapedTitle := xmlEscape("SearXNG search: " + q)
	escapedDesc := xmlEscape("Search results for \"" + q + "\" - SearXNG")
	escapedQueryTerms := xmlEscape(q)

	link := "/search?q=" + url.QueryEscape(q)
	opensearchQuery := fmt.Sprintf(`<opensearch:Query role="request" searchTerms="%s" startPage="1" />`, escapedQueryTerms)

	// SearXNG template uses the number of results for both totalResults and itemsPerPage.
	nr := resp.NumberOfResults

	var items bytes.Buffer
	for _, r := range resp.Results {
		title := xmlEscape(r.Title)
		urlStr := ""
		if r.URL != nil {
			urlStr = *r.URL
		}
		linkEsc := xmlEscape(urlStr)
		desc := xmlEscape(r.Content)

		pub := ""
		if r.Pubdate != nil && strings.TrimSpace(*r.Pubdate) != "" {
			pub = "<pubDate>" + xmlEscape(*r.Pubdate) + "</pubDate>"
		}

		items.WriteString(
			fmt.Sprintf(
				`<item><title>%s</title><type>result</type><link>%s</link><description>%s</description>%s</item>`,
				title,
				linkEsc,
				desc,
				pub,
			),
		)
	}

	xml := fmt.Sprintf(
		`<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0"
     xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
     xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>%s</title>
    <link>%s</link>
    <description>%s</description>
    <opensearch:totalResults>%d</opensearch:totalResults>
    <opensearch:startIndex>1</opensearch:startIndex>
    <opensearch:itemsPerPage>%d</opensearch:itemsPerPage>
    <atom:link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml"/>
    %s
    %s
  </channel>
</rss>
`,
		escapedTitle,
		xmlEscape(link),
		escapedDesc,
		nr,
		nr,
		opensearchQuery,
		items.String(),
	)

	_, err := w.Write([]byte(xml))
	return err
}

func xmlEscape(s string) string {
	var b bytes.Buffer
	_ = xml.EscapeText(&b, []byte(s))
	return b.String()
}

func hostFromURL(urlStr string) string {
	if strings.TrimSpace(urlStr) == "" {
		return ""
	}
	u, err := url.Parse(urlStr)
	if err != nil {
		return ""
	}
	return u.Host
}

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

func scoreString(v any) string {
	switch t := v.(type) {
	case float64:
		return strconv.FormatFloat(t, 'f', -1, 64)
	case float32:
		return strconv.FormatFloat(float64(t), 'f', -1, 64)
	case int:
		return strconv.Itoa(t)
	case int64:
		return strconv.FormatInt(t, 10)
	case json.Number:
		if f, err := t.Float64(); err == nil {
			return strconv.FormatFloat(f, 'f', -1, 64)
		}
		return ""
	default:
		return ""
	}
}

