package server

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

	"github.com/go-chi/chi/v5"

	"github.com/operator/command-center/internal/config"
	"github.com/operator/command-center/internal/integrations/feeds"
)

// FeedsDeps bundles what the feed routes need. Constructed in main.go
// from the FeedsManager + secrets store; nil disables the /api/feeds
// surface entirely.
type FeedsDeps struct {
	Manager *config.FeedsDocument
	Secrets feeds.SecretsStore
}

// feedListItem is the JSON shape returned by GET /api/feeds/. Doesn't
// include the URL (which contains the passkey).
type feedListItem struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	TrackerID string `json:"tracker_id,omitempty"`
	Enabled   bool   `json:"enabled"`
	HasURL    bool   `json:"has_url"` // is a URL placed in secrets?
}

func (s *Server) handleListFeeds(w http.ResponseWriter, r *http.Request) {
	if s.feeds == nil || s.feeds.Manager == nil {
		writeJSON(w, http.StatusOK, map[string]any{"count": 0, "feeds": []feedListItem{}})
		return
	}
	out := make([]feedListItem, 0, len(s.feeds.Manager.Feeds))
	for _, f := range s.feeds.Manager.Feeds {
		hasURL := false
		if s.feeds.Secrets != nil {
			if _, err := feeds.ResolveURL(r.Context(), s.feeds.Secrets, f.ID); err == nil {
				hasURL = true
			}
		}
		out = append(out, feedListItem{
			ID: f.ID, Name: f.Name, TrackerID: f.TrackerID, Enabled: f.Enabled, HasURL: hasURL,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"count": len(out), "feeds": out})
}

// handleFetchFeedItems pulls the feed URL out of secrets, fetches the
// RSS XML on-demand (no automatic polling per the operator's
// no-auto-scraping preference), parses it, and returns the items. The
// response URL field has the passkey query param redacted before
// emitting so debug-info dumps don't leak it.
func (s *Server) handleFetchFeedItems(w http.ResponseWriter, r *http.Request) {
	if s.feeds == nil || s.feeds.Manager == nil || s.feeds.Secrets == nil {
		http.Error(w, "feeds not configured", http.StatusServiceUnavailable)
		return
	}
	id := chi.URLParam(r, "id")
	var entry *config.FeedEntry
	for i := range s.feeds.Manager.Feeds {
		if s.feeds.Manager.Feeds[i].ID == id {
			entry = &s.feeds.Manager.Feeds[i]
			break
		}
	}
	if entry == nil {
		http.Error(w, "feed id not found", http.StatusNotFound)
		return
	}
	if !entry.Enabled {
		http.Error(w, "feed is disabled (flip enabled:true in feeds.yaml)", http.StatusServiceUnavailable)
		return
	}

	feedURL, err := feeds.ResolveURL(r.Context(), s.feeds.Secrets, id)
	if err != nil {
		if errors.Is(err, feeds.ErrFeedNotConfigured) {
			http.Error(w, "feed URL not placed in secrets store yet (run cc-set-feed-url)", http.StatusNotFound)
			return
		}
		http.Error(w, "resolve url: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Bound the fetch — TD's feed responds in <1s normally, but we don't
	// want a slow upstream to hold a frontend request indefinitely.
	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()

	channel, err := feeds.Fetch(ctx, http.DefaultClient, feedURL)
	if err != nil {
		http.Error(w, "fetch: "+err.Error(), http.StatusBadGateway)
		return
	}

	// Augment the response with the operator-meaningful entry metadata
	// (id, name, tracker_id) so the frontend doesn't have to cross-
	// reference the list endpoint to know which feed it just refreshed.
	writeJSON(w, http.StatusOK, map[string]any{
		"feed": map[string]any{
			"id":              entry.ID,
			"name":            entry.Name,
			"tracker_id":      entry.TrackerID,
			"source_redacted": redactPasskey(feedURL),
		},
		"channel": channel,
	})
}

// redactPasskey returns the feed URL with the `tp=…` and `passkey=…` and
// related query params replaced with `***`. The result is safe to expose
// in API responses + debug dumps without leaking the passkey embedded in
// the URL.
func redactPasskey(raw string) string {
	parsed, err := url.Parse(raw)
	if err != nil {
		return "(url parse failed)"
	}
	// TD's t.rss uses `;`-separated tokens in the query, so url.Parse
	// parses the whole thing as a single RawQuery. We split manually so
	// passkey-shaped tokens get redacted regardless of separator.
	q := parsed.RawQuery
	if q == "" {
		return parsed.String()
	}
	out := make([]string, 0)
	for _, sep := range []string{";", "&"} {
		if strings.Contains(q, sep) {
			for _, tok := range strings.Split(q, sep) {
				out = append(out, redactToken(tok))
			}
			parsed.RawQuery = strings.Join(out, sep)
			return parsed.String()
		}
	}
	parsed.RawQuery = redactToken(q)
	return parsed.String()
}

func redactToken(tok string) string {
	eq := strings.IndexByte(tok, '=')
	if eq < 0 {
		return tok
	}
	key := strings.ToLower(tok[:eq])
	switch key {
	case "tp", "passkey", "rsskey", "auth", "apikey", "api_key", "uid", "u":
		return tok[:eq] + "=***"
	default:
		return tok
	}
}

// Decoder helper — used by the routes wiring to swallow ResolveURL errors
// from the list endpoint cleanly without dragging in the secrets package
// imports at the call site.
var _ = json.Marshal
