package pulls

import (
	"context"
	"iter"
	"net/url"
	"slices"
	"time"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/orm"
	"tangled.org/core/tid"

	comatproto "github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/atclient"
	"github.com/bluesky-social/indigo/atproto/syntax"
	lexutil "github.com/bluesky-social/indigo/lex/util"
)

func (s *Pulls) pullLabelDefs(repo *models.Repo) (map[string]*models.LabelDefinition, error) {
	defs, err := db.GetLabelDefinitions(
		s.db,
		orm.FilterIn("at_uri", repo.Labels),
		orm.FilterContains("scope", tangled.RepoPullNSID),
	)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*models.LabelDefinition, len(defs))
	for i := range defs {
		d := defs[i]
		if !slices.Contains(d.Scope, tangled.RepoPullNSID) {
			continue
		}
		out[d.AtUri().String()] = &d
	}
	return out, nil
}

func formLabelEntries(form url.Values, defs map[string]*models.LabelDefinition) iter.Seq2[string, string] {
	return func(yield func(string, string) bool) {
		for key := range defs {
			for _, v := range form[key] {
				if v == "" {
					continue
				}
				if !yield(key, v) {
					return
				}
			}
		}
	}
}

func labelStateFromForm(form url.Values, defs map[string]*models.LabelDefinition) models.LabelState {
	state := models.NewLabelState()
	actx := &models.LabelApplicationCtx{Defs: defs}
	for key, val := range formLabelEntries(form, defs) {
		_ = actx.ApplyLabelOp(state, models.LabelOp{
			Operation:    models.LabelOperationAdd,
			OperandKey:   key,
			OperandValue: val,
		})
	}
	return state
}

func buildCreationLabelOps(
	userDid syntax.DID,
	subject syntax.ATURI,
	rkey string,
	form url.Values,
	defs map[string]*models.LabelDefinition,
	performedAt time.Time,
) []models.LabelOp {
	var ops []models.LabelOp
	for key, val := range formLabelEntries(form, defs) {
		ops = append(ops, models.LabelOp{
			Did:          userDid.String(),
			Rkey:         rkey,
			Subject:      subject,
			Operation:    models.LabelOperationAdd,
			OperandKey:   key,
			OperandValue: val,
			PerformedAt:  performedAt,
		})
	}
	return ops
}

func (s *Pulls) applyCreationLabels(
	ctx context.Context,
	client *atclient.APIClient,
	userDid syntax.DID,
	pull *models.Pull,
	form url.Values,
	repo *models.Repo,
) {
	l := s.logger.With("handler", "applyCreationLabels", "user", userDid)

	defs, err := s.pullLabelDefs(repo)
	if err != nil {
		l.Warn("failed to fetch label defs", "err", err)
		return
	}
	if len(defs) == 0 {
		return
	}

	rkey := tid.TID()
	raw := buildCreationLabelOps(userDid, pull.AtUri(), rkey, form, defs, time.Now())

	valid := make([]models.LabelOp, 0, len(raw))
	for _, op := range raw {
		def := defs[op.OperandKey]

		// validate permissions: only collaborators can apply labels currently
		//
		// TODO: introduce a repo:triage permission
		ok, err := s.acl.HasRepoPermissionErr(ctx, repo, op.Did, "repo:push")
		if err != nil {
			l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey)
			continue
		}
		if !ok {
			l.Warn("forbidden label op", "subject", op.Subject, "key", op.OperandKey)
			continue
		}

		// resolve Handle to DID
		if def.ValueType.IsString() && def.ValueType.IsDidFormat() {
			val := syntax.AtIdentifier(op.OperandValue)
			if val.IsHandle() {
				ident, err := s.idResolver.Directory().Lookup(ctx, val)
				if err != nil {
					l.Warn("failed to resolve handle", "err", err, "subject", op.Subject, "key", op.OperandKey)
				}
				op.OperandValue = ident.DID.String()
			}
		}

		if err := def.ValidateOperandValue(&op); err != nil {
			l.Warn("invalid label op", "err", err, "subject", op.Subject, "key", op.OperandKey)
			continue
		}
		valid = append(valid, op)
	}
	if len(valid) == 0 {
		return
	}

	record := models.LabelOpsAsRecord(valid)
	if _, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
		Collection: tangled.LabelOpNSID,
		Repo:       userDid.String(),
		Rkey:       rkey,
		Record:     &lexutil.LexiconTypeDecoder{Val: &record},
	}); err != nil {
		l.Warn("failed to write label ops to PDS", "err", err, "subject", pull.AtUri())
		return
	}

	if err := s.indexLabelOps(ctx, valid); err != nil {
		l.Warn("failed to index label ops", "err", err, "subject", pull.AtUri())
		if _, err := comatproto.RepoDeleteRecord(context.Background(), client, &comatproto.RepoDeleteRecord_Input{
			Collection: tangled.LabelOpNSID,
			Repo:       userDid.String(),
			Rkey:       rkey,
		}); err != nil {
			l.Warn("failed to rollback label ops record from PDS", "err", err, "subject", pull.AtUri())
		}
		return
	}

	s.notifier.NewPullLabelOp(ctx, userDid, pull, valid)
}

func (s *Pulls) indexLabelOps(ctx context.Context, ops []models.LabelOp) error {
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	for _, op := range ops {
		if _, err := db.AddLabelOp(tx, &op); err != nil {
			return err
		}
	}
	return tx.Commit()
}
