// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// 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 federation

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"slices"
	"strings"

	"code.superseriousbusiness.org/activity/pub"
	"code.superseriousbusiness.org/activity/streams/vocab"
	"code.superseriousbusiness.org/gopkg/log"
	"code.superseriousbusiness.org/gotosocial/internal/ap"
	apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
	"code.superseriousbusiness.org/gotosocial/internal/config"
	"code.superseriousbusiness.org/gotosocial/internal/db"
	"code.superseriousbusiness.org/gotosocial/internal/federation/federatingdb"
	"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
	"code.superseriousbusiness.org/gotosocial/internal/gtserror"
	"code.superseriousbusiness.org/gotosocial/internal/uris"
	errorsv2 "codeberg.org/gruf/go-errors/v2"
	"codeberg.org/gruf/go-kv/v2"
)

// federatingActor wraps the pub.FederatingActor
// with some custom GoToSocial-specific logic.
type federatingActor struct {
	sideEffectActor pub.DelegateActor
	wrapped         pub.FederatingActor
}

func deliveryRecipientPreSort(actorAndCollectionIRIs []*url.URL) []*url.URL {
	var (
		thisHost       = config.GetHost()
		thisAcctDomain = config.GetAccountDomain()
	)

	slices.SortFunc(
		actorAndCollectionIRIs,
		func(a *url.URL, b *url.URL) int {
			// We want to sort by putting more specific actor URIs *before* collection URIs.
			// Since the only collection URIs we ever address are our own followers URIs, we
			// can just use host and regexes to identify these collections, and shove them
			// to the back of the slice. This ensures that directly addressed (ie., mentioned)
			// accounts get delivery-attempted *first*, and then delivery attempts move on to
			// followers of the author. This should have the effect of making conversation
			/// threads feel more snappy, as replies will be sent quicker to participants.
			var (
				aIsFollowers = (a.Host == thisHost || a.Host == thisAcctDomain) && uris.IsFollowersPath(a)
				bIsFollowers = (b.Host == thisHost || b.Host == thisAcctDomain) && uris.IsFollowersPath(b)
			)

			switch {
			case aIsFollowers == bIsFollowers:
				// Both followers URIs or
				// both not followers URIs,
				// order doesn't matter.
				return 0

			case aIsFollowers:
				// a is followers
				// URI, b is not.
				//
				// Sort b before a.
				return 1

			default:
				// b is followers
				// URI, a is not.
				//
				// Sort a before b.
				return -1
			}
		},
	)

	return actorAndCollectionIRIs
}

// newFederatingActor returns a federatingActor.
func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
	sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock)

	// Hook in our own custom Serialize function.
	sideEffectActor.Serialize = ap.Serialize

	// Hook in our own custom recipient pre-sort function.
	sideEffectActor.DeliveryRecipientPreSort = deliveryRecipientPreSort

	return &federatingActor{
		sideEffectActor: sideEffectActor,
		wrapped:         pub.NewCustomActor(sideEffectActor, false, true, clock),
	}
}

// PostInboxScheme is a reimplementation of the default baseActor
// implementation of PostInboxScheme in pub/base_actor.go.
//
// Key differences from that implementation:
//   - More explicit debug logging when a request is not processed.
//   - Normalize content of activity object.
//   - *ALWAYS* return gtserror.WithCode if there's an issue, to
//     provide more helpful messages to remote callers.
//   - Return code 202 instead of 200 on successful POST, to reflect
//     that we process most side effects asynchronously.
func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
	l := log.WithContext(ctx).
		WithFields([]kv.Field{
			{"userAgent", r.UserAgent()},
			{"path", r.URL.Path},
		}...)

	// Ensure valid ActivityPub Content-Type.
	// https://www.w3.org/TR/activitypub/#server-to-server-interactions
	if ct := r.Header.Get("Content-Type"); !apiutil.ASContentType(ct) {
		const ct1 = "application/activity+json"
		const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams"
		err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2)
		return false, gtserror.NewErrorNotAcceptable(err)
	}

	// Authenticate request by checking http signature.
	//
	// NOTE: the behaviour here is a little strange as we have
	// the competing code styles of the go-fed interface expecting
	// that any 'err' is fatal, but 'authenticated' bool is intended to
	// be the main passer of whether failed auth occurred, but we in
	// the gts codebase use errors to pass-back non-200 status codes,
	// so we specifically have to check for already wrapped with code.
	//
	ctx, didAuthentication, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
	switch {

	// If error was already wrapped with an
	// HTTP code then don't bother rewrapping
	// it, just return it as-is for caller to
	// handle. AuthenticatePostInbox already
	// calls WriteHeader() in some situations.
	//
	// In most cases, the returned error will be
	// 401 Unauthorized, to indicate that the
	// signature on the request was invalid.
	case errorsv2.AsV2[gtserror.WithCode](err) != nil:
		return false, err

	// If error was returned but wasn't wrapped,
	// wrap it into a 500 Internal Server Error.
	case err != nil:
		err := gtserror.Newf("error authenticating post inbox: %w", err)
		return false, gtserror.NewErrorInternalError(err)

	// If we got no error but we didn't do auth,
	// this means we were either still handshaking
	// during auth (very rare) or more likely the
	// remote account was marked as gone/deleted
	// so we couldn't even get their key to verify.
	//
	// If this is so, we know we've already handled any
	// necessary deletes, and AuthenticatePostInbox
	// will have written 202 Accepted, so we can bail.
	case !didAuthentication:
		log.Debug(ctx, "requesting account is gone or we're still handshaking with it, ignoring inbox post")
		return true, nil

	// No problem.
	default:
		log.Trace(ctx, "AuthenticatePostInbox was successful, continuing with request")
	}

	// Ensure requester is not suspended.
	requester := gtscontext.RequestingAccount(ctx)
	switch {
	case !requester.IsSuspended():
		// Account in good standing.
		// Allow request to continue.

	case requester.DeletedSelf():
		// Looks like pub key owner deleted their own account.
		// Likely their instance is still sending out deletes,
		// but we'll have already deleted everything of theirs.
		// Don't do any further processing of the request.
		log.Debugf(ctx,
			"requesting account %s self deleted, ignoring inbox post",
			requester.UsernameDomain(),
		)
		return true, nil

	default:
		// Likely suspended by an admin or
		// defed action on *this* instance.
		const text = "requesting account suspended"
		return false, gtserror.NewErrorForbidden(errors.New(text), text)
	}

	/*
		Begin processing the request, but note that we
		have not yet applied authorization (ie., blocks).
	*/

	// Resolve the activity, rejecting badly formatted / transient.
	activity, ok, errWithCode := ap.ResolveIncomingActivity(r)
	if errWithCode != nil {
		return false, errWithCode
	} else if !ok { // transient
		return false, nil
	}

	// Set additional context data. Primarily this means
	// looking at the Activity and seeing which IRIs are
	// involved in it tangentially.
	ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
	if err != nil {
		err := gtserror.Newf("error during post inbox request body hook: %w", err)
		return false, gtserror.NewErrorInternalError(err)
	}

	// Check authorization of the activity; this will include blocks.
	authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity)
	if err != nil {
		if errorsv2.AsV2[*errOtherIRIBlocked](err) != nil {
			// There's no direct block between requester(s) and
			// receiver. However, one or more of the other IRIs
			// involved in the request (account replied to, note
			// boosted, etc) is blocked either at domain level or
			// by the receiver. We don't need to return 403 here,
			// instead, just return 202 accepted but don't do any
			// further processing of the activity.
			return true, nil //nolint
		}

		// Real error has occurred.
		err := gtserror.Newf("error authorizing post inbox: %w", err)
		return false, gtserror.NewErrorInternalError(err)
	}

	if !authorized {
		// Block exists either from this instance against
		// one or more directly involved actors, or between
		// receiving account and one of those actors.
		const text = "blocked"
		return false, gtserror.NewErrorForbidden(errors.New(text), text)
	}

	// Copy existing URL + add
	// request host and scheme.
	inboxID := func() *url.URL {
		u := new(url.URL)
		*u = *r.URL
		u.Host = r.Host
		u.Scheme = scheme
		return u
	}()

	// At this point we have everything we need, and have verified that
	// the POST request is authentic (properly signed) and authorized
	// (permitted to interact with the target inbox).
	//
	// Post the activity to the Actor's inbox and trigger side effects.
	if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil {

		// Check if it's a bad request because the
		// object or target props weren't populated,
		// or we failed parsing activity details.
		//
		// Log such activities to help debug, then
		// return the rejection (400) to the peer.
		if gtserror.IsMalformed(err) ||
			errorsv2.IsV2(err,
				pub.ErrObjectRequired,
				pub.ErrTargetRequired) {

			// Include the activity in logging context for useful info.
			l = l.WithField("activity", federatingdb.Serialize{activity})
			l.Warnf("malformed incoming activity: %v", err)

			const text = "malformed incoming activity"
			return false, gtserror.NewErrorBadRequest(errors.New(text), text)
		}

		// Check if a function in the federatingDB
		// has returned an explicit errWithCode for us.
		if errWithCode, ok := err.(gtserror.WithCode); ok {
			return false, errWithCode
		}

		// Extract error string for some
		// more specific AP error handling.
		switch errStr := err.Error(); {

		// Check for errors "cannot determine id of activitystreams property" or
		// "cannot determine id of activitystreams value" from activity/pub/util.go
		// This likely means we've been delivered a type we just don't recognise.
		// If this is so, just log it and return `false, nil` so caller gets 202.
		case strings.Contains(errStr, "cannot determine id of activitystreams"):

			// Include the activity in logging context for useful info.
			l = l.WithField("activity", federatingdb.Serialize{activity})
			l.Warn(ctx, "ignored unhandleable Activity posted to inbox")
			return false, nil

		// Check for error "object [blah] not in activity origin" from
		// activity/pub/util.go. This means someone is trying to send
		// an Activity that updates or deletes an object that doesn't
		// belong to them, eg., `@someone@example.org` is trying to
		// Delete or Update an object on a different server.
		//
		// Tell the remote server they're not allowed to do that.
		case strings.Contains(errStr, "not in activity origin"):

			// Include the activity in attached log fields on error for useful info in later logging.
			const text = "actor not permitted to delete or update object that doesn't belong to them"
			err := gtserror.WithLogField(errors.New(text), "activity", federatingdb.Serialize{activity})
			return false, gtserror.NewErrorForbidden(err, text)
		}

		// Something else went wrong, what the heck! This is an actual 500-able error.
		// Include the activity in attached log fields on error for useful info in later logging.
		err := gtserror.Newf("error calling sideEffectActor.PostInbox: %w", err)
		err = gtserror.WithLogField(err, "activity", federatingdb.Serialize{activity})
		return false, gtserror.NewErrorInternalError(err)
	}

	// Side effects are complete. Now delegate determining whether
	// to do inbox forwarding, as well as the action to do it.
	if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {

		// As a not-ideal side-effect, InboxForwarding will try
		// to create entries if the federatingDB returns `false`
		// when calling `Exists()` to determine whether the Activity
		// is in the database.
		//
		// Since our `Exists()` function currently *always*
		// returns false, it will *always* attempt to parse
		// out and insert the Activity, trying to fetch other
		// items from the DB in the process, which may or may
		// not exist yet. Therefore, we should expect some
		// errors coming from this function, and only warn log
		// on certain ones.
		//
		// This check may be removed when the `Exists()` func
		// is updated, and/or federating callbacks are handled
		// properly.
		if !errorsv2.IsV2(err,
			db.ErrAlreadyExists,
			db.ErrNoEntries,
		) {
			// Failed inbox forwarding is not a show-stopper,
			// and doesn't even necessarily denote a real error.
			l.Warnf("error calling sideEffectActor.InboxForwarding: %v", err)
		}
	}

	// Request is now undergoing processing. Caller
	// of this function will handle writing Accepted.
	return true, nil
}

/*
	Functions below are just lightly wrapped versions
	of the original go-fed federatingActor functions.
*/

func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
	return f.PostInboxScheme(c, w, r, "https")
}

func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
	log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox)
	return f.wrapped.Send(c, outbox, t)
}

func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
	return f.wrapped.GetInbox(c, w, r)
}

func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
	return f.wrapped.PostOutbox(c, w, r)
}

func (f *federatingActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) {
	return f.wrapped.PostOutboxScheme(c, w, r, scheme)
}

func (f *federatingActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
	return f.wrapped.GetOutbox(c, w, r)
}
