package mediabrowser

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

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

type serverType int

const (
	JellyfinServer serverType = iota
	EmbyServer
)

type serverInfo struct {
	LocalAddress string `json:"LocalAddress"`
	Name         string `json:"ServerName"`
	Version      string `json:"Version"`
	OS           string `json:"OperatingSystem"`
	ID           string `json:"Id"`
}

// MediaBrowser is an api instance of Jellyfin/Emby.
type MediaBrowser struct {
	Server         string
	client         string
	version        string
	device         string
	deviceID       string
	useragent      string
	auth           string
	header         map[string]string
	ServerInfo     serverInfo
	Username       string
	password       string
	Authenticated  bool
	AccessToken    string
	userID         string
	httpClient     *http.Client
	loginParams    map[string]string
	userCache      []User
	CacheExpiry    time.Time
	cacheLength    int
	noFail         bool
	Hyphens        bool
	serverType     serverType
	timeoutHandler common.TimeoutHandler
}

// NewServer returns a new Jellyfin object.
func NewServer(st serverType, server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*MediaBrowser, error) {
	mb := &MediaBrowser{}
	mb.serverType = st
	mb.Server = server
	mb.client = client
	mb.version = version
	mb.device = device
	mb.deviceID = deviceID
	mb.useragent = fmt.Sprintf("%s/%s", client, version)
	mb.timeoutHandler = timeoutHandler
	mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version)
	mb.header = map[string]string{
		"Accept":               "application/json",
		"Content-type":         "application/json; charset=UTF-8",
		"X-Application":        mb.useragent,
		"Accept-Charset":       "UTF-8,*",
		"Accept-Encoding":      "gzip",
		"User-Agent":           mb.useragent,
		"X-Emby-Authorization": mb.auth,
	}
	mb.httpClient = &http.Client{
		Timeout: 10 * time.Second,
	}
	infoURL := fmt.Sprintf("%s/System/Info/Public", server)
	req, _ := http.NewRequest("GET", infoURL, nil)
	resp, err := mb.httpClient.Do(req)
	defer mb.timeoutHandler()
	if err == nil {
		data, _ := ioutil.ReadAll(resp.Body)
		json.Unmarshal(data, &mb.ServerInfo)
	}
	mb.cacheLength = cacheTimeout
	mb.CacheExpiry = time.Now()
	return mb, nil
}

func (mb *MediaBrowser) get(url string, params map[string]string) (string, int, error) {
	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 mb.header {
		req.Header.Add(name, value)
	}
	resp, err := mb.httpClient.Do(req)
	defer mb.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		if resp.StatusCode == 401 && mb.Authenticated {
			mb.Authenticated = false
			_, _, authErr := mb.Authenticate(mb.Username, mb.password)
			if authErr == nil {
				v1, v2, v3 := mb.get(url, params)
				return v1, v2, v3
			}
		}
		return "", resp.StatusCode, err
	}
	defer resp.Body.Close()
	var data io.Reader
	encoding := resp.Header.Get("Content-Encoding")
	switch encoding {
	case "gzip":
		data, _ = gzip.NewReader(resp.Body)
	default:
		data = resp.Body
	}
	buf := new(strings.Builder)
	io.Copy(buf, data)
	//var respData map[string]interface{}
	//json.NewDecoder(data).Decode(&respData)
	return buf.String(), resp.StatusCode, nil
}

func (mb *MediaBrowser) post(url string, data interface{}, response bool) (string, int, error) {
	params, _ := json.Marshal(data)
	req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
	for name, value := range mb.header {
		req.Header.Add(name, value)
	}
	resp, err := mb.httpClient.Do(req)
	defer mb.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		if resp.StatusCode == 401 && mb.Authenticated {
			mb.Authenticated = false
			_, _, authErr := mb.Authenticate(mb.Username, mb.password)
			if authErr == nil {
				v1, v2, v3 := mb.post(url, data, response)
				return v1, v2, v3
			}
		}
		return "", resp.StatusCode, err
	}
	if response {
		defer resp.Body.Close()
		var outData io.Reader
		switch resp.Header.Get("Content-Encoding") {
		case "gzip":
			outData, _ = gzip.NewReader(resp.Body)
		default:
			outData = resp.Body
		}
		buf := new(strings.Builder)
		io.Copy(buf, outData)
		return buf.String(), resp.StatusCode, nil
	}
	return "", resp.StatusCode, nil
}

// Authenticate attempts to authenticate using a username & password
func (mb *MediaBrowser) Authenticate(username, password string) (User, int, error) {
	mb.Username = username
	mb.password = password
	mb.loginParams = map[string]string{
		"Username": username,
		"Pw":       password,
		"Password": password,
	}
	buffer := &bytes.Buffer{}
	encoder := json.NewEncoder(buffer)
	encoder.SetEscapeHTML(false)
	err := encoder.Encode(mb.loginParams)
	if err != nil {
		return User{}, 0, err
	}
	// loginParams, _ := json.Marshal(jf.loginParams)
	url := fmt.Sprintf("%s/Users/authenticatebyname", mb.Server)
	req, err := http.NewRequest("POST", url, buffer)
	defer mb.timeoutHandler()
	if err != nil {
		return User{}, 0, err
	}
	for name, value := range mb.header {
		req.Header.Add(name, value)
	}
	resp, err := mb.httpClient.Do(req)
	if err != nil || resp.StatusCode != 200 {
		return User{}, resp.StatusCode, err
	}
	defer resp.Body.Close()
	var d io.Reader
	switch resp.Header.Get("Content-Encoding") {
	case "gzip":
		d, _ = gzip.NewReader(resp.Body)
	default:
		d = resp.Body
	}
	data, err := io.ReadAll(d)
	if err != nil {
		return User{}, 0, err
	}
	var respData map[string]interface{}
	json.Unmarshal(data, &respData)
	mb.AccessToken = respData["AccessToken"].(string)
	var user User
	ju, err := json.Marshal(respData["User"])
	if err != nil {
		return User{}, 0, err
	}
	json.Unmarshal(ju, &user)
	mb.userID = user.ID
	mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", mb.client, mb.device, mb.deviceID, mb.version, mb.AccessToken)
	mb.header["X-Emby-Authorization"] = mb.auth
	mb.Authenticated = true
	return user, resp.StatusCode, nil
}

// DeleteUser deletes the user corresponding to the provided ID.
func (mb *MediaBrowser) DeleteUser(userID string) (int, error) {
	if mb.serverType == JellyfinServer {
		return jfDeleteUser(mb, userID)
	}
	return embyDeleteUser(mb, userID)
}

// GetUsers returns all (visible) users on the Emby instance.
func (mb *MediaBrowser) GetUsers(public bool) ([]User, int, error) {
	if mb.serverType == JellyfinServer {
		return jfGetUsers(mb, public)
	}
	return embyGetUsers(mb, public)
}

// UserByName returns the user corresponding to the provided username.
func (mb *MediaBrowser) UserByName(username string, public bool) (User, int, error) {
	if mb.serverType == JellyfinServer {
		return jfUserByName(mb, username, public)
	}
	return embyUserByName(mb, username, public)
}

// UserByID returns the user corresponding to the provided ID.
func (mb *MediaBrowser) UserByID(userID string, public bool) (User, int, error) {
	if mb.serverType == JellyfinServer {
		return jfUserByID(mb, userID, public)
	}
	return embyUserByID(mb, userID, public)
}

// NewUser creates a new user with the provided username and password.
func (mb *MediaBrowser) NewUser(username, password string) (User, int, error) {
	if mb.serverType == JellyfinServer {
		return jfNewUser(mb, username, password)
	}
	return embyNewUser(mb, username, password)
}

// SetPolicy sets the access policy for the user corresponding to the provided ID.
func (mb *MediaBrowser) SetPolicy(userID string, policy Policy) (int, error) {
	if mb.serverType == JellyfinServer {
		return jfSetPolicy(mb, userID, policy)
	}
	return embySetPolicy(mb, userID, policy)
}

// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID.
func (mb *MediaBrowser) SetConfiguration(userID string, configuration Configuration) (int, error) {
	if mb.serverType == JellyfinServer {
		return jfSetConfiguration(mb, userID, configuration)
	}
	return embySetConfiguration(mb, userID, configuration)
}

// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
func (mb *MediaBrowser) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) {
	if mb.serverType == JellyfinServer {
		return jfGetDisplayPreferences(mb, userID)
	}
	return embyGetDisplayPreferences(mb, userID)
}

// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
func (mb *MediaBrowser) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) {
	if mb.serverType == JellyfinServer {
		return jfSetDisplayPreferences(mb, userID, displayprefs)
	}
	return embySetDisplayPreferences(mb, userID, displayprefs)
}