/*
   GoToSocial
   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU Affero General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package typeutils

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/url"

	"github.com/go-fed/activity/streams"
	"github.com/go-fed/activity/streams/vocab"
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)

// Converts a gts model account into an Activity Streams person type, following
// the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/
func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
	// first check if we have this person in our asCache already
	if personI, err := c.asCache.Fetch(a.ID); err == nil {
		if person, ok := personI.(vocab.ActivityStreamsPerson); ok {
			// we have it, so just return it as-is
			return person, nil
		}
	}

	person := streams.NewActivityStreamsPerson()

	// id should be the activitypub URI of this user
	// something like https://example.org/users/example_user
	profileIDURI, err := url.Parse(a.URI)
	if err != nil {
		return nil, err
	}
	idProp := streams.NewJSONLDIdProperty()
	idProp.SetIRI(profileIDURI)
	person.SetJSONLDId(idProp)

	// following
	// The URI for retrieving a list of accounts this user is following
	followingURI, err := url.Parse(a.FollowingURI)
	if err != nil {
		return nil, err
	}
	followingProp := streams.NewActivityStreamsFollowingProperty()
	followingProp.SetIRI(followingURI)
	person.SetActivityStreamsFollowing(followingProp)

	// followers
	// The URI for retrieving a list of this user's followers
	followersURI, err := url.Parse(a.FollowersURI)
	if err != nil {
		return nil, err
	}
	followersProp := streams.NewActivityStreamsFollowersProperty()
	followersProp.SetIRI(followersURI)
	person.SetActivityStreamsFollowers(followersProp)

	// inbox
	// the activitypub inbox of this user for accepting messages
	inboxURI, err := url.Parse(a.InboxURI)
	if err != nil {
		return nil, err
	}
	inboxProp := streams.NewActivityStreamsInboxProperty()
	inboxProp.SetIRI(inboxURI)
	person.SetActivityStreamsInbox(inboxProp)

	// outbox
	// the activitypub outbox of this user for serving messages
	outboxURI, err := url.Parse(a.OutboxURI)
	if err != nil {
		return nil, err
	}
	outboxProp := streams.NewActivityStreamsOutboxProperty()
	outboxProp.SetIRI(outboxURI)
	person.SetActivityStreamsOutbox(outboxProp)

	// featured posts
	// Pinned posts.
	featuredURI, err := url.Parse(a.FeaturedCollectionURI)
	if err != nil {
		return nil, err
	}
	featuredProp := streams.NewTootFeaturedProperty()
	featuredProp.SetIRI(featuredURI)
	person.SetTootFeatured(featuredProp)

	// featuredTags
	// NOT IMPLEMENTED

	// preferredUsername
	// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
	preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty()
	preferredUsernameProp.SetXMLSchemaString(a.Username)
	person.SetActivityStreamsPreferredUsername(preferredUsernameProp)

	// name
	// Used as profile display name.
	nameProp := streams.NewActivityStreamsNameProperty()
	if a.Username != "" {
		nameProp.AppendXMLSchemaString(a.DisplayName)
	} else {
		nameProp.AppendXMLSchemaString(a.Username)
	}
	person.SetActivityStreamsName(nameProp)

	// summary
	// Used as profile bio.
	if a.Note != "" {
		summaryProp := streams.NewActivityStreamsSummaryProperty()
		summaryProp.AppendXMLSchemaString(a.Note)
		person.SetActivityStreamsSummary(summaryProp)
	}

	// url
	// Used as profile link.
	profileURL, err := url.Parse(a.URL)
	if err != nil {
		return nil, err
	}
	urlProp := streams.NewActivityStreamsUrlProperty()
	urlProp.AppendIRI(profileURL)
	person.SetActivityStreamsUrl(urlProp)

	// manuallyApprovesFollowers
	// Will be shown as a locked account.
	manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
	manuallyApprovesFollowersProp.Set(a.Locked)
	person.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp)

	// discoverable
	// Will be shown in the profile directory.
	discoverableProp := streams.NewTootDiscoverableProperty()
	discoverableProp.Set(a.Discoverable)
	person.SetTootDiscoverable(discoverableProp)

	// devices
	// NOT IMPLEMENTED, probably won't implement

	// alsoKnownAs
	// Required for Move activity.
	// TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool

	// publicKey
	// Required for signatures.
	publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()

	// create the public key
	publicKey := streams.NewW3IDSecurityV1PublicKey()

	// set ID for the public key
	publicKeyIDProp := streams.NewJSONLDIdProperty()
	publicKeyURI, err := url.Parse(a.PublicKeyURI)
	if err != nil {
		return nil, err
	}
	publicKeyIDProp.SetIRI(publicKeyURI)
	publicKey.SetJSONLDId(publicKeyIDProp)

	// set owner for the public key
	publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty()
	publicKeyOwnerProp.SetIRI(profileIDURI)
	publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp)

	// set the pem key itself
	encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey)
	if err != nil {
		return nil, err
	}
	publicKeyBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: encodedPublicKey,
	})
	publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
	publicKeyPEMProp.Set(string(publicKeyBytes))
	publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp)

	// append the public key to the public key property
	publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey)

	// set the public key property on the Person
	person.SetW3IDSecurityV1PublicKey(publicKeyProp)

	// tag
	// TODO: Any tags used in the summary of this profile

	// attachment
	// Used for profile fields.
	// TODO: The PropertyValue type has to be added: https://schema.org/PropertyValue

	// endpoints
	// NOT IMPLEMENTED -- this is for shared inbox which we don't use

	// icon
	// Used as profile avatar.
	if a.AvatarMediaAttachmentID != "" {
		if a.AvatarMediaAttachment == nil {
			avatar := &gtsmodel.MediaAttachment{}
			if err := c.db.GetByID(ctx, a.AvatarMediaAttachmentID, avatar); err != nil {
				return nil, err
			}
			a.AvatarMediaAttachment = avatar
		}

		iconProperty := streams.NewActivityStreamsIconProperty()

		iconImage := streams.NewActivityStreamsImage()

		mediaType := streams.NewActivityStreamsMediaTypeProperty()
		mediaType.Set(a.AvatarMediaAttachment.File.ContentType)
		iconImage.SetActivityStreamsMediaType(mediaType)

		avatarURLProperty := streams.NewActivityStreamsUrlProperty()
		avatarURL, err := url.Parse(a.AvatarMediaAttachment.URL)
		if err != nil {
			return nil, err
		}
		avatarURLProperty.AppendIRI(avatarURL)
		iconImage.SetActivityStreamsUrl(avatarURLProperty)

		iconProperty.AppendActivityStreamsImage(iconImage)
		person.SetActivityStreamsIcon(iconProperty)
	}

	// image
	// Used as profile header.
	if a.HeaderMediaAttachmentID != "" {
		if a.HeaderMediaAttachment == nil {
			header := &gtsmodel.MediaAttachment{}
			if err := c.db.GetByID(ctx, a.HeaderMediaAttachmentID, header); err != nil {
				return nil, err
			}
			a.HeaderMediaAttachment = header
		}

		headerProperty := streams.NewActivityStreamsImageProperty()

		headerImage := streams.NewActivityStreamsImage()

		mediaType := streams.NewActivityStreamsMediaTypeProperty()
		mediaType.Set(a.HeaderMediaAttachment.File.ContentType)
		headerImage.SetActivityStreamsMediaType(mediaType)

		headerURLProperty := streams.NewActivityStreamsUrlProperty()
		headerURL, err := url.Parse(a.HeaderMediaAttachment.URL)
		if err != nil {
			return nil, err
		}
		headerURLProperty.AppendIRI(headerURL)
		headerImage.SetActivityStreamsUrl(headerURLProperty)

		headerProperty.AppendActivityStreamsImage(headerImage)
		person.SetActivityStreamsImage(headerProperty)
	}

	// put the person in our cache in case we need it again soon
	if err := c.asCache.Store(a.ID, person); err != nil {
		return nil, err
	}

	return person, nil
}

// Converts a gts model account into a VERY MINIMAL Activity Streams person type, following
// the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/
//
// The returned account will just have the Type, Username, PublicKey, and ID properties set.
func (c *converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) {
	person := streams.NewActivityStreamsPerson()

	// id should be the activitypub URI of this user
	// something like https://example.org/users/example_user
	profileIDURI, err := url.Parse(a.URI)
	if err != nil {
		return nil, err
	}
	idProp := streams.NewJSONLDIdProperty()
	idProp.SetIRI(profileIDURI)
	person.SetJSONLDId(idProp)

	// preferredUsername
	// Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
	preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty()
	preferredUsernameProp.SetXMLSchemaString(a.Username)
	person.SetActivityStreamsPreferredUsername(preferredUsernameProp)

	// publicKey
	// Required for signatures.
	publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()

	// create the public key
	publicKey := streams.NewW3IDSecurityV1PublicKey()

	// set ID for the public key
	publicKeyIDProp := streams.NewJSONLDIdProperty()
	publicKeyURI, err := url.Parse(a.PublicKeyURI)
	if err != nil {
		return nil, err
	}
	publicKeyIDProp.SetIRI(publicKeyURI)
	publicKey.SetJSONLDId(publicKeyIDProp)

	// set owner for the public key
	publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty()
	publicKeyOwnerProp.SetIRI(profileIDURI)
	publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp)

	// set the pem key itself
	encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey)
	if err != nil {
		return nil, err
	}
	publicKeyBytes := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: encodedPublicKey,
	})
	publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
	publicKeyPEMProp.Set(string(publicKeyBytes))
	publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp)

	// append the public key to the public key property
	publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey)

	// set the public key property on the Person
	person.SetW3IDSecurityV1PublicKey(publicKeyProp)

	return person, nil
}

func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
	// first check if we have this note in our asCache already
	if noteI, err := c.asCache.Fetch(s.ID); err == nil {
		if note, ok := noteI.(vocab.ActivityStreamsNote); ok {
			// we have it, so just return it as-is
			return note, nil
		}
	}

	// ensure prerequisites here before we get stuck in

	// check if author account is already attached to status and attach it if not
	// if we can't retrieve this, bail here already because we can't attribute the status to anyone
	if s.Account == nil {
		a, err := c.db.GetAccountByID(ctx, s.AccountID)
		if err != nil {
			return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)
		}
		s.Account = a
	}

	// create the Note!
	status := streams.NewActivityStreamsNote()

	// id
	statusURI, err := url.Parse(s.URI)
	if err != nil {
		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err)
	}
	statusIDProp := streams.NewJSONLDIdProperty()
	statusIDProp.SetIRI(statusURI)
	status.SetJSONLDId(statusIDProp)

	// type
	// will be set automatically by go-fed

	// summary aka cw
	statusSummaryProp := streams.NewActivityStreamsSummaryProperty()
	statusSummaryProp.AppendXMLSchemaString(s.ContentWarning)
	status.SetActivityStreamsSummary(statusSummaryProp)

	// inReplyTo
	if s.InReplyToID != "" {
		// fetch the replied status if we don't have it on hand already
		if s.InReplyTo == nil {
			rs := &gtsmodel.Status{}
			if err := c.db.GetByID(ctx, s.InReplyToID, rs); err != nil {
				return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err)
			}
			s.InReplyTo = rs
		}
		rURI, err := url.Parse(s.InReplyTo.URI)
		if err != nil {
			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.InReplyTo.URI, err)
		}

		inReplyToProp := streams.NewActivityStreamsInReplyToProperty()
		inReplyToProp.AppendIRI(rURI)
		status.SetActivityStreamsInReplyTo(inReplyToProp)
	}

	// published
	publishedProp := streams.NewActivityStreamsPublishedProperty()
	publishedProp.Set(s.CreatedAt)
	status.SetActivityStreamsPublished(publishedProp)

	// url
	if s.URL != "" {
		sURL, err := url.Parse(s.URL)
		if err != nil {
			return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err)
		}

		urlProp := streams.NewActivityStreamsUrlProperty()
		urlProp.AppendIRI(sURL)
		status.SetActivityStreamsUrl(urlProp)
	}

	// attributedTo
	authorAccountURI, err := url.Parse(s.Account.URI)
	if err != nil {
		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.URI, err)
	}
	attributedToProp := streams.NewActivityStreamsAttributedToProperty()
	attributedToProp.AppendIRI(authorAccountURI)
	status.SetActivityStreamsAttributedTo(attributedToProp)

	// tags
	tagProp := streams.NewActivityStreamsTagProperty()

	// tag -- mentions
	for _, m := range s.Mentions {
		asMention, err := c.MentionToAS(ctx, m)
		if err != nil {
			return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err)
		}
		tagProp.AppendActivityStreamsMention(asMention)
	}

	// tag -- emojis
	// TODO

	// tag -- hashtags
	// TODO

	status.SetActivityStreamsTag(tagProp)

	// parse out some URIs we need here
	authorFollowersURI, err := url.Parse(s.Account.FollowersURI)
	if err != nil {
		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.FollowersURI, err)
	}

	publicURI, err := url.Parse(asPublicURI)
	if err != nil {
		return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err)
	}

	// to and cc
	toProp := streams.NewActivityStreamsToProperty()
	ccProp := streams.NewActivityStreamsCcProperty()
	switch s.Visibility {
	case gtsmodel.VisibilityDirect:
		// if DIRECT, then only mentioned users should be added to TO, and nothing to CC
		for _, m := range s.Mentions {
			iri, err := url.Parse(m.OriginAccount.URI)
			if err != nil {
				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)
			}
			toProp.AppendIRI(iri)
		}
	case gtsmodel.VisibilityMutualsOnly:
		// TODO
	case gtsmodel.VisibilityFollowersOnly:
		// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC
		toProp.AppendIRI(authorFollowersURI)
		for _, m := range s.Mentions {
			iri, err := url.Parse(m.OriginAccount.URI)
			if err != nil {
				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)
			}
			ccProp.AppendIRI(iri)
		}
	case gtsmodel.VisibilityUnlocked:
		// if UNLOCKED, we want to add followers to TO, and public and mentions to CC
		toProp.AppendIRI(authorFollowersURI)
		ccProp.AppendIRI(publicURI)
		for _, m := range s.Mentions {
			iri, err := url.Parse(m.OriginAccount.URI)
			if err != nil {
				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)
			}
			ccProp.AppendIRI(iri)
		}
	case gtsmodel.VisibilityPublic:
		// if PUBLIC, we want to add public to TO, and followers and mentions to CC
		toProp.AppendIRI(publicURI)
		ccProp.AppendIRI(authorFollowersURI)
		for _, m := range s.Mentions {
			iri, err := url.Parse(m.OriginAccount.URI)
			if err != nil {
				return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err)
			}
			ccProp.AppendIRI(iri)
		}
	}
	status.SetActivityStreamsTo(toProp)
	status.SetActivityStreamsCc(ccProp)

	// conversation
	// TODO

	// content -- the actual post itself
	contentProp := streams.NewActivityStreamsContentProperty()
	contentProp.AppendXMLSchemaString(s.Content)
	status.SetActivityStreamsContent(contentProp)

	// attachment
	attachmentProp := streams.NewActivityStreamsAttachmentProperty()
	for _, a := range s.Attachments {
		doc, err := c.AttachmentToAS(ctx, a)
		if err != nil {
			return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err)
		}
		attachmentProp.AppendActivityStreamsDocument(doc)
	}
	status.SetActivityStreamsAttachment(attachmentProp)

	// replies
	repliesCollection, err := c.StatusToASRepliesCollection(ctx, s, false)
	if err != nil {
		return nil, fmt.Errorf("error creating repliesCollection: %s", err)
	}

	repliesProp := streams.NewActivityStreamsRepliesProperty()
	repliesProp.SetActivityStreamsCollection(repliesCollection)
	status.SetActivityStreamsReplies(repliesProp)

	// put the note in our cache in case we need it again soon
	if err := c.asCache.Store(s.ID, status); err != nil {
		return nil, err
	}

	return status, nil
}

func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
	// parse out the various URIs we need for this
	// origin account (who's doing the follow)
	originAccountURI, err := url.Parse(originAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
	}
	originActor := streams.NewActivityStreamsActorProperty()
	originActor.AppendIRI(originAccountURI)

	// target account (who's being followed)
	targetAccountURI, err := url.Parse(targetAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
	}

	// uri of the follow activity itself
	followURI, err := url.Parse(f.URI)
	if err != nil {
		return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err)
	}

	// start preparing the follow activity
	follow := streams.NewActivityStreamsFollow()

	// set the actor
	follow.SetActivityStreamsActor(originActor)

	// set the id
	followIDProp := streams.NewJSONLDIdProperty()
	followIDProp.SetIRI(followURI)
	follow.SetJSONLDId(followIDProp)

	// set the object
	followObjectProp := streams.NewActivityStreamsObjectProperty()
	followObjectProp.AppendIRI(targetAccountURI)
	follow.SetActivityStreamsObject(followObjectProp)

	// set the To property
	followToProp := streams.NewActivityStreamsToProperty()
	followToProp.AppendIRI(targetAccountURI)
	follow.SetActivityStreamsTo(followToProp)

	return follow, nil
}

func (c *converter) MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) {
	if m.TargetAccount == nil {
		a, err := c.db.GetAccountByID(ctx, m.TargetAccountID)
		if err != nil {
			return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)
		}
		m.TargetAccount = a
	}

	// create the mention
	mention := streams.NewActivityStreamsMention()

	// href -- this should be the URI of the mentioned user
	hrefProp := streams.NewActivityStreamsHrefProperty()
	hrefURI, err := url.Parse(m.TargetAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.TargetAccount.URI, err)
	}
	hrefProp.SetIRI(hrefURI)
	mention.SetActivityStreamsHref(hrefProp)

	// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
	var domain string
	if m.TargetAccount.Domain == "" {
		domain = c.config.AccountDomain
	} else {
		domain = m.TargetAccount.Domain
	}
	username := m.TargetAccount.Username
	nameString := fmt.Sprintf("@%s@%s", username, domain)
	nameProp := streams.NewActivityStreamsNameProperty()
	nameProp.AppendXMLSchemaString(nameString)
	mention.SetActivityStreamsName(nameProp)

	return mention, nil
}

func (c *converter) AttachmentToAS(ctx context.Context, a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) {
	// type -- Document
	doc := streams.NewActivityStreamsDocument()

	// mediaType aka mime content type
	mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty()
	mediaTypeProp.Set(a.File.ContentType)
	doc.SetActivityStreamsMediaType(mediaTypeProp)

	// url -- for the original image not the thumbnail
	urlProp := streams.NewActivityStreamsUrlProperty()
	imageURL, err := url.Parse(a.URL)
	if err != nil {
		return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err)
	}
	urlProp.AppendIRI(imageURL)
	doc.SetActivityStreamsUrl(urlProp)

	// name -- aka image description
	nameProp := streams.NewActivityStreamsNameProperty()
	nameProp.AppendXMLSchemaString(a.Description)
	doc.SetActivityStreamsName(nameProp)

	// blurhash
	blurProp := streams.NewTootBlurhashProperty()
	blurProp.Set(a.Blurhash)
	doc.SetTootBlurhash(blurProp)

	// focalpoint
	// TODO

	return doc, nil
}

/*
	We want to end up with something like this:

	{
	"@context": "https://www.w3.org/ns/activitystreams",
	"actor": "https://ondergrond.org/users/dumpsterqueer",
	"id": "https://ondergrond.org/users/dumpsterqueer#likes/44584",
	"object": "https://testingtesting123.xyz/users/gotosocial_test_account/statuses/771aea80-a33d-4d6d-8dfd-57d4d2bfcbd4",
	"type": "Like"
	}
*/
func (c *converter) FaveToAS(ctx context.Context, f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) {
	// check if targetStatus is already pinned to this fave, and fetch it if not
	if f.Status == nil {
		s, err := c.db.GetStatusByID(ctx, f.StatusID)
		if err != nil {
			return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err)
		}
		f.Status = s
	}

	// check if the targetAccount is already pinned to this fave, and fetch it if not
	if f.TargetAccount == nil {
		a, err := c.db.GetAccountByID(ctx, f.TargetAccountID)
		if err != nil {
			return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err)
		}
		f.TargetAccount = a
	}

	// check if the faving account is already pinned to this fave, and fetch it if not
	if f.Account == nil {
		a, err := c.db.GetAccountByID(ctx, f.AccountID)
		if err != nil {
			return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err)
		}
		f.Account = a
	}

	// create the like
	like := streams.NewActivityStreamsLike()

	// set the actor property to the fave-ing account's URI
	actorProp := streams.NewActivityStreamsActorProperty()
	actorIRI, err := url.Parse(f.Account.URI)
	if err != nil {
		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err)
	}
	actorProp.AppendIRI(actorIRI)
	like.SetActivityStreamsActor(actorProp)

	// set the ID property to the fave's URI
	idProp := streams.NewJSONLDIdProperty()
	idIRI, err := url.Parse(f.URI)
	if err != nil {
		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.URI, err)
	}
	idProp.Set(idIRI)
	like.SetJSONLDId(idProp)

	// set the object property to the target status's URI
	objectProp := streams.NewActivityStreamsObjectProperty()
	statusIRI, err := url.Parse(f.Status.URI)
	if err != nil {
		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err)
	}
	objectProp.AppendIRI(statusIRI)
	like.SetActivityStreamsObject(objectProp)

	// set the TO property to the target account's IRI
	toProp := streams.NewActivityStreamsToProperty()
	toIRI, err := url.Parse(f.TargetAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err)
	}
	toProp.AppendIRI(toIRI)
	like.SetActivityStreamsTo(toProp)

	return like, nil
}

func (c *converter) BoostToAS(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) {
	// the boosted status is probably pinned to the boostWrapperStatus but double check to make sure
	if boostWrapperStatus.BoostOf == nil {
		b, err := c.db.GetStatusByID(ctx, boostWrapperStatus.BoostOfID)
		if err != nil {
			return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)
		}
		boostWrapperStatus.BoostOf = b
	}

	// create the announce
	announce := streams.NewActivityStreamsAnnounce()

	// set the actor
	boosterURI, err := url.Parse(boostingAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.URI, err)
	}
	actorProp := streams.NewActivityStreamsActorProperty()
	actorProp.AppendIRI(boosterURI)
	announce.SetActivityStreamsActor(actorProp)

	// set the ID
	boostIDURI, err := url.Parse(boostWrapperStatus.URI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.URI, err)
	}
	idProp := streams.NewJSONLDIdProperty()
	idProp.SetIRI(boostIDURI)
	announce.SetJSONLDId(idProp)

	// set the object
	boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err)
	}
	objectProp := streams.NewActivityStreamsObjectProperty()
	objectProp.AppendIRI(boostedStatusURI)
	announce.SetActivityStreamsObject(objectProp)

	// set the published time
	publishedProp := streams.NewActivityStreamsPublishedProperty()
	publishedProp.Set(boostWrapperStatus.CreatedAt)
	announce.SetActivityStreamsPublished(publishedProp)

	// set the to
	followersURI, err := url.Parse(boostingAccount.FollowersURI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.FollowersURI, err)
	}
	toProp := streams.NewActivityStreamsToProperty()
	toProp.AppendIRI(followersURI)
	announce.SetActivityStreamsTo(toProp)

	// set the cc
	boostedURI, err := url.Parse(boostedAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostedAccount.URI, err)
	}

	publicURI, err := url.Parse(asPublicURI)
	if err != nil {
		return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", asPublicURI, err)
	}

	ccProp := streams.NewActivityStreamsCcProperty()
	ccProp.AppendIRI(boostedURI)
	ccProp.AppendIRI(publicURI)
	announce.SetActivityStreamsCc(ccProp)

	return announce, nil
}

/*
	we want to end up with something like this:

	{
		"@context": "https://www.w3.org/ns/activitystreams",
		"actor": "https://example.org/users/some_user",
		"id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK",
		"object":"https://some_other.instance/users/some_other_user",
		"type":"Block"
	}
*/
func (c *converter) BlockToAS(ctx context.Context, b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) {
	if b.Account == nil {
		a, err := c.db.GetAccountByID(ctx, b.AccountID)
		if err != nil {
			return nil, fmt.Errorf("BlockToAS: error getting block owner account from database: %s", err)
		}
		b.Account = a
	}

	if b.TargetAccount == nil {
		a, err := c.db.GetAccountByID(ctx, b.TargetAccountID)
		if err != nil {
			return nil, fmt.Errorf("BlockToAS: error getting block target account from database: %s", err)
		}
		b.TargetAccount = a
	}

	// create the block
	block := streams.NewActivityStreamsBlock()

	// set the actor property to the block-ing account's URI
	actorProp := streams.NewActivityStreamsActorProperty()
	actorIRI, err := url.Parse(b.Account.URI)
	if err != nil {
		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.Account.URI, err)
	}
	actorProp.AppendIRI(actorIRI)
	block.SetActivityStreamsActor(actorProp)

	// set the ID property to the blocks's URI
	idProp := streams.NewJSONLDIdProperty()
	idIRI, err := url.Parse(b.URI)
	if err != nil {
		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.URI, err)
	}
	idProp.Set(idIRI)
	block.SetJSONLDId(idProp)

	// set the object property to the target account's URI
	objectProp := streams.NewActivityStreamsObjectProperty()
	targetIRI, err := url.Parse(b.TargetAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
	}
	objectProp.AppendIRI(targetIRI)
	block.SetActivityStreamsObject(objectProp)

	// set the TO property to the target account's IRI
	toProp := streams.NewActivityStreamsToProperty()
	toIRI, err := url.Parse(b.TargetAccount.URI)
	if err != nil {
		return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
	}
	toProp.AppendIRI(toIRI)
	block.SetActivityStreamsTo(toProp)

	return block, nil
}

/*
	the goal is to end up with something like this:

	{
		"@context": "https://www.w3.org/ns/activitystreams",
		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
		"type": "Collection",
		"first": {
		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?page=true",
		"type": "CollectionPage",
		"next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
		"partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
		"items": []
		}
	}
*/
func (c *converter) StatusToASRepliesCollection(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool) (vocab.ActivityStreamsCollection, error) {
	collectionID := fmt.Sprintf("%s/replies", status.URI)
	collectionIDURI, err := url.Parse(collectionID)
	if err != nil {
		return nil, err
	}

	collection := streams.NewActivityStreamsCollection()

	// collection.id
	collectionIDProp := streams.NewJSONLDIdProperty()
	collectionIDProp.SetIRI(collectionIDURI)
	collection.SetJSONLDId(collectionIDProp)

	// first
	first := streams.NewActivityStreamsFirstProperty()
	firstPage := streams.NewActivityStreamsCollectionPage()

	// first.id
	firstPageIDProp := streams.NewJSONLDIdProperty()
	firstPageID, err := url.Parse(fmt.Sprintf("%s?page=true", collectionID))
	if err != nil {
		return nil, gtserror.NewErrorInternalError(err)
	}
	firstPageIDProp.SetIRI(firstPageID)
	firstPage.SetJSONLDId(firstPageIDProp)

	// first.next
	nextProp := streams.NewActivityStreamsNextProperty()
	nextPropID, err := url.Parse(fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts))
	if err != nil {
		return nil, gtserror.NewErrorInternalError(err)
	}
	nextProp.SetIRI(nextPropID)
	firstPage.SetActivityStreamsNext(nextProp)

	// first.partOf
	partOfProp := streams.NewActivityStreamsPartOfProperty()
	partOfProp.SetIRI(collectionIDURI)
	firstPage.SetActivityStreamsPartOf(partOfProp)

	first.SetActivityStreamsCollectionPage(firstPage)

	// collection.first
	collection.SetActivityStreamsFirst(first)

	return collection, nil
}

/*
	the goal is to end up with something like this:
	{
		"@context": "https://www.w3.org/ns/activitystreams",
		"id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true",
		"type": "CollectionPage",
		"next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?min_id=106720870266901180&only_other_accounts=true&page=true",
		"partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies",
		"items": [
			"https://example.com/users/someone/statuses/106720752853216226",
			"https://somewhere.online/users/eeeeeeeeeep/statuses/106720870163727231"
		]
	}
*/
func (c *converter) StatusURIsToASRepliesPage(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool, minID string, replies map[string]*url.URL) (vocab.ActivityStreamsCollectionPage, error) {
	collectionID := fmt.Sprintf("%s/replies", status.URI)

	page := streams.NewActivityStreamsCollectionPage()

	// .id
	pageIDProp := streams.NewJSONLDIdProperty()
	pageIDString := fmt.Sprintf("%s?page=true&only_other_accounts=%t", collectionID, onlyOtherAccounts)
	if minID != "" {
		pageIDString = fmt.Sprintf("%s&min_id=%s", pageIDString, minID)
	}

	pageID, err := url.Parse(pageIDString)
	if err != nil {
		return nil, gtserror.NewErrorInternalError(err)
	}
	pageIDProp.SetIRI(pageID)
	page.SetJSONLDId(pageIDProp)

	// .partOf
	collectionIDURI, err := url.Parse(collectionID)
	if err != nil {
		return nil, err
	}
	partOfProp := streams.NewActivityStreamsPartOfProperty()
	partOfProp.SetIRI(collectionIDURI)
	page.SetActivityStreamsPartOf(partOfProp)

	// .items
	items := streams.NewActivityStreamsItemsProperty()
	var highestID string
	for k, v := range replies {
		items.AppendIRI(v)
		if k > highestID {
			highestID = k
		}
	}
	page.SetActivityStreamsItems(items)

	// .next
	nextProp := streams.NewActivityStreamsNextProperty()
	nextPropIDString := fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts)
	if highestID != "" {
		nextPropIDString = fmt.Sprintf("%s&min_id=%s", nextPropIDString, highestID)
	}

	nextPropID, err := url.Parse(nextPropIDString)
	if err != nil {
		return nil, gtserror.NewErrorInternalError(err)
	}
	nextProp.SetIRI(nextPropID)
	page.SetActivityStreamsNext(nextProp)

	return page, nil
}