// TODO: Unify function names

// Storage is the handler for cache and db access
// It handles storing various data in the database as well as caching that data
// Said data includes notes, accounts, metadata about media files, servers and similar
package storage

import (
	"crypto/ed25519"
	"fmt"

	"github.com/rs/zerolog/log"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"

	"git.mstar.dev/mstar/linstrom/config"
	"git.mstar.dev/mstar/linstrom/shared"
	"git.mstar.dev/mstar/linstrom/storage/cache"
)

// Always keep a reference of the server's own RemoteServer entry here
// Removes the need to perform a db request every time a new local anything
// is created
var serverSelf RemoteServer

// Storage is responsible for all database, cache and media related actions
// and serves as the lowest layer of the cake
type Storage struct {
	db    *gorm.DB
	cache *cache.Cache
}

func NewStorage(dbUrl string, cache *cache.Cache) (*Storage, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	db, err := gorm.Open(postgres.Open(dbUrl), &gorm.Config{
		Logger: newGormLogger(log.Logger),
	})
	if err != nil {
		return nil, err
	}
	err = db.AutoMigrate(
		MediaMetadata{},
		Account{},
		RemoteServer{},
		Note{},
		Role{},
		PasskeySession{},
		InboundJob{},
		OutboundJob{},
		AccessToken{},
		Emote{},
		UserInfoField{},
		AccountRelation{},
	)
	if err != nil {
		return nil, fmt.Errorf("failed to apply migrations: %w", err)
	}

	s := &Storage{db, cache}

	if err = s.insertDefaultRoles(); err != nil {
		return nil, fmt.Errorf("default roles insertion failed: %w", err)
	}

	if err = s.insertPlaceholderFile(); err != nil {
		return nil, fmt.Errorf("placeholder file insertion failed: %w", err)
	}

	if err = s.insertSelfFromConfig(); err != nil {
		return nil, fmt.Errorf("self insertion failed: %w", err)
	}

	return s, nil
}

func (s *Storage) insertSelfFromConfig() error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	const ServerActorId = "self"

	var err error

	// Insert server info
	serverData := RemoteServer{}
	err = s.db.Where("id = 1").
		// Set once on creation
		Attrs(RemoteServer{
			Domain: config.GlobalConfig.General.GetFullDomain(),
		}).
		// Set every time
		Assign(RemoteServer{
			IsSelf:     true,
			Name:       config.GlobalConfig.Self.ServerDisplayName,
			ServerType: REMOTE_SERVER_LINSTROM,
			Icon:       "placeholder", // TODO: Set to server icon media
		}).FirstOrCreate(&serverData).Error
	if err != nil {
		return err
	}
	// Set module specific global var
	serverSelf = serverData

	// Insert server actor
	serverActor := Account{}
	serverActorPublicKey, serverActorPrivateKey, err := ed25519.GenerateKey(nil)
	if err != nil {
		return err
	}
	err = s.db.Where(Account{ID: ServerActorId}).
		// Values to always (re)set after launch
		Assign(Account{
			Username:    "self",
			DisplayName: config.GlobalConfig.Self.ServerActorDisplayName,
			// Server:      serverData,
			ServerId: serverData.ID,
			// CustomFields: []uint{},
			Description: "Server actor of a Linstrom server",
			// Tags:         []string{},
			IsBot: true,
			// Followers: []string{},
			// Follows: []string{},
			Indexable:        false,
			RestrictedFollow: false,
			IdentifiesAs:     []Being{},
			Gender:           []string{},
			Roles:            []string{"ServerActor"}, // TODO: Add server actor role once created
		}).
		// Values that'll only be set on first creation
		Attrs(Account{
			PublicKey:  serverActorPublicKey,
			PrivateKey: serverActorPrivateKey,
			Icon:       "placeholder",
			Background: nil,
			Banner:     nil,
		}).
		FirstOrCreate(&serverActor).Error
	if err != nil {
		return err
	}
	return nil
}

func (s *Storage) insertDefaultRoles() error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	for _, role := range allDefaultRoles {
		log.Debug().Str("role-name", role.Name).Msg("Inserting default role")
		if err := s.db.FirstOrCreate(role).Error; err != nil {
			return err
		}
	}
	return nil
}

func (s *Storage) insertPlaceholderFile() error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return s.db.Model(&MediaMetadata{}).Assign(&MediaMetadata{
		ID:       "placeholder",
		Type:     "image/webp",
		Name:     "placeholderFile",
		Blurred:  false,
		Remote:   false,
		Location: "/placeholder-file",
		AltText:  "Greyscale image of a pidgeon, captioned with the text \"Duck\"",
	}).FirstOrCreate(&MediaMetadata{}).Error
}
