package ombi

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/hrfee/jfa-go/common"
)

const (
	NotifAgentDiscord  = 1
	NotifAgentTelegram = 4
)

// Ombi represents a running Ombi instance.
type Ombi struct {
	server, key    string
	header         map[string]string
	httpClient     *http.Client
	userCache      []map[string]interface{}
	cacheExpiry    time.Time
	cacheLength    int
	timeoutHandler common.TimeoutHandler
}

// NewOmbi returns an Ombi object.
func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
	return &Ombi{
		server: server,
		key:    key,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
		header: map[string]string{
			"ApiKey": key,
		},
		cacheLength:    30,
		cacheExpiry:    time.Now(),
		timeoutHandler: timeoutHandler,
	}
}

// does a GET and returns the response as a string.
func (ombi *Ombi) getJSON(url string, params map[string]string) (string, int, error) {
	if ombi.key == "" {
		return "", 401, fmt.Errorf("No API key provided")
	}
	var req *http.Request
	if params != nil {
		jsonParams, _ := json.Marshal(params)
		req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams))
	} else {
		req, _ = http.NewRequest("GET", url, nil)
	}
	for name, value := range ombi.header {
		req.Header.Add(name, value)
	}
	resp, err := ombi.httpClient.Do(req)
	defer ombi.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		if resp.StatusCode == 401 {
			return "", 401, fmt.Errorf("Invalid API Key")
		}
		return "", resp.StatusCode, err
	}
	defer resp.Body.Close()
	var data io.Reader
	switch resp.Header.Get("Content-Encoding") {
	case "gzip":
		data, _ = gzip.NewReader(resp.Body)
	default:
		data = resp.Body
	}
	buf := new(strings.Builder)
	_, err = io.Copy(buf, data)
	if err != nil {
		return "", 500, err
	}
	return buf.String(), resp.StatusCode, nil
}

// does a POST and optionally returns response as string. Returns a string instead of an io.reader bcs i couldn't get it working otherwise.
func (ombi *Ombi) send(mode string, url string, data interface{}, response bool, headers map[string]string) (string, int, error) {
	responseText := ""
	params, _ := json.Marshal(data)
	req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params))
	req.Header.Add("Content-Type", "application/json")
	for name, value := range ombi.header {
		req.Header.Add(name, value)
	}
	for name, value := range headers {
		req.Header.Add(name, value)
	}
	resp, err := ombi.httpClient.Do(req)
	defer ombi.timeoutHandler()
	if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
		if resp.StatusCode == 401 {
			return "", 401, fmt.Errorf("Invalid API Key")
		}
		return responseText, resp.StatusCode, err
	}
	if response {
		defer resp.Body.Close()
		var out io.Reader
		switch resp.Header.Get("Content-Encoding") {
		case "gzip":
			out, _ = gzip.NewReader(resp.Body)
		default:
			out = resp.Body
		}
		buf := new(strings.Builder)
		_, err = io.Copy(buf, out)
		if err != nil {
			return "", 500, err
		}
		responseText = buf.String()
	}
	return responseText, resp.StatusCode, nil
}

func (ombi *Ombi) post(url string, data map[string]interface{}, response bool) (string, int, error) {
	return ombi.send("POST", url, data, response, nil)
}

func (ombi *Ombi) put(url string, data map[string]interface{}, response bool) (string, int, error) {
	return ombi.send("PUT", url, data, response, nil)
}

// ModifyUser applies the given modified user object to the corresponding user.
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error) {
	if _, ok := user["id"]; !ok {
		err = fmt.Errorf("No ID provided")
		return
	}
	_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
	return
}

// DeleteUser deletes the user corresponding to the given ID.
func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
	url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
	req, _ := http.NewRequest("DELETE", url, nil)
	req.Header.Add("Content-Type", "application/json")
	for name, value := range ombi.header {
		req.Header.Add(name, value)
	}
	resp, err := ombi.httpClient.Do(req)
	defer ombi.timeoutHandler()
	return resp.StatusCode, err
}

// UserByID returns the user corresponding to the provided ID.
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, code int, err error) {
	resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
	json.Unmarshal([]byte(resp), &result)
	return
}

// GetUsers returns all users on the Ombi instance.
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
	if time.Now().After(ombi.cacheExpiry) {
		resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
		var result []map[string]interface{}
		json.Unmarshal([]byte(resp), &result)
		ombi.userCache = result
		if (code == 200 || code == 204) && err == nil {
			ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
		}
		return result, code, err
	}
	return ombi.userCache, 200, nil
}

// Strip these from a user when saving as a template.
// We also need to strip userQualityProfiles{"id", "userId"}
var stripFromOmbi = []string{
	"alias",
	"emailAddress",
	"hasLoggedIn",
	"id",
	"lastLoggedIn",
	"password",
	"userName",
}

// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code int, err error) {
	result, code, err = ombi.UserByID(id)
	if err != nil || code != 200 {
		return
	}
	for _, key := range stripFromOmbi {
		if _, ok := result[key]; ok {
			delete(result, key)
		}
	}
	if qp, ok := result["userQualityProfiles"].(map[string]interface{}); ok {
		delete(qp, "id")
		delete(qp, "userId")
		result["userQualityProfiles"] = qp
	}
	return
}

// NewUser creates a new user with the given username, password and email address.
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
	url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
	user := template
	user["userName"] = username
	user["password"] = password
	user["emailAddress"] = email
	resp, code, err := ombi.post(url, user, true)
	var data map[string]interface{}
	json.Unmarshal([]byte(resp), &data)
	if err != nil || code != 200 {
		var lst []string
		if data["errors"] != nil {
			lst = data["errors"].([]string)
		}
		return lst, code, err
	}
	ombi.cacheExpiry = time.Now()
	return nil, code, err
}

type NotificationPref struct {
	Agent   int    `json:"agent"`
	UserID  string `json:"userId"`
	Value   string `json:"value"`
	Enabled bool   `json:"enabled"`
}

func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, code int, err error) {
	id := user["id"].(string)
	url := fmt.Sprintf("%s/api/v1/Identity/NotificationPreferences", ombi.server)
	data := []NotificationPref{}
	if discordID != "" {
		data = append(data, NotificationPref{NotifAgentDiscord, id, discordID, true})
	}
	if telegramUser != "" {
		data = append(data, NotificationPref{NotifAgentTelegram, id, telegramUser, true})
	}
	result, code, err = ombi.send("POST", url, data, true, map[string]string{"UserName": user["userName"].(string)})
	return
}