// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package federation

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

	"forgejo.org/models/forgefed"
	"forgejo.org/models/user"
	"forgejo.org/modules/activitypub"
	fm "forgejo.org/modules/forgefed"
	"forgejo.org/modules/log"

	ap "github.com/go-ap/activitypub"
)

// Factory function for ActorID. Created struct is asserted to be valid
func NewActorIDFromKeyID(ctx context.Context, uri string) (fm.ActorID, error) {
	parsedURI, err := url.Parse(uri)
	if err != nil {
		return fm.ActorID{}, err
	}

	parsedURI.Fragment = ""

	actionsUser := user.NewAPServerActor()
	clientFactory, err := activitypub.GetClientFactory(ctx)
	if err != nil {
		return fm.ActorID{}, err
	}

	apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
	if err != nil {
		return fm.ActorID{}, err
	}

	userResponse, err := apClient.GetBody(parsedURI.String())
	if err != nil {
		return fm.ActorID{}, err
	}

	var actor ap.Actor
	err = actor.UnmarshalJSON(userResponse)
	if err != nil {
		return fm.ActorID{}, err
	}

	result, err := fm.NewActorID(actor.PublicKey.Owner.String())
	return result, err
}

func FindOrCreateFederatedUserKey(ctx context.Context, keyID string) (pubKey any, err error) {
	log.Trace("KeyID: %v", keyID)
	var federatedUser *user.FederatedUser
	var keyURL *url.URL

	keyURL, err = url.Parse(keyID)
	if err != nil {
		return nil, err
	}

	// Try if the signing actor is an already known federated user
	_, federatedUser, err = user.FindFederatedUserByKeyID(ctx, keyURL.String())
	if err != nil {
		return nil, err
	}

	if federatedUser == nil {
		rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
		if err != nil {
			return nil, err
		}

		_, federatedUser, _, err = FindOrCreateFederatedUser(ctx, rawActorID.AsURI())
		if err != nil {
			return nil, err
		}
	} else {
		_, err = forgefed.GetFederationHost(ctx, federatedUser.FederationHostID)
		if err != nil {
			return nil, err
		}
	}

	if federatedUser.PublicKey.Valid {
		pubKey, err := x509.ParsePKIXPublicKey(federatedUser.PublicKey.V)
		if err != nil {
			return nil, err
		}
		log.Trace("For KeyID %v found pubKey %v", keyID, pubKey)
		return pubKey, nil
	}

	// Fetch missing public key
	pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
	if err != nil {
		return nil, err
	}
	if apPerson.Type == ap.ActivityVocabularyType("Person") {
		// Check federatedUser.id = person.id
		if federatedUser.ExternalID != apPerson.ID.String() {
			return nil, fmt.Errorf("federated user fetched (%v) does not match the stored one %v", apPerson, federatedUser)
		}
		// update federated user
		federatedUser.KeyID = sql.NullString{
			String: apPerson.PublicKey.ID.String(),
			Valid:  true,
		}
		federatedUser.PublicKey = sql.Null[sql.RawBytes]{
			V:     pubKeyBytes,
			Valid: true,
		}
		err = user.UpdateFederatedUser(ctx, federatedUser)
		if err != nil {
			return nil, err
		}
		log.Trace("For %v found pubKey %v", keyID, pubKey)
		return pubKey, nil
	}
	log.Trace("For %v found no pubKey", keyID)
	return nil, nil
}

func FindOrCreateFederationHostKey(ctx context.Context, keyID string) (pubKey any, err error) {
	log.Trace("KeyID: %v", keyID)
	keyURL, err := url.Parse(keyID)
	if err != nil {
		return nil, err
	}
	rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
	if err != nil {
		return nil, err
	}

	// Is there an already known federation host?
	federationHost, err := forgefed.FindFederationHostByKeyID(ctx, keyURL.String())
	if err != nil {
		return nil, err
	}

	if federationHost == nil {
		federationHost, err = FindOrCreateFederationHost(ctx, rawActorID.AsURI())
		if err != nil {
			return nil, err
		}
	}

	// Is there an already an key?
	if federationHost.PublicKey.Valid {
		pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
		if err != nil {
			return nil, err
		}
		log.Trace("For %v found pubKey: %v", keyID, pubKey)
		return pubKey, nil
	}

	// If not, fetch missing public key
	pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
	if err != nil {
		return nil, err
	}
	if apPerson.Type == ap.ActivityVocabularyType("Application") {
		// Check federationhost.id = person.id
		if federationHost.HostPort != rawActorID.HostPort || federationHost.HostFqdn != rawActorID.Host ||
			federationHost.HostSchema != rawActorID.HostSchema {
			return nil, fmt.Errorf("federation host fetched (%v) does not match the stored one %v", apPerson, federationHost)
		}
		// update federation host
		federationHost.KeyID = sql.NullString{
			String: apPerson.PublicKey.ID.String(),
			Valid:  true,
		}
		federationHost.PublicKey = sql.Null[sql.RawBytes]{
			V:     pubKeyBytes,
			Valid: true,
		}
		err = forgefed.UpdateFederationHost(ctx, federationHost)
		if err != nil {
			return nil, err
		}
		log.Trace("For %v found pubKey: %v", keyID, pubKey)
		return pubKey, nil
	}
	log.Trace("For %v found no pubKey.", keyID)
	return nil, nil
}

func fetchKeyFromAp(ctx context.Context, keyURL url.URL) (pubKey any, pubKeyBytes []byte, apPerson *ap.Person, err error) {
	log.Trace("keyURL %v", keyURL)
	actionsUser := user.NewAPServerActor()

	clientFactory, err := activitypub.GetClientFactory(ctx)
	if err != nil {
		return nil, nil, nil, err
	}

	apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
	if err != nil {
		return nil, nil, nil, err
	}

	b, err := apClient.GetBody(keyURL.String())
	if err != nil {
		return nil, nil, nil, err
	}

	person := ap.PersonNew(ap.IRI(keyURL.String()))
	err = person.UnmarshalJSON(b)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
	}

	pubKeyFromAp := person.PublicKey
	if pubKeyFromAp.ID.String() != keyURL.String() {
		return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %v in %v", keyURL, string(b))
	}

	pubKeyBytes, err = decodePublicKeyPem(pubKeyFromAp.PublicKeyPem)
	if err != nil {
		return nil, nil, nil, err
	}

	pubKey, err = x509.ParsePKIXPublicKey(pubKeyBytes)
	if err != nil {
		return nil, nil, nil, err
	}

	log.Trace("For %v fetched pubKey %v", keyURL, pubKey)
	return pubKey, pubKeyBytes, person, err
}

func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
	block, _ := pem.Decode([]byte(pubKeyPem))
	if block == nil || block.Type != "PUBLIC KEY" {
		return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
	}

	return block.Bytes, nil
}
