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

import (
	"embed"
	"encoding/xml"
	"html"
	"html/template"
	"io/fs"
	"net/http"
	"strconv"
	"strings"

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

//go:embed all:templates
var templatesFS embed.FS

//go:embed all:static
var staticFS embed.FS

// PageData holds all data passed to templates.
type PageData struct {
	SourceURL           string
	Query               string
	Pageno              int
	PrevPage            int
	NextPage            int
	HasNext             bool
	NumberOfResults     int
	Results             []ResultView
	Answers             []string
	Corrections         []string
	Suggestions         []string
	Infoboxes           []InfoboxView
	UnresponsiveEngines [][2]string
	PageNumbers         []PageNumber
	ShowHeader          bool
	IsImageSearch       bool
	// New fields for three-column layout
	Categories         []string
	CategoryIcons     map[string]string
	DisabledCategories []string
	ActiveCategory    string
	TimeFilters       []FilterOption
	TypeFilters       []FilterOption
	ActiveTime        string
	ActiveType        string
}

// ResultView is a template-friendly wrapper around a MainResult.
type ResultView struct {
	contracts.MainResult
	// TemplateName is the actual template to dispatch to, computed from Template.
	// "videos" maps to "video_item", everything else maps to "result_item".
	TemplateName string
	// SafeTitle and SafeContent are HTML-unescaped versions for rendering.
	// The API returns HTML entities which Go templates escape by default.
	SafeTitle   template.HTML
	SafeContent template.HTML
}

// PageNumber represents a numbered pagination button.
type PageNumber struct {
	Num        int
	IsCurrent  bool
}

// InfoboxView is a template-friendly infobox.
type InfoboxView struct {
	Title   string
	Content string
	ImgSrc  string
}

// FilterOption represents a filter radio option for the sidebar.
type FilterOption struct {
	Label string
	Value string
}

var (
	tmplFull    *template.Template
	tmplIndex   *template.Template
	tmplFragment *template.Template
	tmplPreferences *template.Template
)

func init() {
	// Strip the leading "templates/" prefix from embed paths.
	tmplFS, _ := fs.Sub(templatesFS, "templates")

	funcMap := template.FuncMap{
		"urlquery": template.URLQueryEscaper,
	}

	tmplFull = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS,
		"base.html", "results.html", "results_inner.html", "result_item.html", "video_item.html", "image_item.html",
	))
	tmplIndex = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS,
		"base.html", "index.html",
	))
	tmplFragment = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS,
		"results.html", "results_inner.html", "result_item.html", "video_item.html", "image_item.html",
	))
	tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS,
		"base.html", "preferences.html",
	))
}

// StaticFS returns the embedded static file system for serving CSS/JS/images.
func StaticFS() (fs.FS, error) {
	return fs.Sub(staticFS, "static")
}

// OpenSearchXML returns the OpenSearch description XML with the base URL
// safely embedded via xml.EscapeText (no raw string interpolation).
func OpenSearchXML(baseURL string) ([]byte, error) {
	tmplFS, _ := fs.Sub(templatesFS, "templates")
	data, err := fs.ReadFile(tmplFS, "opensearch.xml")
	if err != nil {
		return nil, err
	}

	var buf strings.Builder
	xml.Escape(&buf, []byte(baseURL))
	escapedBaseURL := buf.String()

	result := strings.ReplaceAll(string(data), "{baseUrl}", escapedBaseURL)
	return []byte(result), nil
}

// FromResponse builds PageData from a search response and request params.
func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData {
	// Set defaults
	if activeCategory == "" {
		activeCategory = "all"
	}

	pd := PageData{
		Query:               query,
		Pageno:              pageno,
		NumberOfResults:     resp.NumberOfResults,
		UnresponsiveEngines: resp.UnresponsiveEngines,

		// New: categories with icons
		Categories: []string{"all", "news", "images", "videos", "maps"},
		DisabledCategories: []string{"shopping", "music", "weather"},
		CategoryIcons: map[string]string{
			"all":      "🌐",
			"news":     "📰",
			"images":   "🖼️",
			"videos":   "🎬",
			"maps":     "🗺️",
			"shopping": "🛒",
			"music":    "🎵",
			"weather":  "🌤️",
		},
		ActiveCategory: activeCategory,
		IsImageSearch:  activeCategory == "images",

		// Time filters
		TimeFilters: []FilterOption{
			{Label: "Any time", Value: ""},
			{Label: "Past hour", Value: "h"},
			{Label: "Past 24 hours", Value: "d"},
			{Label: "Past week", Value: "w"},
			{Label: "Past month", Value: "m"},
			{Label: "Past year", Value: "y"},
		},
		ActiveTime: activeTime,

		// Type filters
		TypeFilters: []FilterOption{
			{Label: "All results", Value: ""},
			{Label: "News", Value: "news"},
			{Label: "Videos", Value: "video"},
			{Label: "Images", Value: "image"},
		},
		ActiveType: activeType,
	}

	// Convert results.
	pd.Results = make([]ResultView, len(resp.Results))
	for i, r := range resp.Results {
		tmplName := "result_item"
		if r.Template == "videos" {
			tmplName = "video_item"
		}
		// Sanitize URLs to prevent javascript:/data: scheme injection.
		if r.URL != nil {
			safe := util.SanitizeResultURL(*r.URL)
			r.URL = &safe
		}
		r.Thumbnail = util.SanitizeResultURL(r.Thumbnail)
		pd.Results[i] = ResultView{
			MainResult:   r,
			TemplateName: tmplName,
			SafeTitle:    template.HTML(html.UnescapeString(r.Title)),
			SafeContent:  template.HTML(html.UnescapeString(r.Content)),
		}
	}

	// Convert answers (they're map[string]any — extract string values).
	for _, a := range resp.Answers {
		if s, ok := a["answer"].(string); ok && s != "" {
			pd.Answers = append(pd.Answers, s)
		}
	}

	pd.Corrections = resp.Corrections
	pd.Suggestions = resp.Suggestions

	// Convert infoboxes.
	for _, ib := range resp.Infoboxes {
		iv := InfoboxView{}
		if v, ok := ib["infobox"].(string); ok {
			iv.Content = v
		}
		if v, ok := ib["title"].(string); ok {
			iv.Title = v
		}
		if v, ok := ib["img_src"].(string); ok {
			iv.ImgSrc = util.SanitizeResultURL(v)
		}
		if iv.Title != "" || iv.Content != "" {
			pd.Infoboxes = append(pd.Infoboxes, iv)
		}
	}

	// Pagination.
	pd.PrevPage = pageno - 1
	if pd.PrevPage < 1 {
		pd.PrevPage = 1
	}
	pd.NextPage = pageno + 1
	// Assume there are more results if we got results on this page.
	pd.HasNext = len(resp.Results) > 0

	// Build page number list.
	pstart := 1
	pend := 10
	if pageno > 5 {
		pstart = pageno - 4
		pend = pageno + 5
	}
	if pstart < 1 {
		pstart = 1
	}
	for x := pstart; x <= pend; x++ {
		pd.PageNumbers = append(pd.PageNumbers, PageNumber{Num: x, IsCurrent: x == pageno})
	}

	return pd
}

// RenderIndex renders the homepage (search box only).
func RenderIndex(w http.ResponseWriter, sourceURL string) error {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	return tmplIndex.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL})
}

// RenderSearch renders the full search results page (with base layout).
func RenderSearch(w http.ResponseWriter, data PageData) error {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	data.ShowHeader = true
	return tmplFull.ExecuteTemplate(w, "base", data)
}

// RenderSearchFragment renders only the results fragment for HTMX requests.
func RenderSearchFragment(w http.ResponseWriter, data PageData) error {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	return tmplFragment.ExecuteTemplate(w, "results_inner", data)
}

// IsHTMXRequest checks if the request is an HTMX partial request.
func IsHTMXRequest(r *http.Request) bool {
	return r.Header.Get("HX-Request") == "true"
}

// FormatQuery parses format, pageno, and q from an HTTP request.
func FormatQuery(r *http.Request) (format string, pageno int, query string) {
	format = r.FormValue("format")
	if format == "" {
		format = "html"
	}
	pageno, _ = strconv.Atoi(r.FormValue("pageno"))
	if pageno < 1 {
		pageno = 1
	}
	query = strings.TrimSpace(r.FormValue("q"))
	return
}

// RenderSearch decides between full page or fragment based on HTMX header.
func RenderSearchAuto(w http.ResponseWriter, r *http.Request, data PageData) error {
	if IsHTMXRequest(r) {
		return RenderSearchFragment(w, data)
	}
	return RenderSearch(w, data)
}

// RenderPreferences renders the full preferences page.
func RenderPreferences(w http.ResponseWriter, sourceURL string) error {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL})
}

