package service

import (
	"bytes"
	"context"
	"embed"
	"encoding/json"
	"fmt"
	"strings"
	"text/template"
	"time"

	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
	invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"

	collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
	"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
	"github.com/owncloud/reva/v2/pkg/events"
	"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
	"github.com/owncloud/reva/v2/pkg/storagespace"
	"github.com/owncloud/reva/v2/pkg/utils"
)

//go:embed l10n/locale
var _translationFS embed.FS

var (
	_resourceTypeResource = "resource"
	_resourceTypeSpace    = "storagespace"
	_resourceTypeShare    = "share"

	_domain = "userlog"
)

// OC10Notification is the oc10 style representation of an event
// some fields are left out for simplicity
type OC10Notification struct {
	EventID        string                 `json:"notification_id"`
	Service        string                 `json:"app"`
	UserName       string                 `json:"user"`
	Timestamp      string                 `json:"datetime"`
	ResourceID     string                 `json:"object_id"`
	ResourceType   string                 `json:"object_type"`
	Subject        string                 `json:"subject"`
	SubjectRaw     string                 `json:"subjectRich"`
	Message        string                 `json:"message"`
	MessageRaw     string                 `json:"messageRich"`
	MessageDetails map[string]interface{} `json:"messageRichParameters"`
}

// Converter is responsible for converting eventhistory events to OC10Notifications
type Converter struct {
	locale                string
	gatewaySelector       pool.Selectable[gateway.GatewayAPIClient]
	serviceName           string
	translationPath       string
	defaultLanguage       string
	serviceAccountContext context.Context

	// cached within one request not to query other service too much
	spaces    map[string]*storageprovider.StorageSpace
	users     map[string]*user.User
	resources map[string]*storageprovider.ResourceInfo
}

// NewConverter returns a new Converter
func NewConverter(ctx context.Context, loc string, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], name, translationPath, defaultLanguage string) *Converter {
	return &Converter{
		locale:                loc,
		gatewaySelector:       gatewaySelector,
		serviceName:           name,
		translationPath:       translationPath,
		defaultLanguage:       defaultLanguage,
		serviceAccountContext: ctx,
		spaces:                make(map[string]*storageprovider.StorageSpace),
		users:                 make(map[string]*user.User),
		resources:             make(map[string]*storageprovider.ResourceInfo),
	}
}

// ConvertEvent converts an eventhistory event to an OC10Notification
func (c *Converter) ConvertEvent(eventid string, event interface{}) (OC10Notification, error) {
	switch ev := event.(type) {
	default:
		return OC10Notification{}, fmt.Errorf("unknown event type: %T", ev)
	// file related
	case events.PostprocessingStepFinished:
		switch ev.FinishedStep {
		case events.PPStepAntivirus:
			res := ev.Result.(events.VirusscanResult)
			return c.virusMessage(eventid, VirusFound, ev.ExecutingUser, res.ResourceID, ev.Filename, res.Description, res.Scandate)
		case events.PPStepPolicies:
			return c.policiesMessage(eventid, PoliciesEnforced, ev.ExecutingUser, ev.Filename, time.Now())
		default:
			return OC10Notification{}, fmt.Errorf("unknown postprocessing step: %s", ev.FinishedStep)
		}

	// space related
	case events.SpaceDisabled:
		return c.spaceMessage(eventid, SpaceDisabled, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp)
	case events.SpaceDeleted:
		return c.spaceDeletedMessage(eventid, ev.Executant, ev.ID.GetOpaqueId(), ev.SpaceName, ev.Timestamp)
	case events.SpaceShared:
		return c.spaceMessage(eventid, SpaceShared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp)
	case events.SpaceUnshared:
		return c.spaceMessage(eventid, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp)
	case events.SpaceMembershipExpired:
		return c.spaceMessage(eventid, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt)

	// share related
	case events.ShareCreated:
		return c.shareMessage(eventid, ShareCreated, ev.Executant, ev.ItemID, ev.ShareID, utils.TSToTime(ev.CTime))
	case events.ShareExpired:
		return c.shareMessage(eventid, ShareExpired, ev.ShareOwner, ev.ItemID, ev.ShareID, ev.ExpiredAt)
	case events.ShareRemoved:
		return c.shareMessage(eventid, ShareRemoved, ev.Executant, ev.ItemID, ev.ShareID, ev.Timestamp)
	case events.OCMCoreShareCreated:
		return c.omcShareCreatedMessage(ev, eventid)
	case events.OCMCoreShareDelete:
		return c.omcShareDeleteMessage(ev, eventid)
	}
}

// ConvertGlobalEvent converts a global event to an OC10Notification
func (c *Converter) ConvertGlobalEvent(typ string, data json.RawMessage) (OC10Notification, error) {
	switch typ {
	default:
		return OC10Notification{}, fmt.Errorf("unknown global event type: %s", typ)
	case "deprovision":
		var dd DeprovisionData
		if err := json.Unmarshal(data, &dd); err != nil {
			return OC10Notification{}, err
		}

		return c.deprovisionMessage(PlatformDeprovision, dd.DeprovisionDate.Format(dd.DeprovisionFormat))
	}

}

func (c *Converter) spaceDeletedMessage(eventid string, executant *user.UserId, spaceid string, spacename string, ts time.Time) (OC10Notification, error) {
	usr, err := c.getUser(context.Background(), executant)
	if err != nil {
		return OC10Notification{}, err
	}

	subj, subjraw, msg, msgraw, err := composeMessage(SpaceDeleted, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"username":  usr.GetDisplayName(),
		"spacename": spacename,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	space := &storageprovider.StorageSpace{Id: &storageprovider.StorageSpaceId{OpaqueId: spaceid}, Name: spacename}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       usr.GetUsername(),
		Timestamp:      ts.Format(time.RFC3339Nano),
		ResourceID:     spaceid,
		ResourceType:   _resourceTypeSpace,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: generateDetails(usr, space, nil, nil),
	}, nil
}

func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, executant *user.UserId, spaceid string, ts time.Time) (OC10Notification, error) {
	usr, err := c.getUser(context.Background(), executant)
	if err != nil {
		return OC10Notification{}, err
	}

	space, err := c.getSpace(c.serviceAccountContext, spaceid)
	if err != nil {
		return OC10Notification{}, err
	}

	subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"username":  usr.GetDisplayName(),
		"spacename": space.GetName(),
	})
	if err != nil {
		return OC10Notification{}, err
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       usr.GetUsername(),
		Timestamp:      ts.Format(time.RFC3339Nano),
		ResourceID:     spaceid,
		ResourceType:   _resourceTypeSpace,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: generateDetails(usr, space, nil, nil),
	}, nil
}

func (c *Converter) shareMessage(eventid string, nt NotificationTemplate, executant *user.UserId, resourceid *storageprovider.ResourceId, shareid *collaboration.ShareId, ts time.Time) (OC10Notification, error) {
	usr, err := c.getUser(context.Background(), executant)
	if err != nil {
		return OC10Notification{}, err
	}

	info, err := c.getResource(c.serviceAccountContext, resourceid)
	if err != nil {
		return OC10Notification{}, err
	}

	subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"username":     usr.GetDisplayName(),
		"resourcename": info.GetName(),
	})
	if err != nil {
		return OC10Notification{}, err
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       usr.GetUsername(),
		Timestamp:      ts.Format(time.RFC3339Nano),
		ResourceID:     storagespace.FormatResourceID(info.GetId()),
		ResourceType:   _resourceTypeShare,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: generateDetails(usr, nil, info, shareid),
	}, nil
}

func (c *Converter) omcShareCreatedMessage(ev events.OCMCoreShareCreated, eventid string) (OC10Notification, error) {
	sharerUser, err := c.getOCMUser(ev.Sharer, ev.GranteeUserID)
	if err != nil {
		return OC10Notification{}, err
	}

	shareid := &collaboration.ShareId{OpaqueId: ev.ShareID}
	subj, subjraw, msg, msgraw, err := composeMessage(ShareCreated, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"username":     sharerUser.GetDisplayName(),
		"resourcename": ev.ResourceName,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       sharerUser.GetDisplayName(),
		Timestamp:      utils.TSToTime(ev.CTime).Format(time.RFC3339Nano),
		ResourceID:     ev.ItemID,
		ResourceType:   _resourceTypeShare,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: generateDetails(sharerUser, nil, nil, shareid),
	}, nil
}

func (c *Converter) omcShareDeleteMessage(ev events.OCMCoreShareDelete, eventid string) (OC10Notification, error) {
	sharerUser, err := c.getOCMUser(ev.Sharer, ev.Grantee)
	if err != nil {
		return OC10Notification{}, err
	}

	shareid := &collaboration.ShareId{OpaqueId: ev.ShareID}
	subj, subjraw, msg, msgraw, err := composeMessage(ShareRemoved, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"username":     sharerUser.GetDisplayName(),
		"resourcename": ev.ResourceName,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       sharerUser.GetDisplayName(),
		Timestamp:      utils.TSToTime(ev.CTime).Format(time.RFC3339Nano),
		ResourceID:     ev.ShareID,
		ResourceType:   _resourceTypeShare,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: generateDetails(sharerUser, nil, nil, shareid),
	}, nil
}

func (c *Converter) getOCMUser(sharer *user.UserId, grantee *user.UserId) (*user.User, error) {
	gwc, err := c.gatewaySelector.Next()
	if err != nil {
		return nil, err
	}

	// Context c.serviceAccountContext will cause the user to be not found.
	// GetAcceptedUser() calls getUserFilter() that gets the user from the context
	// and use it as 'initiator' parameter in the GetRemoteUser() call.
	// The 'initiator' should be ev.Grantee.
	granteeJson, _ := json.Marshal(grantee)
	rspSharer, err := gwc.GetAcceptedUser(context.Background(), &invitepb.GetAcceptedUserRequest{
		RemoteUserId: sharer,
		Opaque: &typespb.Opaque{
			Map: map[string]*typespb.OpaqueEntry{
				"user-filter": {
					Decoder: "json",
					Value:   granteeJson,
				},
			},
		},
	})
	if err != nil {
		return nil, err
	}

	return rspSharer.GetRemoteUser(), nil
}

func (c *Converter) virusMessage(eventid string, nt NotificationTemplate, executant *user.User, rid *storageprovider.ResourceId, filename string, virus string, ts time.Time) (OC10Notification, error) {
	subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"resourcename":     filename,
		"virusdescription": virus,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	dets := map[string]interface{}{
		"resource": map[string]string{
			"name": filename,
		},
		"virus": map[string]interface{}{
			"name":     virus,
			"scandate": ts,
		},
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       executant.GetUsername(),
		Timestamp:      ts.Format(time.RFC3339Nano),
		ResourceID:     storagespace.FormatResourceID(rid),
		ResourceType:   _resourceTypeResource,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: dets,
	}, nil
}

func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, executant *user.User, filename string, ts time.Time) (OC10Notification, error) {
	subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"resourcename": filename,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	dets := map[string]interface{}{
		"resource": map[string]string{
			"name": filename,
		},
	}

	return OC10Notification{
		EventID:        eventid,
		Service:        c.serviceName,
		UserName:       executant.GetUsername(),
		Timestamp:      ts.Format(time.RFC3339Nano),
		ResourceType:   _resourceTypeResource,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: dets,
	}, nil
}

func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate string) (OC10Notification, error) {
	subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
		"date": deproDate,
	})
	if err != nil {
		return OC10Notification{}, err
	}

	return OC10Notification{
		EventID: "deprovision",
		Service: c.serviceName,
		// UserName:       executant.GetUsername(), // TODO: do we need the deprovisioner?
		Timestamp:      time.Now().Format(time.RFC3339Nano), // Fake timestamp? Or we store one with the event?
		ResourceType:   _resourceTypeResource,
		Subject:        subj,
		SubjectRaw:     subjraw,
		Message:        msg,
		MessageRaw:     msgraw,
		MessageDetails: map[string]interface{}{},
	}, nil
}

func (c *Converter) getSpace(ctx context.Context, spaceID string) (*storageprovider.StorageSpace, error) {
	if space, ok := c.spaces[spaceID]; ok {
		return space, nil
	}
	gwc, err := c.gatewaySelector.Next()
	if err != nil {
		return nil, err
	}
	space, err := utils.GetSpace(ctx, spaceID, gwc)
	if err == nil {
		c.spaces[spaceID] = space
	}
	return space, err
}

func (c *Converter) getResource(ctx context.Context, resourceID *storageprovider.ResourceId) (*storageprovider.ResourceInfo, error) {
	if r, ok := c.resources[resourceID.GetOpaqueId()]; ok {
		return r, nil
	}
	gwc, err := c.gatewaySelector.Next()
	if err != nil {
		return nil, err
	}
	resource, err := utils.GetResourceByID(ctx, resourceID, gwc)
	if err == nil {
		c.resources[resourceID.GetOpaqueId()] = resource
	}
	return resource, err
}

func (c *Converter) getUser(_ context.Context, userID *user.UserId) (*user.User, error) {
	if u, ok := c.users[userID.GetOpaqueId()]; ok {
		return u, nil
	}
	gwc, err := c.gatewaySelector.Next()
	if err != nil {
		return nil, err
	}
	usr, err := utils.GetUser(userID, gwc)
	if err == nil {
		c.users[userID.GetOpaqueId()] = usr
	}
	return usr, err
}

func composeMessage(nt NotificationTemplate, locale, defaultLocale, path string, vars map[string]interface{}) (string, string, string, string, error) {
	subjectraw, messageraw := loadTemplates(nt, locale, defaultLocale, path)

	subject, err := executeTemplate(subjectraw, vars)
	if err != nil {
		return "", "", "", "", err
	}

	message, err := executeTemplate(messageraw, vars)
	return subject, subjectraw, message, messageraw, err
}

func loadTemplates(nt NotificationTemplate, locale, defaultLocale, path string) (string, string) {
	t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, path, _translationFS, "l10n/locale").Locale(locale)
	return t.Get(nt.Subject), t.Get(nt.Message)
}

func executeTemplate(raw string, vars map[string]interface{}) (string, error) {
	for o, n := range _placeholders {
		raw = strings.ReplaceAll(raw, o, n)
	}
	tpl, err := template.New("").Parse(raw)
	if err != nil {
		return "", err
	}
	var writer bytes.Buffer
	if err := tpl.Execute(&writer, vars); err != nil {
		return "", err
	}

	return writer.String(), nil
}

func generateDetails(user *user.User, space *storageprovider.StorageSpace, item *storageprovider.ResourceInfo, shareid *collaboration.ShareId) map[string]interface{} {
	details := make(map[string]interface{})

	if user != nil {
		details["user"] = map[string]string{
			"id":          user.GetId().GetOpaqueId(),
			"name":        user.GetUsername(),
			"displayname": user.GetDisplayName(),
		}
	}

	if space != nil {
		details["space"] = map[string]string{
			"id":   space.GetId().GetOpaqueId(),
			"name": space.GetName(),
		}
	}

	if item != nil {
		details["resource"] = map[string]string{
			"id":   storagespace.FormatResourceID(item.GetId()),
			"name": item.GetName(),
		}
	}

	if shareid != nil {
		details["share"] = map[string]string{
			"id": shareid.GetOpaqueId(),
		}
	}

	return details
}
