package web

import (
	"encoding/json"
	"encoding/xml"
	"html"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"pete/internal/storage"
)

// feedItemLimit caps how many stories each outbound feed carries.
const feedItemLimit = 50

// feedCacheControl is a short cache window; feeds refresh a few times an hour.
const feedCacheControl = "public, max-age=300"

const (
	rssContentNS = "http://purl.org/rss/1.0/modules/content/"
	rssAtomNS    = "http://www.w3.org/2005/Atom"
	jsonFeedVer  = "https://jsonfeed.org/version/1.1"
)

// --- RSS 2.0 ---

type rssFeedXML struct {
	XMLName   xml.Name   `xml:"rss"`
	Version   string     `xml:"version,attr"`
	ContentNS string     `xml:"xmlns:content,attr"`
	AtomNS    string     `xml:"xmlns:atom,attr"`
	Channel   rssChannel `xml:"channel"`
}

type rssChannel struct {
	Title         string      `xml:"title"`
	Link          string      `xml:"link"`
	Description   string      `xml:"description"`
	Language      string      `xml:"language,omitempty"`
	LastBuildDate string      `xml:"lastBuildDate,omitempty"`
	Generator     string      `xml:"generator,omitempty"`
	AtomLink      rssAtomLink `xml:"atom:link"`
	Items         []rssItem   `xml:"item"`
}

type rssAtomLink struct {
	Href string `xml:"href,attr"`
	Rel  string `xml:"rel,attr"`
	Type string `xml:"type,attr"`
}

type rssItem struct {
	Title       string      `xml:"title"`
	Link        string      `xml:"link"`
	GUID        rssGUID     `xml:"guid"`
	PubDate     string      `xml:"pubDate,omitempty"`
	Category    string      `xml:"category,omitempty"`
	Description string      `xml:"description"`
	Content     *rssContent `xml:"content:encoded,omitempty"`
}

type rssGUID struct {
	IsPermaLink string `xml:"isPermaLink,attr"`
	Value       string `xml:",chardata"`
}

type rssContent struct {
	Value string `xml:",cdata"`
}

// --- JSON Feed 1.1 ---

type jsonFeed struct {
	Version     string         `json:"version"`
	Title       string         `json:"title"`
	HomePageURL string         `json:"home_page_url"`
	FeedURL     string         `json:"feed_url"`
	Description string         `json:"description,omitempty"`
	Items       []jsonFeedItem `json:"items"`
}

type jsonFeedItem struct {
	ID            string           `json:"id"`
	URL           string           `json:"url"`
	Title         string           `json:"title"`
	ContentText   string           `json:"content_text,omitempty"`
	Summary       string           `json:"summary,omitempty"`
	Image         string           `json:"image,omitempty"`
	DatePublished string           `json:"date_published,omitempty"`
	Authors       []jsonFeedAuthor `json:"authors,omitempty"`
	Tags          []string         `json:"tags,omitempty"`
}

type jsonFeedAuthor struct {
	Name string `json:"name"`
}

// handleFeedXML serves an RSS 2.0 feed for the given channel ("" = all channels).
func (s *Server) handleFeedXML(w http.ResponseWriter, r *http.Request, channel string) {
	stories, err := storage.ListForFeed(channel, feedItemLimit)
	if err != nil {
		slog.Error("web: feed query failed", "channel", channel, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	base := s.publicBase(r)
	title, homeURL, desc := s.feedMeta(channel, base)
	selfURL := base + feedPath(channel, "xml")

	feed := rssFeedXML{
		Version:   "2.0",
		ContentNS: rssContentNS,
		AtomNS:    rssAtomNS,
		Channel: rssChannel{
			Title:       title,
			Link:        homeURL,
			Description: desc,
			Language:    "en",
			Generator:   "Pete",
			AtomLink:    rssAtomLink{Href: selfURL, Rel: "self", Type: "application/rss+xml"},
			Items:       make([]rssItem, 0, len(stories)),
		},
	}
	if len(stories) > 0 {
		feed.Channel.LastBuildDate = feedTime(stories[0]).UTC().Format(time.RFC1123Z)
	}
	for _, st := range stories {
		link := storyLink(st)
		item := rssItem{
			Title:       st.Headline,
			Link:        link,
			GUID:        rssGUID{IsPermaLink: "false", Value: st.GUID},
			PubDate:     feedTime(st).UTC().Format(time.RFC1123Z),
			Category:    st.Channel,
			Description: st.Lede,
		}
		if html := contentToHTML(st.Content); html != "" {
			item.Content = &rssContent{Value: html}
		}
		feed.Channel.Items = append(feed.Channel.Items, item)
	}

	w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
	w.Header().Set("Cache-Control", feedCacheControl)
	if _, err := w.Write([]byte(xml.Header)); err != nil {
		return
	}
	enc := xml.NewEncoder(w)
	enc.Indent("", "  ")
	if err := enc.Encode(feed); err != nil {
		slog.Error("web: rss encode failed", "err", err)
	}
}

// handleFeedJSON serves a JSON Feed 1.1 for the given channel ("" = all).
func (s *Server) handleFeedJSON(w http.ResponseWriter, r *http.Request, channel string) {
	stories, err := storage.ListForFeed(channel, feedItemLimit)
	if err != nil {
		slog.Error("web: feed query failed", "channel", channel, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	base := s.publicBase(r)
	title, homeURL, desc := s.feedMeta(channel, base)

	feed := jsonFeed{
		Version:     jsonFeedVer,
		Title:       title,
		HomePageURL: homeURL,
		FeedURL:     base + feedPath(channel, "json"),
		Description: desc,
		Items:       make([]jsonFeedItem, 0, len(stories)),
	}
	for _, st := range stories {
		item := jsonFeedItem{
			ID:            st.GUID,
			URL:           storyLink(st),
			Title:         st.Headline,
			ContentText:   strings.TrimSpace(st.Content),
			Summary:       st.Lede,
			Image:         st.ImageURL,
			DatePublished: feedTime(st).UTC().Format(time.RFC3339),
			Tags:          []string{st.Channel},
		}
		if item.ContentText == "" {
			item.ContentText = st.Lede
		}
		if st.Source != "" {
			item.Authors = []jsonFeedAuthor{{Name: st.Source}}
		}
		feed.Items = append(feed.Items, item)
	}

	w.Header().Set("Content-Type", "application/feed+json; charset=utf-8")
	w.Header().Set("Cache-Control", feedCacheControl)
	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")
	if err := enc.Encode(feed); err != nil {
		slog.Error("web: json feed encode failed", "err", err)
	}
}

// feedMeta returns the title, home-page URL, and description for a feed. An
// empty channel is the site-wide feed; a slug scopes to that channel.
func (s *Server) feedMeta(channel, base string) (title, homeURL, desc string) {
	site := s.cfg.SiteTitle
	if site == "" {
		site = "Pete"
	}
	if channel == "" {
		return site, base + "/", "The latest across every channel."
	}
	ch := channelBySlug(channel)
	return site + " — " + ch.Title, base + "/" + channel, ch.Blurb
}

// feedPath is the site-relative path of a feed, e.g. "/feed.xml" or
// "/gaming/feed.json".
func feedPath(channel, ext string) string {
	if channel == "" {
		return "/feed." + ext
	}
	return "/" + channel + "/feed." + ext
}

// storyLink is the canonical outbound link for a story: its canonical URL when
// known, otherwise the raw article URL.
func storyLink(s storage.Story) string {
	if s.URLCanonical != "" {
		return s.URLCanonical
	}
	return s.ArticleURL
}

// feedTime picks the best timestamp for a story: its published date when
// present, otherwise when Pete first saw it.
func feedTime(s storage.Story) time.Time {
	if s.PublishedAt > 0 {
		return time.Unix(s.PublishedAt, 0)
	}
	return time.Unix(s.SeenAt, 0)
}

// channelBySlug looks up a channel by slug, falling back to a bare title-cased
// entry so an unknown slug never panics.
func channelBySlug(slug string) Channel {
	for _, ch := range channels {
		if ch.Slug == slug {
			return ch
		}
	}
	return Channel{Slug: slug, Title: slug, Theme: slug}
}

// publicBase returns the site's public origin (scheme://host, no trailing
// slash). It prefers the configured base_url and falls back to the request's
// host so feeds still carry absolute URLs when base_url is unset.
func (s *Server) publicBase(r *http.Request) string {
	if b := strings.TrimRight(s.cfg.BaseURL, "/"); b != "" {
		return b
	}
	scheme := "http"
	if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
		scheme = "https"
	}
	return scheme + "://" + r.Host
}

// contentToHTML turns Pete's stored plain-text article body (paragraphs split by
// blank lines, soft line breaks inside) into simple, escaped HTML for RSS
// content:encoded. Returns "" when there is no body.
func contentToHTML(text string) string {
	text = strings.TrimSpace(text)
	if text == "" {
		return ""
	}
	var b strings.Builder
	for _, para := range strings.Split(text, "\n\n") {
		para = strings.TrimSpace(para)
		if para == "" {
			continue
		}
		b.WriteString("<p>")
		b.WriteString(strings.ReplaceAll(html.EscapeString(para), "\n", "<br>"))
		b.WriteString("</p>")
	}
	return b.String()
}
