package tmi

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

	"context"

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

// Responses
type ChannelBannedUsersResponse struct {
	TotalCount  int                 `json:"_total"`
	BannedUsers []ChannelBannedUser `json:"banned_users"`
}

type ChannelUserBannedStatusResponse struct {
	BannedUser ChannelBannedUser `json:"banned_user"`
}

type ChannelUserBannedStatusParams struct {
	ChanneID              string  `json:"channel_id"`
	UserID                string  `json:"user_id"`
	StatusRequesterUserID *string `json:"requester_user_id"`
}

type ChannelBannedUser struct {
	BannedUserID    int        `json:"banned_user_id"`
	RequesterUserID *int       `json:"requester_user_id,omitempty"`
	BannedAt        time.Time  `json:"banned_at"`
	ExpiresAt       *time.Time `json:"expires_at"`
	ExpiresInMS     *int64     `json:"expires_in_ms"`
	Reason          *string    `json:"reason"`
}

type BanChannelUserResponse struct {
	ResponseCode              string             `json:"response_code"`
	BanStatus                 *ChannelBannedUser `json:"ban_status"`
	MinTimeoutDurationSeconds *int               `json:"min_timeout_duration_seconds,omitempty"`
	MaxTimeoutDurationSeconds *int               `json:"max_timeout_duration_seconds,omitempty"`
}

type UnbanChannelUserResponse struct {
	ResponseCode string             `json:"response_code"`
	BanStatus    *ChannelBannedUser `json:"ban_status"`
}

// Requests
type BanChannelUserRequest struct {
	BannedUser BanChannelUserParams `json:"banned_user"`
}

type BanChannelUserParams struct {
	BannedUserID    int     `json:"banned_user_id"`
	RequesterUserID int     `json:"requester_user_id"`
	ExpiresIn       *string `json:"expires_in"`
	Reason          *string `json:"reason"`
}

type UnbanChannelUserRequest struct {
	UnbannedUser UnbanChannelUserParams `json:"unbanned_user"`
}

type UnbanChannelUserParams struct {
	RequesterUserID int `json:"requester_user_id"`
}

func (c *clientImpl) BanUser(ctx context.Context, channelID, bannedUserID, requesterUserID string, expiresIn *string, reason *string, reqOpts *twitchclient.ReqOpts) (*BanChannelUserResponse, error) {
	path := fmt.Sprintf("/rooms/%s/bans", channelID)

	intBannedUserID, err := strconv.Atoi(bannedUserID)
	if err != nil {
		return nil, err
	}

	intRequesterUserID, err := strconv.Atoi(requesterUserID)
	if err != nil {
		return nil, err
	}

	bodyBytes, err := json.Marshal(
		BanChannelUserRequest{
			BannedUser: BanChannelUserParams{
				BannedUserID:    intBannedUserID,
				RequesterUserID: intRequesterUserID,
				ExpiresIn:       expiresIn,
				Reason:          reason,
			},
		},
	)
	if err != nil {
		return nil, err
	}

	req, err := c.NewRequest("PUT", path, bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, err
	}

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

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

	defer closeBody(resp)

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

	if resp.StatusCode != http.StatusOK {
		if decoded.ResponseCode != "" {
			return &decoded, nil
		}

		return nil, twitchclient.HandleFailedResponse(resp)
	}

	return &decoded, nil
}

func (c *clientImpl) UnbanUser(ctx context.Context, channelID, userID, requesterID string, reqOpts *twitchclient.ReqOpts) (string, *ChannelBannedUser, error) {
	path := fmt.Sprintf("/rooms/%s/bans/%s", channelID, userID)

	intRequesterUserID, err := strconv.Atoi(requesterID)
	if err != nil {
		return BadUnbanError, nil, err
	}

	bodyBytes, err := json.Marshal(
		UnbanChannelUserRequest{
			UnbannedUser: UnbanChannelUserParams{
				RequesterUserID: intRequesterUserID,
			},
		},
	)
	if err != nil {
		return "", nil, err
	}

	req, err := c.NewRequest("DELETE", path, bytes.NewReader(bodyBytes))
	if err != nil {
		return BadUnbanError, nil, err
	}

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

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

	defer closeBody(resp)

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

	if resp.StatusCode != http.StatusOK {
		return decoded.ResponseCode, nil, twitchclient.HandleFailedResponse(resp)
	}

	return decoded.ResponseCode, decoded.BanStatus, err
}

// GetChannelBannedUsers returns the details of a channel's bans.
func (c *clientImpl) GetChannelBannedUsers(ctx context.Context, channelID string, limit, offset int, includeTimeouts bool, reqOpts *twitchclient.ReqOpts) (ChannelBannedUsersResponse, error) {
	path := fmt.Sprintf("/rooms/%s/bans?limit=%d&offset=%d&include_timeouts=%t", channelID, limit, offset, includeTimeouts)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return ChannelBannedUsersResponse{}, err
	}

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

	defer closeBody(resp)

	if resp.StatusCode == http.StatusNotFound {
		// endpoint returns 404 if the channel does not exist
		return ChannelBannedUsersResponse{}, nil
	} else if resp.StatusCode != http.StatusOK {
		// unexpected status code
		return ChannelBannedUsersResponse{}, httpErrorImpl{
			statusCode: resp.StatusCode,
		}
	}

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

// 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.
func (c *clientImpl) GetBanStatus(ctx context.Context, channelID, userID string, statusRequesterID *string, reqOpts *twitchclient.ReqOpts) (ChannelBannedUser, bool, error) {
	path := fmt.Sprintf("/rooms/%s/bans", channelID)

	params := ChannelUserBannedStatusParams{
		ChanneID:              channelID,
		UserID:                userID,
		StatusRequesterUserID: statusRequesterID,
	}

	bodyBytes, err := json.Marshal(params)
	if err != nil {
		return ChannelBannedUser{}, false, err
	}

	req, err := c.NewRequest("POST", path, bytes.NewReader(bodyBytes))

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

	if resp.StatusCode == http.StatusNotFound {
		// endpoint returns 404 if a user is not banned in room
		return ChannelBannedUser{}, false, nil
	} else if resp.StatusCode != http.StatusOK {
		// unexpected status code
		return ChannelBannedUser{}, false, httpErrorImpl{
			statusCode: resp.StatusCode,
		}
	}

	decoded := ChannelUserBannedStatusResponse{}
	if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
		return ChannelBannedUser{}, false, err
	}
	return decoded.BannedUser, true, nil
}
