package storage

import (
	"crypto/ed25519"
	"crypto/rand"
	"errors"
	"fmt"
	"strings"
	"time"

	ap "git.mstar.dev/mstar/linstrom/activitypub/shared"
	"git.mstar.dev/mstar/linstrom/shared"
	"github.com/go-webauthn/webauthn/webauthn"
	"github.com/google/uuid"
	"github.com/mstarongithub/passkey"
	"github.com/rs/zerolog/log"
	"gorm.io/gorm"
)

// Database representation of a user account
// This can be a bot, remote or not
// If remote, this is used for caching the account
type Account struct {
	ID string `gorm:"primarykey"` // ID is a uuid for this account
	// Username of the user (eg "max" if the full username is @max@example.com)
	// Assume unchangable (once set by a user) to be kind to other implementations
	// Would be an easy avenue to fuck with them though
	Username  string    `gorm:"unique"`
	CreatedAt time.Time // When this entry was created. Automatically set by gorm
	// When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm
	UpdatedAt time.Time
	// When this entry was deleted (for soft deletions)
	// Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to
	// If not null, this entry is marked as deleted
	DeletedAt gorm.DeletedAt `gorm:"index"`
	// Server       RemoteServer   // `gorm:"foreignKey:ServerId;references:ID"` // The server this user is from
	ServerId     uint     // Id of the server this user is from, needed for including RemoteServer
	DisplayName  string   // The display name of the user. Can be different from the handle
	CustomFields []uint   `gorm:"serializer:json"` // IDs to the custom fields a user has
	Description  string   // The description of a user account
	Tags         []string `gorm:"serializer:json"` // Hashtags
	IsBot        bool     // Whether to mark this account as a script controlled one
	Relations    []uint   `gorm:"serializer:json"` // List of ids of all relations this account has. Both follows and followers
	Icon         string   // ID of a media file used as icon
	Background   *string  // ID of a media file used as background image
	Banner       *string  // ID of a media file used as banner
	Indexable    bool     // Whether this account can be found by crawlers
	PublicKey    []byte   // The public key of the account
	// Whether this account restricts following
	// If true, the owner must approve of a follow request first
	RestrictedFollow bool
	// List of things the owner identifies as
	// Example [cat human robot] means that the owner probably identifies as
	// a cyborg-catgirl/boy/human or a cathuman shaped robot, refer to Gender for pronouns
	IdentifiesAs []Being `gorm:"serializer:json"`
	// List of pronouns the owner identifies with
	// An unordered list since the owner can freely set it
	// Examples: [she her], [it they its them] or, if you want to go fancy, [this is super serious]
	Gender []string `gorm:"serializer:json"`
	// The roles assocciated with an account. Values are the names of the roles
	Roles []string `gorm:"serializer:json"`

	Location *string
	Birthday *time.Time

	// --- And internal account stuff ---
	// Still public fields since they wouldn't be able to be stored in the db otherwise
	PrivateKey []byte // The private key of the account. Nil if remote user
	WebAuthnId []byte // The unique and random ID of this account used for passkey authentication
	// Whether the account got verified and is allowed to be active
	// For local accounts being active means being allowed to login and perform interactions
	// For remote users, if an account is not verified, any interactions it sends are discarded
	Verified bool
	// TODO: Turn this into a map to give passkeys names.
	// Needed for supporting a decent passkey management interface.
	// Or check if webauthn.Credential has sufficiently easy to identify data
	// to use instead of a string mapping
	PasskeyCredentials []webauthn.Credential `gorm:"serializer:json"` // Webauthn credentials data
	// Has a RemoteAccountLinks included if remote user
	RemoteLinks *RemoteAccountLinks
}

// Contains static and cached info about a remote account, mostly links
type RemoteAccountLinks struct {
	// ---- Section: gorm
	// Sets this struct up as a value that an Account may have
	gorm.Model
	AccountID string

	// Just about every link here is optional to accomodate for servers with only minimal accounts
	// Minimal being handle, ap link and inbox
	ApLink           string
	ViewLink         *string
	FollowersLink    *string
	FollowingLink    *string
	InboxLink        string
	OutboxLink       *string
	FeaturedLink     *string
	FeaturedTagsLink *string
}

// Find an account in the db using a given full handle (@max@example.com)
// Returns an account and nil if an account is found, otherwise nil and the error
func (s *Storage) FindAccountByFullHandle(handle string) (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Str("account-handle", handle).Msg("Looking for account by handle")
	log.Debug().Str("account-handle", handle).Msg("Checking if there's a cache hit")

	// Try and find the account in cache first
	cacheAccId, err := s.cacheHandleToAccUid(handle)
	if err == nil {
		log.Info().Str("account-handle", handle).Msg("Hit account handle in cache")
		// Then always load via id since unique key access should be faster than string matching
		return s.FindAccountById(*cacheAccId)
	} else {
		if !errors.Is(err, errCacheNotFound) {
			log.Error().Err(err).Str("account-handle", handle).Msg("Problem while checking cache for account")
			return nil, err
		}
	}

	// Failed to find in cache, go the slow route of hitting the db
	log.Debug().Str("account-handle", handle).Msg("Didn't hit account in cache, going to db")
	name, server, err := ap.SplitFullHandle(handle)
	if err != nil {
		log.Warn().Err(err).Str("account-handle", handle).Msg("Failed to split up account handle")
		return nil, err
	}

	acc := Account{}
	res := s.db.Where("name = ?", name).Where("server = ?", server).First(&acc)
	if res.Error != nil {
		if errors.Is(res.Error, gorm.ErrRecordNotFound) {
			log.Info().Str("account-handle", handle).Msg("Account with handle not found")
		} else {
			log.Error().Err(err).Str("account-handle", handle).Msg("Failed to get account with handle")
		}
		return nil, res.Error
	}
	log.Info().Str("account-handle", handle).Msg("Found account, also inserting into cache")
	if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
		log.Warn().
			Err(err).
			Str("account-handle", handle).
			Msg("Found account but failed to insert into cache")
	}
	if err = s.cache.Set(cacheUserHandleToIdPrefix+strings.TrimLeft(handle, "@"), acc.ID); err != nil {
		log.Warn().
			Err(err).
			Str("account-handle", handle).
			Msg("Failed to store handle to id in cache")
	}
	return &acc, nil
}

// Find an account given a specific ID
func (s *Storage) FindAccountById(id string) (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Str("account-id", id).Msg("Looking for account by id")
	log.Debug().Str("account-id", id).Msg("First trying to hit cache")
	acc, err := s.cacheAccIdToData(id)
	if err == nil {
		log.Info().Str("account-id", id).Msg("Found account in cache")
		return acc, nil
	} else if !errors.Is(err, errCacheNotFound) {
		log.Error().Err(err).Str("account-id", id).Msg("Error while looking for account in cache")
		return nil, err
	}

	log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db")
	acc = &Account{}
	res := s.db.Where(Account{ID: id}).First(acc)
	if res.Error != nil {
		if errors.Is(res.Error, gorm.ErrRecordNotFound) {
			log.Warn().Str("account-id", id).Msg("Account not found")
			return nil, ErrEntryNotFound
		} else {
			log.Error().Err(res.Error).Str("account-id", id).Msg("Failed to look for account")
			return nil, res.Error
		}
	}
	log.Info().Str("account-id", id).Msg("Found account in db, also adding to cache")
	if err = s.cache.Set(cacheUserIdToAccPrefix+id, acc); err != nil {
		log.Warn().Err(err).Str("account-id", id).Msg("Failed to add account to cache")
	}
	return acc, nil
}

func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Str("account-username", username).Msg("Looking for local account")
	log.Debug().Str("account-username", username).Msg("Checking cache first")

	// Try and find the account in cache first
	cacheAccId, err := s.cacheLocalUsernameToAccUid(username)
	if err == nil {
		log.Info().Str("account-username", username).Msg("Hit account handle in cache")
		// Then always load via id since unique key access should be faster than string matching
		return s.FindAccountById(*cacheAccId)
	} else {
		if err != errCacheNotFound {
			log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account")
			return nil, err
		}
	}

	// Failed to find in cache, go the slow route of hitting the db
	log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db")

	acc := Account{}
	res := s.db.Where("username = ?", username).
		Where("server_id = ?", serverSelf.ID).
		First(&acc)
	if res.Error != nil {
		if errors.Is(res.Error, gorm.ErrRecordNotFound) {
			log.Info().
				Str("account-username", username).
				Msg("Local account with username not found")
		} else {
			log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username")
		}
		return nil, ErrEntryNotFound
	}
	log.Info().Str("account-username", username).Msg("Found account, also inserting into cache")
	if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
		log.Warn().
			Err(err).
			Str("account-username", username).
			Msg("Found account but failed to insert into cache")
	}
	if err = s.cache.Set(cacheLocalUsernameToIdPrefix+username, acc.ID); err != nil {
		log.Warn().
			Err(err).
			Str("account-username", username).
			Msg("Failed to store local username to id in cache")
	}
	return &acc, nil
}

func (s *Storage) FindAccountByPasskeyId(pkeyId []byte) (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Looking for account")
	log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Checking cache first")

	// Try and find the account in cache first
	cacheAccId, err := s.cachePkeyIdToAccId(pkeyId)
	if err == nil {
		log.Info().Bytes("account-passkey-id", pkeyId).Msg("Hit passkey id in cache")
		// Then always load via id since unique key access should be faster than string matching
		return s.FindAccountById(*cacheAccId)
	} else {
		if err != errCacheNotFound {
			log.Error().Err(err).Bytes("account-passkey-id", pkeyId).Msg("Problem while checking cache for account")
			return nil, err
		}
	}

	// Failed to find in cache, go the slow route of hitting the db
	log.Debug().Bytes("account-passkey-id", pkeyId).Msg("Didn't hit account in cache, going to db")

	acc := Account{}
	res := s.db.Where("web_authn_id = ?", pkeyId).
		First(&acc)
	if res.Error != nil {
		if res.Error == gorm.ErrRecordNotFound {
			log.Info().
				Bytes("account-passkey-id", pkeyId).
				Msg("Local account with passkey id not found")
			return nil, ErrEntryNotFound
		} else {
			log.Error().Err(res.Error).Bytes("account-passkey-id", pkeyId).Msg("Failed to get local account with passkey id")
			return nil, res.Error
		}
	}
	log.Info().Bytes("account-passkey-id", pkeyId).Msg("Found account, also inserting into cache")
	// if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil {
	// 	log.Warn().
	// 		Err(err).
	// 		Bytes("account-passkey-id", pkeyId).
	// 		Msg("Found account but failed to insert into cache")
	// }
	// if err = s.cache.Set(cachePasskeyIdToAccIdPrefix+string(pkeyId), acc.ID); err != nil {
	// 	log.Warn().
	// 		Err(err).
	// 		Bytes("account-passkey-id", pkeyId).
	// 		Msg("Failed to store local username to id in cache")
	// }
	return &acc, nil
}

// Update a given account in storage and cache
func (s *Storage) UpdateAccount(acc *Account) error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	// If the account is nil or doesn't have an id, error out
	if acc == nil || acc.ID == "" {
		return ErrInvalidData
	}
	res := s.db.Save(acc)
	if res.Error != nil {
		return res.Error
	}
	if err := s.cache.Set(cacheUserIdToAccPrefix+acc.ID, acc); err != nil {
		return err
	}
	return nil
}

// Create a new empty account for future use
func (s *Storage) NewEmptyAccount() (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Msg("Creating new empty account")
	acc := Account{}
	// Generate the 64 bit id for passkey and webauthn stuff
	log.Debug().Msg("Creating webauthn id for new account")
	data := make([]byte, 64)
	c, err := rand.Read(data)
	for err != nil || c != len(data) || c < 64 {
		data = make([]byte, 64)
		c, err = rand.Read(data)
	}
	log.Debug().Msg("Random webauthn id for new account created")
	acc.ID = uuid.NewString()

	accountRole, err := s.NewEmptyRole(acc.ID)
	if err != nil {
		return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
	}
	accountRole.IsUserRole = true
	if err = s.UpdateRole(accountRole); err != nil {
		return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
	}

	acc.WebAuthnId = data
	acc.Relations = []uint{}
	acc.Tags = []string{}
	acc.Gender = []string{}
	acc.CustomFields = []uint{}
	acc.IdentifiesAs = []Being{}
	acc.PasskeyCredentials = []webauthn.Credential{}
	acc.Roles = []string{DefaultUserRole.Name, accountRole.Name}
	acc.Icon = "placeholder"
	log.Debug().Any("account", &acc).Msg("Saving new account in db")
	res := s.db.Save(&acc)
	if res.Error != nil {
		log.Error().Err(res.Error).Msg("Failed to safe new account")
		return nil, res.Error
	}
	log.Info().Str("account-id", acc.ID).Msg("Created new account")
	return &acc, nil
}

// Create a new local account using the given handle
// The handle in this case is only the part before the domain (example: @bob@example.com would have a handle of bob)
// It also sets up a bunch of values that tend to be obvious for local accounts
func (s *Storage) NewLocalAccount(handle string) (*Account, error) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Trace().Caller().Send()
	log.Debug().Str("account-handle", handle).Msg("Creating new local account")
	acc, err := s.NewEmptyAccount()
	if err != nil {
		log.Error().Err(err).Msg("Failed to create empty account for use")
		return nil, err
	}
	acc.Username = handle
	// acc.Server = serverSelf
	acc.ServerId = serverSelf.ID
	acc.DisplayName = handle

	publicKey, privateKey, err := ed25519.GenerateKey(nil)
	if err != nil {
		log.Error().Err(err).Msg("Failed to generate key pair for new local account")
		return nil, err
	}
	acc.PrivateKey = privateKey
	acc.PublicKey = publicKey

	log.Debug().
		Str("account-handle", handle).
		Str("account-id", acc.ID).
		Msg("Saving new local account")
	res := s.db.Save(acc)
	if res.Error != nil {
		log.Error().Err(res.Error).Any("account-full", acc).Msg("Failed to save local account")
		return nil, res.Error
	}
	log.Info().
		Str("account-handle", handle).
		Str("account-id", acc.ID).
		Msg("Created new local account")
	return acc, nil
}

func (s *Storage) DeleteAccount(accId string) error {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return s.db.Delete(&Account{ID: accId}).Error
}

// ---- Section WebAuthn.User
// Implements the webauthn.User interface for interaction with passkeys

func (a *Account) WebAuthnID() []byte {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return a.WebAuthnId
}

func (u *Account) WebAuthnName() string {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return u.Username
}

func (u *Account) WebAuthnDisplayName() string {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return u.DisplayName
}

func (u *Account) WebAuthnCredentials() []webauthn.Credential {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return u.PasskeyCredentials
}

func (u *Account) WebAuthnIcon() string {
	defer shared.Untrace(shared.Trace(&log.Logger))
	return ""
}

// ---- Section passkey.User
// Implements the passkey.User interface

func (u *Account) PutCredential(new webauthn.Credential) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	u.PasskeyCredentials = append(u.PasskeyCredentials, new)
}

// Section passkey.UserStore
// Implements the passkey.UserStore interface

func (s *Storage) GetOrCreateUser(userID string) passkey.User {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().
		Str("account-handle", userID).
		Msg("Looking for or creating account for passkey stuff")
	acc := &Account{}
	res := s.db.Where(Account{Username: userID, ServerId: serverSelf.ID}).
		First(acc)
	if errors.Is(res.Error, gorm.ErrRecordNotFound) {
		log.Debug().Str("account-handle", userID)
		var err error
		acc, err = s.NewLocalAccount(userID)
		if err != nil {
			log.Error().
				Err(err).
				Str("account-handle", userID).
				Msg("Failed to create new account for webauthn request")
			return nil
		}
	}
	return acc
}

func (s *Storage) GetUserByWebAuthnId(id []byte) passkey.User {
	defer shared.Untrace(shared.Trace(&log.Logger))
	log.Debug().Bytes("webauthn-id", id).Msg("Looking for account with webauthn id")
	acc := Account{}
	res := s.db.Where(Account{WebAuthnId: id}).First(&acc)
	if res.Error != nil {
		log.Error().
			Err(res.Error).
			Bytes("webauthn-id", id).
			Msg("Failed to find user with webauthn ID")
		return nil
	}
	log.Info().Msg("Found account with given webauthn id")
	return &acc
}

func (s *Storage) SaveUser(rawUser passkey.User) {
	defer shared.Untrace(shared.Trace(&log.Logger))
	user, ok := rawUser.(*Account)
	if !ok {
		log.Error().Any("raw-user", rawUser).Msg("Failed to cast raw user to db account")
	}
	s.db.Save(user)
}
