package features

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/grafana/grafana-plugin-sdk-go/backend"

	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/guardian"
	"github.com/grafana/grafana/pkg/services/sqlstore"
)

type actionType string

const (
	ActionSaved    actionType = "saved"
	ActionDeleted  actionType = "deleted"
	EditingStarted actionType = "editing-started"
	//EditingFinished actionType = "editing-finished"

	GitopsChannel = "grafana/dashboard/gitops"
)

// DashboardEvent events related to dashboards
type dashboardEvent struct {
	UID       string                 `json:"uid"`
	Action    actionType             `json:"action"` // saved, editing, deleted
	User      *models.UserDisplayDTO `json:"user,omitempty"`
	SessionID string                 `json:"sessionId,omitempty"`
	Message   string                 `json:"message,omitempty"`
	Dashboard *models.Dashboard      `json:"dashboard,omitempty"`
	Error     string                 `json:"error,omitempty"`
}

// DashboardHandler manages all the `grafana/dashboard/*` channels
type DashboardHandler struct {
	Publisher        models.ChannelPublisher
	ClientCount      models.ChannelClientCount
	Store            sqlstore.Store
	DashboardService dashboards.DashboardService
}

// GetHandlerForPath called on init
func (h *DashboardHandler) GetHandlerForPath(_ string) (models.ChannelHandler, error) {
	return h, nil // all dashboards share the same handler
}

// OnSubscribe for now allows anyone to subscribe to any dashboard
func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) {
	parts := strings.Split(e.Path, "/")
	if parts[0] == "gitops" {
		// gitops gets all changes for everything, so lets make sure it is an admin user
		if !user.HasRole(models.ROLE_ADMIN) {
			return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
		}
		return models.SubscribeReply{
			Presence: true,
		}, backend.SubscribeStreamStatusOK, nil
	}

	// make sure can view this dashboard
	if len(parts) == 2 && parts[0] == "uid" {
		query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId}
		if err := h.DashboardService.GetDashboard(ctx, &query); err != nil {
			logger.Error("Error getting dashboard", "query", query, "error", err)
			return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
		}

		dash := query.Result
		guard := guardian.New(ctx, dash.Id, user.OrgId, user)
		if canView, err := guard.CanView(); err != nil || !canView {
			return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil
		}

		return models.SubscribeReply{
			Presence:  true,
			JoinLeave: true,
		}, backend.SubscribeStreamStatusOK, nil
	}

	// Unknown path
	logger.Error("Unknown dashboard channel", "path", e.Path)
	return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
}

// OnPublish is called when someone begins to edit a dashboard
func (h *DashboardHandler) OnPublish(ctx context.Context, user *models.SignedInUser, e models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) {
	parts := strings.Split(e.Path, "/")
	if parts[0] == "gitops" {
		// gitops gets all changes for everything, so lets make sure it is an admin user
		if !user.HasRole(models.ROLE_ADMIN) {
			return models.PublishReply{}, backend.PublishStreamStatusPermissionDenied, nil
		}

		// Eventually this could broadcast a message back to the dashboard saying a pull request exists
		return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("not implemented yet")
	}

	// make sure can view this dashboard
	if len(parts) == 2 && parts[0] == "uid" {
		event := dashboardEvent{}
		err := json.Unmarshal(e.Data, &event)
		if err != nil || event.UID != parts[1] {
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("bad request")
		}
		if event.Action != EditingStarted {
			// just ignore the event
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???")
		}
		query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId}
		if err := h.DashboardService.GetDashboard(ctx, &query); err != nil {
			logger.Error("Unknown dashboard", "query", query)
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil
		}

		guard := guardian.New(ctx, query.Result.Id, user.OrgId, user)
		canEdit, err := guard.CanEdit()
		if err != nil {
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
		}

		// Ignore edit events if the user can not edit
		if !canEdit {
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP
		}

		// Tell everyone who is editing
		event.User = user.ToUserDisplayDTO()

		msg, err := json.Marshal(event)
		if err != nil {
			return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
		}
		return models.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil
	}

	return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil
}

// DashboardSaved should broadcast to the appropriate stream
func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
	msg, err := json.Marshal(event)
	if err != nil {
		return err
	}

	// Only broadcast non-error events
	if event.Error == "" {
		err = h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg)
		if err != nil {
			return err
		}
	}

	// Send everything to the gitops channel
	return h.Publisher(orgID, GitopsChannel, msg)
}

// DashboardSaved will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardSaved(orgID int64, user *models.UserDisplayDTO, message string, dashboard *models.Dashboard, err error) error {
	if err != nil && !h.HasGitOpsObserver(orgID) {
		return nil // only broadcast if it was OK
	}

	msg := dashboardEvent{
		UID:       dashboard.Uid,
		Action:    ActionSaved,
		User:      user,
		Message:   message,
		Dashboard: dashboard,
	}

	if err != nil {
		msg.Error = err.Error()
	}

	return h.publish(orgID, msg)
}

// DashboardDeleted will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardDeleted(orgID int64, user *models.UserDisplayDTO, uid string) error {
	return h.publish(orgID, dashboardEvent{
		UID:    uid,
		Action: ActionDeleted,
		User:   user,
	})
}

// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel
func (h *DashboardHandler) HasGitOpsObserver(orgID int64) bool {
	count, err := h.ClientCount(orgID, GitopsChannel)
	if err != nil {
		logger.Error("error getting client count", "error", err)
		return false
	}
	return count > 0
}
