package slack

import (
	"context"
	"encoding/json"
)

const (
	VTModal   ViewType = "modal"
	VTHomeTab ViewType = "home"
)

type ViewType string

type ViewState struct {
	Values map[string]map[string]BlockAction `json:"values"`
}

type View struct {
	SlackResponse
	ID                 string           `json:"id"`
	TeamID             string           `json:"team_id"`
	Type               ViewType         `json:"type"`
	Title              *TextBlockObject `json:"title"`
	Close              *TextBlockObject `json:"close"`
	Submit             *TextBlockObject `json:"submit"`
	Blocks             Blocks           `json:"blocks"`
	PrivateMetadata    string           `json:"private_metadata"`
	CallbackID         string           `json:"callback_id"`
	State              *ViewState       `json:"state"`
	Hash               string           `json:"hash"`
	ClearOnClose       bool             `json:"clear_on_close"`
	NotifyOnClose      bool             `json:"notify_on_close"`
	RootViewID         string           `json:"root_view_id"`
	PreviousViewID     string           `json:"previous_view_id"`
	AppID              string           `json:"app_id"`
	ExternalID         string           `json:"external_id"`
	BotID              string           `json:"bot_id"`
	AppInstalledTeamID string           `json:"app_installed_team_id"`
}

type ViewSubmissionCallbackResponseURL struct {
	BlockID     string `json:"block_id"`
	ActionID    string `json:"action_id"`
	ChannelID   string `json:"channel_id"`
	ResponseURL string `json:"response_url"`
}

type ViewSubmissionCallback struct {
	Hash         string                              `json:"hash"`
	ResponseURLs []ViewSubmissionCallbackResponseURL `json:"response_urls,omitempty"`
}

type ViewClosedCallback struct {
	IsCleared bool `json:"is_cleared"`
}

const (
	RAClear  ViewResponseAction = "clear"
	RAUpdate ViewResponseAction = "update"
	RAPush   ViewResponseAction = "push"
	RAErrors ViewResponseAction = "errors"
)

type ViewResponseAction string

type ViewSubmissionResponse struct {
	ResponseAction ViewResponseAction `json:"response_action"`
	View           *ModalViewRequest  `json:"view,omitempty"`
	Errors         map[string]string  `json:"errors,omitempty"`
}

func NewClearViewSubmissionResponse() *ViewSubmissionResponse {
	return &ViewSubmissionResponse{
		ResponseAction: RAClear,
	}
}

func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
	return &ViewSubmissionResponse{
		ResponseAction: RAUpdate,
		View:           view,
	}
}

func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
	return &ViewSubmissionResponse{
		ResponseAction: RAPush,
		View:           view,
	}
}

func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionResponse {
	return &ViewSubmissionResponse{
		ResponseAction: RAErrors,
		Errors:         errors,
	}
}

type ModalViewRequest struct {
	Type            ViewType         `json:"type"`
	Title           *TextBlockObject `json:"title,omitempty"`
	Blocks          Blocks           `json:"blocks"`
	Close           *TextBlockObject `json:"close,omitempty"`
	Submit          *TextBlockObject `json:"submit,omitempty"`
	PrivateMetadata string           `json:"private_metadata,omitempty"`
	CallbackID      string           `json:"callback_id,omitempty"`
	ClearOnClose    bool             `json:"clear_on_close,omitempty"`
	NotifyOnClose   bool             `json:"notify_on_close,omitempty"`
	ExternalID      string           `json:"external_id,omitempty"`
}

func (v *ModalViewRequest) ViewType() ViewType {
	return v.Type
}

type HomeTabViewRequest struct {
	Type            ViewType `json:"type"`
	Blocks          Blocks   `json:"blocks"`
	PrivateMetadata string   `json:"private_metadata,omitempty"`
	CallbackID      string   `json:"callback_id,omitempty"`
	ExternalID      string   `json:"external_id,omitempty"`
}

func (v *HomeTabViewRequest) ViewType() ViewType {
	return v.Type
}

type openViewRequest struct {
	TriggerID string           `json:"trigger_id"`
	View      ModalViewRequest `json:"view"`
}

type publishViewRequest struct {
	UserID string             `json:"user_id"`
	View   HomeTabViewRequest `json:"view"`
	Hash   string             `json:"hash,omitempty"`
}

type pushViewRequest struct {
	TriggerID string           `json:"trigger_id"`
	View      ModalViewRequest `json:"view"`
}

type updateViewRequest struct {
	View       ModalViewRequest `json:"view"`
	ExternalID string           `json:"external_id,omitempty"`
	Hash       string           `json:"hash,omitempty"`
	ViewID     string           `json:"view_id,omitempty"`
}

type ViewResponse struct {
	SlackResponse
	View `json:"view"`
}

// OpenView opens a view for a user.
func (api *Client) OpenView(triggerID string, view ModalViewRequest) (*ViewResponse, error) {
	return api.OpenViewContext(context.Background(), triggerID, view)
}

// ValidateUniqueBlockID will verify if each input block has a unique block ID if set
func ValidateUniqueBlockID(view ModalViewRequest) bool {

	uniqueBlockID := map[string]bool{}

	for _, b := range view.Blocks.BlockSet {
		if inputBlock, ok := b.(*InputBlock); ok {
			if _, ok := uniqueBlockID[inputBlock.BlockID]; ok {
				return false
			}
			uniqueBlockID[inputBlock.BlockID] = true
		}
	}

	return true
}

// OpenViewContext opens a view for a user with a custom context.
func (api *Client) OpenViewContext(
	ctx context.Context,
	triggerID string,
	view ModalViewRequest,
) (*ViewResponse, error) {
	if triggerID == "" {
		return nil, ErrParametersMissing
	}

	if !ValidateUniqueBlockID(view) {
		return nil, ErrBlockIDNotUnique
	}

	req := openViewRequest{
		TriggerID: triggerID,
		View:      view,
	}
	encoded, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	endpoint := api.endpoint + "views.open"
	resp := &ViewResponse{}
	err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
	if err != nil {
		return nil, err
	}
	return resp, resp.Err()
}

// PublishView publishes a static view for a user.
func (api *Client) PublishView(userID string, view HomeTabViewRequest, hash string) (*ViewResponse, error) {
	return api.PublishViewContext(context.Background(), userID, view, hash)
}

// PublishViewContext publishes a static view for a user with a custom context.
func (api *Client) PublishViewContext(
	ctx context.Context,
	userID string,
	view HomeTabViewRequest,
	hash string,
) (*ViewResponse, error) {
	if userID == "" {
		return nil, ErrParametersMissing
	}
	req := publishViewRequest{
		UserID: userID,
		View:   view,
		Hash:   hash,
	}
	encoded, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	endpoint := api.endpoint + "views.publish"
	resp := &ViewResponse{}
	err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
	if err != nil {
		return nil, err
	}
	return resp, resp.Err()
}

// PushView pushes a view onto the stack of a root view.
func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewResponse, error) {
	return api.PushViewContext(context.Background(), triggerID, view)
}

// PublishViewContext pushes a view onto the stack of a root view with a custom context.
func (api *Client) PushViewContext(
	ctx context.Context,
	triggerID string,
	view ModalViewRequest,
) (*ViewResponse, error) {
	if triggerID == "" {
		return nil, ErrParametersMissing
	}
	req := pushViewRequest{
		TriggerID: triggerID,
		View:      view,
	}
	encoded, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	endpoint := api.endpoint + "views.push"
	resp := &ViewResponse{}
	err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
	if err != nil {
		return nil, err
	}
	return resp, resp.Err()
}

// UpdateView updates an existing view.
func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID string) (*ViewResponse, error) {
	return api.UpdateViewContext(context.Background(), view, externalID, hash, viewID)
}

// UpdateViewContext updates an existing view with a custom context.
func (api *Client) UpdateViewContext(
	ctx context.Context,
	view ModalViewRequest,
	externalID, hash,
	viewID string,
) (*ViewResponse, error) {
	if externalID == "" && viewID == "" {
		return nil, ErrParametersMissing
	}
	req := updateViewRequest{
		View:       view,
		ExternalID: externalID,
		Hash:       hash,
		ViewID:     viewID,
	}
	encoded, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	endpoint := api.endpoint + "views.update"
	resp := &ViewResponse{}
	err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
	if err != nil {
		return nil, err
	}
	return resp, resp.Err()
}
