package server

import (
	"net/http"

	httputil "git.mstar.dev/mstar/goutils/http"
	"git.mstar.dev/mstar/goutils/sliceutils"
	"github.com/google/jsonapi"
	"github.com/rs/zerolog/hlog"

	"git.mstar.dev/mstar/linstrom/storage"
)

// No create account. That happens during passkey registration
// and remote accounts are getting created at fetch time
func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
	store := StorageFromRequest(r)
	log := hlog.FromRequest(r)

	accId := AccountIdFromRequest(r)
	acc, err := store.FindAccountById(accId)
	switch err {
	case nil:
		// Ok, do nothing
	case storage.ErrEntryNotFound:
		httputil.HttpErr(w, HttpErrIdNotFound, "account not found", http.StatusNotFound)
		return
	default:
		log.Error().Err(err).Str("account-id", accId).Msg("Failed to get account from storage")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to get account from storage",
			http.StatusInternalServerError,
		)
		return
	}
	actorId, ok := r.Context().Value(ContextKeyActorId).(string)
	if ok {
		// Logged in user is accessing account, check if target account has them blocked
		roles, err := store.FindRolesByNames(acc.Roles)
		if err != nil {
			log.Error().
				Err(err).
				Strs("role-names", acc.Roles).
				Msg("Failed to get roles from storage")
			httputil.HttpErr(
				w,
				HttpErrIdDbFailure,
				"Failed to get roles of target account",
				http.StatusInternalServerError,
			)
			return
		}
		collapsedRole := storage.CollapseRolesIntoOne(roles...)
		if sliceutils.Contains(collapsedRole.BlockedUsers, actorId) {
			// Actor account is in list of blocked accounts, deny access
			httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Access forbidden", http.StatusForbidden)
			return
		}
	}

	outAccount, err := convertAccountStorageToLinstrom(acc, store)
	if err != nil {
		log.Error().
			Err(err).
			Msg("Failed to convert storage account (and attached data) into linstrom API representation")
		httputil.HttpErr(
			w,
			HttpErrIdConversionFailure,
			"Failed to convert storage account and attached data into API representation",
			http.StatusInternalServerError,
		)
		return
	}
	err = jsonapi.MarshalPayload(w, outAccount)
	if err != nil {
		log.Error().Err(err).Any("account", outAccount).Msg("Failed to marshal and write account")
	}
}

func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {
	store := StorageFromRequest(r)
	log := hlog.FromRequest(r)
	// Assumption: There must be a valid session once this function is called due to middlewares
	actorId, _ := ActorIdFromRequest(r)
	apiTarget := linstromAccount{}
	err := jsonapi.UnmarshalPayload(r.Body, &apiTarget)
	if err != nil {
		httputil.HttpErr(w, HttpErrIdBadRequest, "bad body", http.StatusBadRequest)
		return
	}
	targetAccId := AccountIdFromRequest(r)
	if apiTarget.Id != targetAccId {
		httputil.HttpErr(
			w,
			HttpErrIdBadRequest,
			"Provided entity's id doesn't match path id",
			http.StatusConflict,
		)
		return
	}
	if !(actorId == apiTarget.Id) {
		httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Invalid permissions", http.StatusForbidden)
		return
	}
	dbTarget, err := store.FindAccountById(apiTarget.Id)
	// Assumption: The only sort of errors that can be returned are db failures.
	// The account not existing is not possible anymore since this is in a valid session
	// and a session is only injected if the actor account can be found
	if err != nil {
		log.Error().
			Err(err).
			Str("account-id", actorId).
			Msg("Failed to get account from db despite valid session")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to get account despite valid session",
			http.StatusInternalServerError,
		)
		return
	}

	// location, birthday, icon, banner, background, custom fields
	// bluesky federation, uhhh

	dbTarget.DisplayName = apiTarget.DisplayName
	dbTarget.Indexable = apiTarget.Indexable
	dbTarget.Description = apiTarget.Description
	// TODO: Figure out how to properly update custom fields
	dbTarget.Gender = apiTarget.Pronouns
	dbTarget.IdentifiesAs = sliceutils.Map(
		sliceutils.Filter(apiTarget.IdentifiesAs, func(t string) bool {
			return storage.IsValidBeing(t)
		}),
		func(t string) storage.Being { return storage.Being(t) },
	)
	dbTarget.Indexable = apiTarget.Indexable
	dbTarget.RestrictedFollow = apiTarget.RestrictedFollow
	err = store.UpdateAccount(dbTarget)
	if err != nil {
		log.Error().Err(err).Msg("Failed to update account in db")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to update db entries",
			http.StatusInternalServerError,
		)
		return
	}
	w.WriteHeader(http.StatusOK)
	newAccData, err := convertAccountStorageToLinstrom(dbTarget, store)
	if err != nil {
		log.Error().Err(err).Msg("Failed to convert updated account back into api form")
		httputil.HttpErr(
			w,
			HttpErrIdConversionFailure,
			"Failed to convert updated account back into api form",
			http.StatusInternalServerError,
		)
		return
	}
	err = jsonapi.MarshalPayload(w, newAccData)
	if err != nil {
		log.Error().Err(err).Msg("Failed to marshal and write updated account")
	}
}

func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {
	actorId, _ := ActorIdFromRequest(r)
	log := hlog.FromRequest(r)
	store := StorageFromRequest(r)
	targetAccountId := AccountIdFromRequest(r)
	if targetAccountId != actorId {
		log.Debug().
			Str("actor-id", actorId).
			Str("target-id", targetAccountId).
			Msg("Invalid attempt to delete account")
		httputil.HttpErr(w, HttpErrIdNotAuthenticated, "Action forbidden", http.StatusForbidden)
		return
	}
	log.Info().Str("account-id", actorId).Msg("Deleting account")
	acc, err := store.FindAccountById(targetAccountId)
	if err != nil {
		log.Error().Err(err).Str("account-id", actorId).Msg("Failed to get account for deletion")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to get account from db",
			http.StatusInternalServerError,
		)
		return
	}
	// allRoles, err := store.FindRolesByNames(acc.Roles)
	// collapsedRole := storage.CollapseRolesIntoOne(allRoles...)

	// TODO: Start job of sending out deletion messages to all federated servers

	// Clean up related data first
	// TODO: Also delete media files
	err = store.DeleteRoleByName(acc.ID)
	if err != nil {
		log.Error().
			Err(err).
			Str("role-name", acc.ID).
			Msg("Failed to delete user role for account deletion request")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to delete user role",
			http.StatusInternalServerError,
		)
		return
	}
	err = store.DeleteAllUserFieldsForAccountId(acc.ID)
	if err != nil {
		log.Error().
			Err(err).
			Str("account-id", acc.ID).
			Msg("Failed to delete custom info fields for account deletion")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to delete custom info fields",
			http.StatusInternalServerError,
		)
		return
	}
	err = store.DeleteAccount(actorId)
	if err != nil {
		log.Error().Err(err).Str("account-id", acc.ID).Msg("Failed to delete account")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to delete account from db",
			http.StatusInternalServerError,
		)
		return
	}
}

// Is logged in following accountId
func linstromIsFollowingToAccount(w http.ResponseWriter, r *http.Request) {
	store := StorageFromRequest(r)
	log := hlog.FromRequest(r)
	actorId, _ := ActorIdFromRequest(r)
	targetId := AccountIdFromRequest(r)

	relation, err := store.GetRelationBetween(actorId, targetId)
	var outData linstromRelation
	switch err {
	case nil:
		outData = linstromRelation{
			Id:        relation.ID,
			CreatedAt: relation.CreatedAt,
			UpdatedAt: relation.UpdatedAt,
			FromId:    relation.FromId,
			ToId:      relation.ToId,
			Accepted:  relation.Accepted,
			Requested: true,
		}
	case storage.ErrEntryNotFound:
		outData = linstromRelation{
			Id:        relation.ID,
			CreatedAt: relation.CreatedAt,
			UpdatedAt: relation.UpdatedAt,
			FromId:    relation.FromId,
			ToId:      relation.ToId,
			Accepted:  false,
			Requested: false,
		}
	default:
		log.Error().
			Err(err).
			Str("from-id", actorId).
			Str("to-id", targetId).
			Msg("Failed to get follow relation")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to get relation",
			http.StatusInternalServerError,
		)
	}

	err = jsonapi.MarshalPayload(w, outData)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to marshal response")
		httputil.HttpErr(
			w,
			HttpErrIdJsonMarshalFail,
			"Failed to marshal response",
			http.StatusInternalServerError,
		)
	}
}

func linstromIsFollowingFromAccount(w http.ResponseWriter, r *http.Request) {
	store := StorageFromRequest(r)
	log := hlog.FromRequest(r)
	actorId, _ := ActorIdFromRequest(r)
	targetId := AccountIdFromRequest(r)

	relation, err := store.GetRelationBetween(targetId, actorId)
	var outData linstromRelation
	switch err {
	case nil:
		outData = linstromRelation{
			Id:        relation.ID,
			CreatedAt: relation.CreatedAt,
			UpdatedAt: relation.UpdatedAt,
			FromId:    relation.FromId,
			ToId:      relation.ToId,
			Accepted:  relation.Accepted,
			Requested: true,
		}
	case storage.ErrEntryNotFound:
		outData = linstromRelation{
			Id:        relation.ID,
			CreatedAt: relation.CreatedAt,
			UpdatedAt: relation.UpdatedAt,
			FromId:    relation.FromId,
			ToId:      relation.ToId,
			Accepted:  false,
			Requested: false,
		}
	default:
		log.Error().
			Err(err).
			Str("from-id", targetId).
			Str("to-id", actorId).
			Msg("Failed to get follow relation")
		httputil.HttpErr(
			w,
			HttpErrIdDbFailure,
			"Failed to get relation",
			http.StatusInternalServerError,
		)
	}

	err = jsonapi.MarshalPayload(w, outData)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to marshal response")
		httputil.HttpErr(
			w,
			HttpErrIdJsonMarshalFail,
			"Failed to marshal response",
			http.StatusInternalServerError,
		)
	}
}

func linstromFollowAccount(w http.ResponseWriter, r *http.Request)   {}
func linstromUnfollowAccount(w http.ResponseWriter, r *http.Request) {}

func linstromIsBlockingAccount(w http.ResponseWriter, r *http.Request) {}
func linstromBlockAccount(w http.ResponseWriter, r *http.Request)      {}
func linstromUnblockAccount(w http.ResponseWriter, r *http.Request)    {}

func linstromIsMutedAccount(w http.ResponseWriter, r *http.Request) {}
func linstromMuteAccount(w http.ResponseWriter, r *http.Request)    {}
func linstromUnmuteAccount(w http.ResponseWriter, r *http.Request)  {}

func linstromReportAccount(w http.ResponseWriter, r *http.Request)        {}
func linstromRetractReportAccount(w http.ResponseWriter, r *http.Request) {}

func linstromAdminAddRoleAccount(w http.ResponseWriter, r *http.Request)    {}
func linstromAdminRemoveRoleAccount(w http.ResponseWriter, r *http.Request) {}
func linstromAdminWarnAccount(w http.ResponseWriter, r *http.Request)       {}

func linstromGetRole(w http.ResponseWriter, r *http.Request)    {}
func linstromCreateRole(w http.ResponseWriter, r *http.Request) {}
func linstromUpdateRole(w http.ResponseWriter, r *http.Request) {}
func linstromDeleteRole(w http.ResponseWriter, r *http.Request) {}
