package timeline

import (
	"context"
	"net/http"
	"sort"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/oauth"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/pagination"
	"tangled.org/core/orm"
)

func (t *Timeline) Timeline(w http.ResponseWriter, r *http.Request) {
	user := t.oauth.GetMultiAccountUser(r)

	const timelineTabCookie = "timeline-tab"
	var followingOnly bool
	if _, hasParam := r.URL.Query()["following"]; hasParam {
		followingOnly = r.URL.Query().Get("following") == "true" && user != nil
	} else if user != nil {
		if c, err := r.Cookie(timelineTabCookie); err == nil {
			followingOnly = c.Value == "following"
		}
	}

	if user != nil {
		val := "global"
		if followingOnly {
			val = "following"
		}
		http.SetCookie(w, &http.Cookie{
			Name:     timelineTabCookie,
			Value:    val,
			Path:     "/",
			MaxAge:   365 * 24 * 60 * 60,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
		})
	}

	var userDid string
	if user != nil {
		userDid = user.Did
	}
	timeline, err := db.MakeTimeline(t.db, 50, userDid, followingOnly)
	if err != nil {
		t.logger.Error("failed to make timeline", "err", err)
		t.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
	}

	repos, err := db.GetTopStarredReposLastWeek(t.db)
	if err != nil {
		t.logger.Error("failed to get top starred repos", "err", err)
		t.pages.Notice(w, "topstarredrepos", "Unable to load.")
		return
	}

	gfiLabel, err := db.GetLabelDefinition(t.db, orm.FilterEq("at_uri", t.config.Label.GoodFirstIssue))
	if err != nil {
		// non-fatal
	}

	var notifications []*models.NotificationWithEntity
	if user != nil {
		notifications, err = db.GetNotificationsWithEntities(
			t.db,
			pagination.Page{Limit: 5, Offset: 0},
			orm.FilterEq("recipient_did", user.Did),
		)
		if err != nil {
			t.logger.Error("failed to get notifications for timeline", "err", err)
		}
	}

	var vouchSuggestions []models.VouchSuggestion
	if user != nil {
		vouchSuggestions, err = db.GetVouchSuggestions(t.db, user.Did, 3)
		if err != nil {
			t.logger.Error("failed to get vouch suggestions", "err", err)
		}
		if len(vouchSuggestions) > 0 {
			suggestionDids := make([]syntax.DID, len(vouchSuggestions))
			for i, sv := range vouchSuggestions {
				suggestionDids[i] = syntax.DID(sv.Did)
			}
			relationships, err := db.GetVouchRelationshipsBatch(t.db, syntax.DID(user.Did), suggestionDids)
			if err != nil {
				t.logger.Error("failed to get vouch relationships for suggestions", "err", err)
			} else {
				for i := range vouchSuggestions {
					vouchSuggestions[i].VouchRelationship = relationships[vouchSuggestions[i].Did]
				}
			}
		}
	}

	var recents []pages.RecentItem
	if user != nil {
		recents, err = t.buildRecents(r.Context(), user.Did)
		if err != nil {
			t.logger.Error("failed to build recents for timeline", "err", err)
		}
	}

	var onboarding *models.Onboarding
	if user != nil {
		onboarding, err = db.GetOnboarding(t.db, user.Did)
		if err != nil {
			t.logger.Error("failed to get onboarding status", "err", err)
		}
	}

	var canFocus bool
	if user != nil {
		focusCount, _ := db.CountFocusNotifs(t.db, user.Did)
		canFocus = focusCount > 1
	}

	err = t.pages.Timeline(w, pages.TimelineParams{
		BaseParams:       pages.BaseParamsFromContext(r.Context()),
		Onboarding:       onboarding.Progress(),
		Timeline:         timeline,
		Repos:            repos,
		GfiLabel:         gfiLabel,
		VouchSuggestions: vouchSuggestions,
		Notifications:    notifications,
		Recents:          recents,
		FollowingOnly:    followingOnly,
		RecentBlogPosts:  t.recentPosts,
		ShowNewsletter:   t.showNewsletter(user),
		CanFocus:         canFocus,
	})
	if err != nil {
		t.logger.Error("failed to render timeline", "err", err)
	}
}

func (t *Timeline) buildRecents(ctx context.Context, userDid string) ([]pages.RecentItem, error) {
	links, err := db.GetRecentLinks(t.db, orm.FilterEq("user_did", userDid))
	if err != nil {
		return nil, err
	}
	if len(links) == 0 {
		return nil, nil
	}

	// group targets by type.
	var repoDids, issueAtUris, pullAtUris []string
	for _, l := range links {
		switch l.LinkType {
		case models.RecentLinkTypeRepo:
			repoDids = append(repoDids, l.Target)
		case models.RecentLinkTypeIssue:
			issueAtUris = append(issueAtUris, l.Target)
		case models.RecentLinkTypePull:
			pullAtUris = append(pullAtUris, l.Target)
		}
	}

	// fetch repos by DID.
	repoByDid := make(map[string]*models.Repo)
	if len(repoDids) > 0 {
		fetched, err := db.GetRepos(t.db, orm.FilterIn("repo_did", repoDids))
		if err != nil {
			return nil, err
		}
		for i := range fetched {
			repoByDid[fetched[i].RepoDid] = &fetched[i]
		}
	}

	// fetch issues by aturi
	issueByAtUri := make(map[string]*models.Issue)
	if len(issueAtUris) > 0 {
		issues, err := db.GetIssues(t.db, orm.FilterIn("at_uri", issueAtUris))
		if err != nil {
			return nil, err
		}
		for _, issue := range issues {
			issueByAtUri[issue.AtUri().String()] = &issue
		}
	}

	// fetch pulls by aturi
	pullByAtUri := make(map[string]*models.Pull)
	if len(pullAtUris) > 0 {
		fetched, err := db.GetPullsPaginated(ctx, t.db, pagination.Page{}, orm.FilterIn("at_uri", pullAtUris))
		if err != nil {
			return nil, err
		}
		for _, p := range fetched {
			pullByAtUri[p.AtUri().String()] = p
		}
	}

	// build result in original link order
	var items []pages.RecentItem
	for _, l := range links {
		item := pages.RecentItem{Link: l}
		switch l.LinkType {
		case models.RecentLinkTypeRepo:
			item.Repo = repoByDid[l.Target]
		case models.RecentLinkTypeIssue:
			item.Issue = issueByAtUri[l.Target]
		case models.RecentLinkTypePull:
			item.Pull = pullByAtUri[l.Target]
		}
		// skip if the entity could not be resolved (e.g. deleted).
		if item.Repo == nil && item.Issue == nil && item.Pull == nil {
			continue
		}
		items = append(items, item)
	}

	// re-sort by visited descending to restore recency order after map lookups.
	sort.Slice(items, func(i, j int) bool {
		return items[i].Link.Visited.After(items[j].Link.Visited)
	})

	return items, nil
}

// showNewsletter decides whether the newsletter widget/CTA should render.
// Anonymous visitors always see it (they can dismiss via localStorage);
// logged-in users whose newsletter_preferences row exists (either
// subscribed or dismissed) do not.
func (t *Timeline) showNewsletter(user *oauth.MultiAccountUser) bool {
	if user == nil {
		return true
	}
	pref, err := db.GetNewsletterPref(t.db, user.Did)
	if err != nil {
		t.logger.Error("failed to read newsletter preference", "did", user.Did, "err", err)
		return true
	}
	return pref == nil
}
