package spindle

import (
	"context"
	"errors"

	"github.com/bluesky-social/indigo/atproto/syntax"

	"tangled.org/core/api/org_tangled"
	"tangled.org/core/rbac"
	"tangled.org/core/spindle/db"
	"tangled.org/core/spindle/knotclient"
)

// event order doesn't matter when the knot decides if the record exists
func (s *Spindle) applyCollaboratorMark(ctx context.Context, m db.RecordMark) bool {
	repo, err := s.servedRepo(m.RepoDid)
	if err != nil {
		s.l.Error("looking up a repo for a collaborator mark", "repo", m.RepoDid, "err", err)
		return false
	}
	subject, parseErr := syntax.ParseDID(m.Rkey)
	if repo == nil || parseErr != nil {
		return s.clearRecordMark(m)
	}
	k, err := s.knots.KnotOf(ctx, repo.RepoDid)
	if err == nil {
		err = s.knots.GetRecord(ctx, k, repo.RepoDid, org_tangled.GraphCollaboratorNSID, m.Rkey, nil)
	}
	exists := err == nil
	if err != nil && !errors.Is(err, knotclient.ErrNotFound) {
		return s.failCollaboratorMark(ctx, m, err)
	}
	// keep the knot read out of the lock, it can hang
	err = s.whileServed(repo, func() error {
		if exists {
			return s.trackCollaborator(repo.RepoDid, subject)
		}
		return s.untrackCollaborator(repo.RepoDid, subject)
	})
	if err != nil {
		return s.failCollaboratorMark(ctx, m, err)
	}
	return s.clearRecordMark(m)
}

func (s *Spindle) failCollaboratorMark(ctx context.Context, m db.RecordMark, err error) bool {
	if ctx.Err() == nil {
		s.l.Warn("applying a collaborator record failed", "repo", m.RepoDid, "subject", m.Rkey, "err", err)
		s.failRecordMark(m.RecordRetry)
	}
	return false
}

func (s *Spindle) clearRecordMark(m db.RecordMark) bool {
	if err := s.db.ClearRecordRetryAt(m.RecordRetry, m.Epoch); err != nil {
		s.l.Error("clearing a record mark", "repo", m.RepoDid, "rkey", m.Rkey, "err", err)
		return false
	}
	return true
}

// db row first, otherwise a revoke can't find it, and a failed policy add takes it back out
func (s *Spindle) trackCollaborator(repoDid, subject syntax.DID) error {
	if err := s.db.AddKnotCollaborator(repoDid, subject); err != nil {
		return err
	}
	if err := s.e.AddCollaborator(subject.String(), rbac.ThisServer, repoDid.String()); err != nil {
		return errors.Join(err, s.db.DeleteRepoCollaboratorBySubjectRepo(subject, repoDid))
	}
	return nil
}

func (s *Spindle) untrackCollaborator(repoDid, subject syntax.DID) error {
	if err := s.e.RemoveCollaborator(subject.String(), rbac.ThisServer, repoDid.String()); err != nil {
		return err
	}
	return s.db.DeleteRepoCollaboratorBySubjectRepo(subject, repoDid)
}
