package hmndata

import (
	"context"

	"git.handmade.network/hmn/hmn/src/db"
	"git.handmade.network/hmn/hmn/src/models"
	"git.handmade.network/hmn/hmn/src/oops"
	"git.handmade.network/hmn/hmn/src/perf"
)

type SnippetQuery struct {
	IDs               []int
	OwnerIDs          []int
	ProjectIDs        []int
	Tags              []int
	DiscordMessageIDs []string

	Limit, Offset int // if empty, no pagination
}

type SnippetAndStuff struct {
	Snippet        models.Snippet
	Owner          *models.User
	Asset          *models.Asset          `db:"asset"`
	DiscordMessage *models.DiscordMessage `db:"discord_message"`
	Tags           []*models.Tag
	Projects       []*ProjectAndStuff
}

func FetchSnippets(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	q SnippetQuery,
) ([]SnippetAndStuff, error) {
	perf := perf.ExtractPerf(ctx)
	perf.StartBlock("SQL", "Fetch snippets")
	defer perf.EndBlock()

	tx, err := dbConn.Begin(ctx)
	if err != nil {
		return nil, oops.New(err, "failed to start transaction")
	}
	defer tx.Rollback(ctx)

	var tagSnippetIDs []int
	if len(q.Tags) > 0 {
		// Get snippet IDs with this tag, then use that in the main query
		snippetIDs, err := db.QueryScalar[int](ctx, tx,
			`
			SELECT DISTINCT snippet_id
			FROM
				snippet_tag
				JOIN tag ON snippet_tag.tag_id = tag.id
			WHERE
				tag.id = ANY ($1)
			`,
			q.Tags,
		)
		if err != nil {
			return nil, oops.New(err, "failed to get snippet IDs for tag")
		}

		// special early-out: no snippets found for these tags at all
		if len(snippetIDs) == 0 {
			return nil, nil
		}

		tagSnippetIDs = snippetIDs
	}

	var projectSnippetIDs []int
	if len(q.ProjectIDs) > 0 {
		// Get snippet IDs for these projects, then use that in the main query
		snippetIDs, err := db.QueryScalar[int](ctx, tx,
			`
			SELECT DISTINCT snippet_id
			FROM
				snippet_project
			WHERE
				project_id = ANY ($1)
			`,
			q.ProjectIDs,
		)
		if err != nil {
			return nil, oops.New(err, "failed to get snippet IDs for tag")
		}

		// special early-out: no snippets found for these projects at all
		if len(snippetIDs) == 0 {
			return nil, nil
		}

		projectSnippetIDs = snippetIDs
	}

	var qb db.QueryBuilder
	qb.Add(
		`
		SELECT $columns
		FROM
			snippet
			LEFT JOIN hmn_user AS owner ON snippet.owner_id = owner.id
			LEFT JOIN asset AS avatar ON avatar.id = owner.avatar_asset_id
			LEFT JOIN asset ON snippet.asset_id = asset.id
			LEFT JOIN discord_message ON snippet.discord_message_id = discord_message.id
		WHERE
			TRUE
		`,
	)
	if len(q.IDs) > 0 {
		qb.Add(`AND snippet.id = ANY ($?)`, q.IDs)
	}
	if len(tagSnippetIDs) > 0 {
		qb.Add(`AND snippet.id = ANY ($?)`, tagSnippetIDs)
	}
	if len(projectSnippetIDs) > 0 {
		qb.Add(`AND snippet.id = ANY ($?)`, projectSnippetIDs)
	}
	if len(q.OwnerIDs) > 0 {
		qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
	}
	if len(q.DiscordMessageIDs) > 0 {
		qb.Add(`AND snippet.discord_message_id = ANY ($?)`, q.DiscordMessageIDs)
	}
	if currentUser == nil {
		qb.Add(
			`AND owner.status = $? -- snippet owner is Approved`,
			models.UserStatusApproved,
		)
	} else if !currentUser.IsStaff {
		qb.Add(
			`
			AND (
				owner.status = $? -- snippet owner is Approved
				OR owner.id = $? -- current user is the snippet owner
			)
			`,
			models.UserStatusApproved,
			currentUser.ID,
		)
	}
	qb.Add(`ORDER BY snippet.when DESC, snippet.id ASC`)
	if q.Limit > 0 {
		qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
	}

	type resultRow struct {
		Snippet        models.Snippet         `db:"snippet"`
		Owner          *models.User           `db:"owner"`
		AvatarAsset    *models.Asset          `db:"avatar"`
		Asset          *models.Asset          `db:"asset"`
		DiscordMessage *models.DiscordMessage `db:"discord_message"`
	}

	results, err := db.Query[resultRow](ctx, tx, qb.String(), qb.Args()...)
	if err != nil {
		return nil, oops.New(err, "failed to fetch threads")
	}

	result := make([]SnippetAndStuff, len(results)) // allocate extra space because why not
	snippetIDs := make([]int, len(results))
	for i, row := range results {
		if results[i].Owner != nil {
			results[i].Owner.AvatarAsset = results[i].AvatarAsset
		}
		result[i] = SnippetAndStuff{
			Snippet:        row.Snippet,
			Owner:          row.Owner,
			Asset:          row.Asset,
			DiscordMessage: row.DiscordMessage,
			// no tags! tags next
		}
		snippetIDs[i] = row.Snippet.ID
	}

	// Fetch tags
	type snippetTagRow struct {
		SnippetID int         `db:"snippet_tag.snippet_id"`
		Tag       *models.Tag `db:"tag"`
	}
	snippetTags, err := db.Query[snippetTagRow](ctx, tx,
		`
		SELECT $columns
		FROM
			snippet_tag
			JOIN tag ON snippet_tag.tag_id = tag.id
		WHERE
			snippet_tag.snippet_id = ANY($1)
		`,
		snippetIDs,
	)
	if err != nil {
		return nil, oops.New(err, "failed to fetch tags for snippets")
	}

	// associate tags with snippets
	resultBySnippetId := make(map[int]*SnippetAndStuff)
	for i := range result {
		resultBySnippetId[result[i].Snippet.ID] = &result[i]
	}
	for _, snippetTag := range snippetTags {
		item := resultBySnippetId[snippetTag.SnippetID]
		item.Tags = append(item.Tags, snippetTag.Tag)
	}

	// Fetch projects
	type snippetProjectRow struct {
		SnippetID int `db:"snippet_id"`
		ProjectID int `db:"project_id"`
	}
	snippetProjects, err := db.Query[snippetProjectRow](ctx, tx,
		`
		SELECT $columns
		FROM snippet_project
		WHERE snippet_id = ANY($1)
		`,
		snippetIDs,
	)
	if err != nil {
		return nil, oops.New(err, "failed to fetch project ids for snippets")
	}
	var projectIds []int
	for _, sp := range snippetProjects {
		projectIds = append(projectIds, sp.ProjectID)
	}
	projects, err := FetchProjects(ctx, tx, currentUser, ProjectsQuery{ProjectIDs: projectIds})
	if err != nil {
		return nil, oops.New(err, "failed to fetch projects for snippets")
	}
	projectMap := make(map[int]*ProjectAndStuff)
	for i := range projects {
		projectMap[projects[i].Project.ID] = &projects[i]
	}
	for _, sp := range snippetProjects {
		snip, hasResult := resultBySnippetId[sp.SnippetID]
		proj, hasProj := projectMap[sp.ProjectID]
		if hasResult && hasProj {
			snip.Projects = append(snip.Projects, proj)
		}
	}

	err = tx.Commit(ctx)
	if err != nil {
		return nil, oops.New(err, "failed to commit transaction")
	}

	return result, nil
}

func FetchSnippet(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	snippetID int,
	q SnippetQuery,
) (SnippetAndStuff, error) {
	q.IDs = []int{snippetID}
	q.Limit = 1
	q.Offset = 0

	res, err := FetchSnippets(ctx, dbConn, currentUser, q)
	if err != nil {
		return SnippetAndStuff{}, oops.New(err, "failed to fetch snippet")
	}

	if len(res) == 0 {
		return SnippetAndStuff{}, db.NotFound
	}

	return res[0], nil
}

func FetchSnippetForDiscordMessage(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	discordMessageID string,
	q SnippetQuery,
) (SnippetAndStuff, error) {
	q.DiscordMessageIDs = []string{discordMessageID}
	q.Limit = 1
	q.Offset = 0

	res, err := FetchSnippets(ctx, dbConn, currentUser, q)
	if err != nil {
		return SnippetAndStuff{}, oops.New(err, "failed to fetch snippet for Discord message")
	}

	if len(res) == 0 {
		return SnippetAndStuff{}, db.NotFound
	}

	return res[0], nil
}
