package tmi

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"context"

	"code.justin.tv/foundation/twitchclient"
)

const (
	defaultStatSampleRate = 1.0
	defaultTimingXactName = "clue"
)

var (
	// ErrEntityNotFound should be returned by Clue methods that receive an ID that
	// does not resolve to a valid entity (user, channel, etc.).
	ErrEntityNotFound = errors.New("entity not found by ID")
)

// Client is a client into TMI's Clue service.
type Client interface {
	// IsVerifiedBot returns whether a user is a verified bot
	IsVerifiedBot(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (bool, error)

	// GetVerifiedBots returns a list of user IDs of verified bots.
	GetVerifiedBots(ctx context.Context, reqOpts *twitchclient.ReqOpts) (GetVerifiedBotsResponse, error)

	// BotProperties returns information about this user's bot status
	BotProperties(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (BotPropertiesResponse, error)

	// GetBots returns a list of user IDs of verified and known bots.
	GetBots(ctx context.Context, reqOpts *twitchclient.ReqOpts) (GetBotsResponse, error)

	// GetBanStatus returns true if the given user is banned from the given channel, or false otherwise.
	// If the user is banned from the channel it also returns the details of the ban action.
	// statusRequesterID is an optional parameter that contains the id of the user making this request.
	// If statusRequesterID is nil or the provided id doesn't belong to a user with moderator or higher
	// privileges, then the result won't include the id of the user that that originally created the ban,
	// as that information is deemed to be sensitive and only accessible to provileged users within that channel.
	GetBanStatus(ctx context.Context, channelID, userID string, statusRequesterUserID *string, reqOpts *twitchclient.ReqOpts) (ChannelBannedUser, bool, error)

	// GetChannelBannedUsers returns the details of bans in a channel. Empty if none exist.
	GetChannelBannedUsers(ctx context.Context, channelID string, limit, offset int, includeTimeouts bool, reqOpts *twitchclient.ReqOpts) (ChannelBannedUsersResponse, error)

	// BanUser sets a temporary or permanent ban on a user from a channel
	BanUser(ctx context.Context, channelID, bannedUserID, modUserID string, expiresIn *string, reason *string, reqOpts *twitchclient.ReqOpts) (*BanChannelUserResponse, error)

	// UnbanUser removes a temporary or permanent ban on a user from a channel
	UnbanUser(ctx context.Context, channelID, userID, modID string, reqOpts *twitchclient.ReqOpts) (string, *ChannelBannedUser, error)

	// GetInternal returns a json blob of room properties for a given channel.
	GetInternalRoomProperties(ctx context.Context, channelID string, reqOpts *twitchclient.ReqOpts) (*InternalRoomPropertiesResponse, error)

	// UpdateInternalRoomProperties updates room properties for a given channel.
	UpdateInternalRoomProperties(ctx context.Context, channelID string, params UpdateInternalRoomPropertiesRequest, reqOpts *twitchclient.ReqOpts) error

	// GetHostTargets returns the channels that a set of channels are hosting, if any.
	GetHostTargets(ctx context.Context, channelIDs []string, reqOpts *twitchclient.ReqOpts) (*HostTargetsResponse, error)

	// GetHosters returns the channels hosting target channel, if any.
	GetHosters(ctx context.Context, channelID string, reqOpts *twitchclient.ReqOpts) (*HostersResponse, error)

	// Gets the entity representing a message rejected by AutoMod
	GetAutoModRejectedMessage(ctx context.Context, msgID string, reqOpts *twitchclient.ReqOpts) (AutoModRejectedMessage, bool, error)

	// Approves a message already rejected by AutoMod
	ApproveAutoModRejected(ctx context.Context, msgID, requesterUserID string, reqOpts *twitchclient.ReqOpts) error

	// Denies a message already rejected by automod
	DenyAutoModRejected(ctx context.Context, msgID, requesterUserID string, reqOpts *twitchclient.ReqOpts) error

	// GlobalBannedWords returns all global banned words, indicating whether they are able to be opt out.
	GlobalBannedWords(ctx context.Context, reqOpts *twitchclient.ReqOpts) (*GlobalBannedWordsMessage, error)

	// ChannelBannedWords returns all channel banned words (not including global banned words).
	ChannelBannedWords(ctx context.Context, channelID, requesterUserID string, reqOpts *twitchclient.ReqOpts) (*ChannelBannedWordsMessage, error)

	// ChannelPermittedWords returns all channel permitted words.
	ChannelPermittedWords(ctx context.Context, channelID, requesterUserID string, reqOpts *twitchclient.ReqOpts) (*ChannelPermittedWordsMessage, error)

	// AddChannelBannedWord adds a single banned term to a channel
	AddChannelBannedWord(ctx context.Context, channelID, requesterUserID string, msg AddChannelBannedWordParams, reqOpts *twitchclient.ReqOpts) (*AddChannelBlockedTermResponse, error)

	// DeleteChannelBannedWord removes a single banned term from a channel
	DeleteChannelBannedWord(ctx context.Context, channelID, requesterUserID string, msg DeleteChannelBannedWordParams, reqOpts *twitchclient.ReqOpts) (*DeleteChannelBlockedTermResponse, error)

	// AddChannelPermittedWord adds a single permitted term to a channel
	AddChannelPermittedWord(ctx context.Context, channelID, requesterUserID string, msg AddChannelPermittedWordParams, reqOpts *twitchclient.ReqOpts) (*AddChannelPermittedTermResponse, error)

	// DeleteChannelPermittedWord removes a single permitted term from a channel
	DeleteChannelPermittedWord(ctx context.Context, channelID string, requesterUserID string, msg DeleteChannelPermittedWordParams, reqOpts *twitchclient.ReqOpts) (*DeleteChannelPermittedTermResponse, error)

	// SendMessage sends a chat message or command to a channel.
	// For chat messages, it returns the body and tag information of the processed and delivered message
	// For commands, it returns the output and result of the command
	// For errors, it returns an error response code and detailed error message
	SendMessage(ctx context.Context, params SendMessageParams, reqOpts *twitchclient.ReqOpts) (SendMessageResponse, error)

	// SendWhisper sends a whisper message via IRC.
	SendWhisper(ctx context.Context, params SendWhisperParams, reqOpts *twitchclient.ReqOpts) error

	// SendUserNotice sends a UserNotice system message.
	SendUserNotice(ctx context.Context, params SendUserNoticeParams, reqOpts *twitchclient.ReqOpts) error

	// ModifyUserProperties updates user properties such as chat color
	ModifyUserProperties(ctx context.Context, userID string, params ModifyUserPropertiesParams, reqOpts *twitchclient.ReqOpts) error

	// UserFirstRoomChat returns information about the user's first time
	// chatting in a room.
	UserFirstRoomChat(ctx context.Context, userID string, roomID string, reqOpts *twitchclient.ReqOpts) (UserFirstRoomChatResponse, error)

	// LastMessageTime returns the last time the target user sent a message in the target room
	// If the user has not sent a message, returns nil
	LastMessageTime(ctx context.Context, userID, roomID string, reqOpts *twitchclient.ReqOpts) (*time.Time, error)

	// SendExtensionMessage sends a chat message to a channel where the sender is an extension.
	SendExtensionMessage(ctx context.Context, message ExtensionMessage, reqOpts *twitchclient.ReqOpts) error

	// DeleteChatMessage deletes a single chat message from stream chat.
	DeleteChatMessage(ctx context.Context, params DeleteChatMessageRequest, reqOpts *twitchclient.ReqOpts) (DeleteChatMessageResponse, error)

	// ChatRestrictedReasons returns a list of reasons that a user cannot chat in a channel
	// If the user _can_ chat, the list will be empty
	ChatRestrictedReasons(ctx context.Context, params ChatRestrictedReasonsParams, reqOpts *twitchclient.ReqOpts) (ChatRestrictedReasonsResponse, error)
}

// NewClient creates and returns a client that can interact with Clue.
func NewClient(conf twitchclient.ClientConf) (Client, error) {
	if conf.TimingXactName == "" {
		conf.TimingXactName = defaultTimingXactName
	}
	c, err := twitchclient.NewClient(conf)
	if err != nil {
		return nil, err
	}
	return &clientImpl{Client: c}, nil
}

type clientImpl struct {
	twitchclient.Client
}

func closeBody(resp *http.Response) {
	if err := resp.Body.Close(); err != nil {
		_ = err // ignore
	}
}

// IsVerifiedBotResponse is meant only for an IsVerifiedBot response from Clue to be unmarshaled into.
type IsVerifiedBotResponse struct {
	IsVerifiedBot bool `json:"is_verified_bot"`
}

// BotPropertiesResponse is gives us back whether or not our bot is known or
type BotPropertiesResponse struct {
	IsVerifiedBot bool `json:"is_verified_bot"`
	IsKnownBot    bool `json:"is_known_bot"`
}

// LastMessageTimeResponse is meant only for a LastMessageTime response from Clue to be unmarshaled into.
type LastMessageTimeResponse struct {
	LastMessageTime *time.Time `json:"last_message_time"`
}

// IsVerifiedBot returns true if the user is a verified bot.
func (c *clientImpl) IsVerifiedBot(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (bool, error) {
	path := fmt.Sprintf("/users/%s/bot_status", userID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return false, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.is_verified_bot",
		StatSampleRate: defaultStatSampleRate,
	})
	var decoded IsVerifiedBotResponse
	if _, err := c.DoJSON(ctx, &decoded, req, combinedReqOpts); err != nil {
		return false, err
	}
	return decoded.IsVerifiedBot, nil
}

// BotProperties returns status information about what sort of bot this user is (if it is any)
func (c *clientImpl) BotProperties(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (BotPropertiesResponse, error) {
	path := fmt.Sprintf("/v2/users/%s/bot_status", userID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return BotPropertiesResponse{}, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.is_bot_v2",
		StatSampleRate: defaultStatSampleRate,
	})
	var decoded BotPropertiesResponse
	if _, err := c.DoJSON(ctx, &decoded, req, combinedReqOpts); err != nil {
		return BotPropertiesResponse{}, err
	}
	return decoded, nil
}

type InternalRoomPropertiesResponse struct {
	Room *InternalRoomProperties `json:"room"`
}

type InternalRoomProperties struct {
	ChannelId                  int                      `json:"id"`
	BroadcasterLanguageMode    bool                     `json:"broadcaster_language_enabled"`
	ChatDelayDuration          int                      `json:"chat_delay_duration"`
	ChatEmoteOnly              bool                     `json:"chat_emote_only"`
	ChatFastsubs               bool                     `json:"chat_fastsubs"`
	ChatRequireVerifiedAccount bool                     `json:"chat_require_verified_account"`
	ChatRules                  []string                 `json:"chat_rules"`
	FollowersOnlyDuration      int                      `json:"followers_only_duration"`
	HideChatLinks              bool                     `json:"hide_chat_links"`
	AutoModProperties          ChannelAutoModProperties `json:"automod_properties"`
	RitualsEnabled             bool                     `json:"rituals_enabled"`
	RitualsWhitelisted         bool                     `json:"rituals_whitelisted"`
	R9kOnlyChat                bool                     `json:"r9k_only_chat"`
	SubscribersOnlyChat        bool                     `json:"subscribers_only_chat"`
	SubOnlyModeMinTier         string                   `json:"sub_only_mode_min_tier"`
	WatchPartyOnlyChat         bool                     `json:"watch_party_only_chat"`
	SlowModeDuration           int                      `json:"slow_mode_duration"`
	AllowedChatterIDs          []string                 `json:"allowed_chatter_ids,omitempty"`
}

// GetInternalRoomProperties returns a json blob of room properties for a given channel.
func (c *clientImpl) GetInternalRoomProperties(ctx context.Context, channelID string, reqOpts *twitchclient.ReqOpts) (*InternalRoomPropertiesResponse, error) {
	path := fmt.Sprintf("/rooms/%s", channelID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.get_internal_room_properties",
		StatSampleRate: defaultStatSampleRate,
	})
	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}

	defer closeBody(resp)

	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrChannelNotFound
	}

	decoded := &InternalRoomPropertiesResponse{}
	if err := json.NewDecoder(resp.Body).Decode(decoded); err != nil {
		return nil, err
	}
	return decoded, nil
}

var ErrChannelNotFound = errors.New("channel not found")

func (c *clientImpl) SendMessage(ctx context.Context, params SendMessageParams, reqOpts *twitchclient.ReqOpts) (SendMessageResponse, error) {
	url := "/internal/send_message"
	bodyBytes, err := json.Marshal(params)
	if err != nil {
		return SendMessageResponse{}, err
	}
	req, err := c.NewRequest("POST", url, bytes.NewReader(bodyBytes))
	if err != nil {
		return SendMessageResponse{}, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.send_message",
		StatSampleRate: defaultStatSampleRate,
	})

	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return SendMessageResponse{}, err
	}

	defer closeBody(resp)

	if resp.StatusCode == http.StatusNotFound {
		return SendMessageResponse{}, ErrChannelNotFound
	} else if resp.StatusCode != http.StatusOK {
		return SendMessageResponse{}, fmt.Errorf("unexpected response code %d during call to SendMessage", resp.StatusCode)
	}

	var decoded SendMessageResponse
	if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
		return SendMessageResponse{}, err
	}
	return decoded, nil
}

func (c *clientImpl) SendUserNotice(ctx context.Context, params SendUserNoticeParams, reqOpts *twitchclient.ReqOpts) error {
	url := "/internal/usernotice"
	bodyBytes, err := json.Marshal(params)
	if err != nil {
		return err
	}
	req, err := c.NewRequest("POST", url, bytes.NewReader(bodyBytes))
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.send_usernotice",
		StatSampleRate: defaultStatSampleRate,
	})

	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}

	defer closeBody(resp)

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected response code %d during call to SendUserNotice", resp.StatusCode)
	}
	return nil
}

func (c *clientImpl) UserFirstRoomChat(ctx context.Context, userID string, roomID string, reqOpts *twitchclient.ReqOpts) (UserFirstRoomChatResponse, error) {
	path := fmt.Sprintf("/internal/users/%s/has_chatted/%s", userID, roomID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return UserFirstRoomChatResponse{}, nil
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.user_first_room_chat",
		StatSampleRate: defaultStatSampleRate,
	})
	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return UserFirstRoomChatResponse{}, nil
	}
	defer func() {
		if cerr := resp.Body.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()

	if resp.StatusCode == http.StatusNotFound {
		return UserFirstRoomChatResponse{}, nil
	} else if resp.StatusCode != http.StatusOK {
		return UserFirstRoomChatResponse{}, httpErrorImpl{
			statusCode: resp.StatusCode,
		}
	}

	decoded := UserFirstRoomChatResponse{}
	err = json.NewDecoder(resp.Body).Decode(&decoded)
	return decoded, err
}

func (c *clientImpl) ModifyUserProperties(ctx context.Context, userID string, params ModifyUserPropertiesParams, reqOpts *twitchclient.ReqOpts) error {
	url := fmt.Sprintf("/users/%s", userID)
	bodyBytes, err := json.Marshal(params)
	if err != nil {
		return err
	}
	req, err := c.NewRequest("PUT", url, bytes.NewReader(bodyBytes))
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.modify_user_properties",
		StatSampleRate: defaultStatSampleRate,
	})

	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}

	defer closeBody(resp)

	if resp.StatusCode == http.StatusBadRequest {
		return httpErrorImpl{
			statusCode: resp.StatusCode,
		}
	} else if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected response code %d during call to ModifyUserProperties", resp.StatusCode)
	}
	return nil
}

// LastMessageTime returns the last time the target user sent a message in the target room
// If the user has not sent a message, returns nil
func (c *clientImpl) LastMessageTime(ctx context.Context, userID, channelID string, reqOpts *twitchclient.ReqOpts) (*time.Time, error) {
	path := fmt.Sprintf("/internal/users/%s/last_message_time/%s", userID, channelID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.clue.last_message_time",
		StatSampleRate: defaultStatSampleRate,
	})
	resp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		if cerr := resp.Body.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	if resp.StatusCode != http.StatusOK {
		return nil, httpErrorImpl{
			statusCode: resp.StatusCode,
		}
	}

	var decoded LastMessageTimeResponse
	if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
		return nil, err
	}
	return decoded.LastMessageTime, nil
}
