package webgui

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config"
)

// PackageJSON is the structure of package.json of a plugin
type PackageJSON struct {
	Name        string `json:"name"`
	Version     string `json:"version"`
	Description string `json:"description"`
	Author      string `json:"author"`
	Copyright   string `json:"copyright"`
	License     string `json:"license"`
	Private     bool   `json:"private"`
	Homepage    string `json:"homepage"`
	TestURL     string `json:"testUrl"`
	Repository  struct {
		Type string `json:"type"`
		URL  string `json:"url"`
	} `json:"repository"`
	Bugs struct {
		URL string `json:"url"`
	} `json:"bugs"`
	Rclone RcloneConfig `json:"rclone"`
}

// RcloneConfig represents the rclone specific config
type RcloneConfig struct {
	HandlesType      []string `json:"handlesType"`
	PluginType       string   `json:"pluginType"`
	RedirectReferrer bool     `json:"redirectReferrer"`
	Test             bool     `json:"-"`
}

func (r *PackageJSON) isTesting() bool {
	return r.Rclone.Test
}

var (
	//loadedTestPlugins *Plugins
	cachePath string

	loadedPlugins *Plugins
	pluginsProxy  = &httputil.ReverseProxy{}
	// PluginsMatch is used for matching author and plugin name in the url path
	PluginsMatch = regexp.MustCompile(`^plugins\/([^\/]*)\/([^\/\?]+)[\/]?(.*)$`)
	// PluginsPath is the base path where webgui plugins are stored
	PluginsPath              string
	pluginsConfigPath        string
	availablePluginsJSONPath = "availablePlugins.json"
)

func init() {
	cachePath = filepath.Join(config.CacheDir, "webgui")
	PluginsPath = filepath.Join(cachePath, "plugins")
	pluginsConfigPath = filepath.Join(PluginsPath, "config")

	loadedPlugins = newPlugins(availablePluginsJSONPath)
	err := loadedPlugins.readFromFile()
	if err != nil {
		fs.Errorf(nil, "error reading available plugins: %v", err)
	}
}

// Plugins represents the structure how plugins are saved onto disk
type Plugins struct {
	mutex         sync.Mutex
	LoadedPlugins map[string]PackageJSON `json:"loadedPlugins"`
	fileName      string
}

func newPlugins(fileName string) *Plugins {
	p := Plugins{LoadedPlugins: map[string]PackageJSON{}}
	p.fileName = fileName
	p.mutex = sync.Mutex{}
	return &p
}

func (p *Plugins) readFromFile() (err error) {
	//p.mutex.Lock()
	//defer p.mutex.Unlock()
	err = CreatePathIfNotExist(pluginsConfigPath)
	if err != nil {
		return err
	}
	availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)
	_, err = os.Stat(availablePluginsJSON)
	if err == nil {
		data, err := ioutil.ReadFile(availablePluginsJSON)
		if err != nil {
			return err
		}
		err = json.Unmarshal(data, &p)
		if err != nil {
			fs.Logf(nil, "%s", err)
		}
		return nil
	} else if os.IsNotExist(err) {
		// path does not exist
		err = p.writeToFile()
		if err != nil {
			return err
		}
	}
	return nil
}

func (p *Plugins) addPlugin(pluginName string, packageJSONPath string) (err error) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	data, err := ioutil.ReadFile(packageJSONPath)
	if err != nil {
		return err
	}
	var pkgJSON = PackageJSON{}
	err = json.Unmarshal(data, &pkgJSON)
	if err != nil {
		return err
	}
	p.LoadedPlugins[pluginName] = pkgJSON

	err = p.writeToFile()
	if err != nil {
		return err
	}

	return nil
}

func (p *Plugins) addTestPlugin(pluginName string, testURL string, handlesType []string) (err error) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	err = p.readFromFile()
	if err != nil {
		return err
	}

	var pkgJSON = PackageJSON{
		Name:    pluginName,
		TestURL: testURL,
		Rclone: RcloneConfig{
			HandlesType: handlesType,
			Test:        true,
		},
	}

	p.LoadedPlugins[pluginName] = pkgJSON

	err = p.writeToFile()
	if err != nil {
		return err
	}

	return nil
}

func (p *Plugins) writeToFile() (err error) {
	//p.mutex.Lock()
	//defer p.mutex.Unlock()
	availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)

	file, err := json.MarshalIndent(p, "", " ")
	if err != nil {
		fs.Logf(nil, "%s", err)
	}
	err = ioutil.WriteFile(availablePluginsJSON, file, 0755)
	if err != nil {
		fs.Logf(nil, "%s", err)
	}
	return nil
}

func (p *Plugins) removePlugin(name string) (err error) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	err = p.readFromFile()
	if err != nil {
		return err
	}

	_, ok := p.LoadedPlugins[name]
	if !ok {
		return fmt.Errorf("plugin %s not loaded", name)
	}
	delete(p.LoadedPlugins, name)

	err = p.writeToFile()
	if err != nil {
		return err
	}
	return nil
}

// GetPluginByName returns the plugin object for the key (author/plugin-name)
func (p *Plugins) GetPluginByName(name string) (out *PackageJSON, err error) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	po, ok := p.LoadedPlugins[name]
	if !ok {
		return nil, fmt.Errorf("plugin %s not loaded", name)
	}
	return &po, nil

}

// getAuthorRepoBranchGithub gives author, repoName and branch from a github.com url
//	url examples:
//	https://github.com/rclone/rclone-webui-react/
//	http://github.com/rclone/rclone-webui-react
//	https://github.com/rclone/rclone-webui-react/tree/caman-js
// 	github.com/rclone/rclone-webui-react
//
func getAuthorRepoBranchGithub(url string) (author string, repoName string, branch string, err error) {
	repoURL := url
	repoURL = strings.Replace(repoURL, "https://", "", 1)
	repoURL = strings.Replace(repoURL, "http://", "", 1)

	urlSplits := strings.Split(repoURL, "/")

	if len(urlSplits) < 3 || len(urlSplits) > 5 || urlSplits[0] != "github.com" {
		return "", "", "", fmt.Errorf("invalid github url: %s", url)
	}

	// get branch name
	if len(urlSplits) == 5 && urlSplits[3] == "tree" {
		return urlSplits[1], urlSplits[2], urlSplits[4], nil
	}

	return urlSplits[1], urlSplits[2], "master", nil
}

func filterPlugins(plugins *Plugins, compare func(packageJSON *PackageJSON) bool) map[string]PackageJSON {
	output := map[string]PackageJSON{}

	for key, val := range plugins.LoadedPlugins {
		if compare(&val) {
			output[key] = val
		}
	}

	return output
}

// getDirectorForProxy is a helper function for reverse proxy of test plugins
func getDirectorForProxy(origin *url.URL) func(req *http.Request) {
	return func(req *http.Request) {
		req.Header.Add("X-Forwarded-Host", req.Host)
		req.Header.Add("X-Origin-Host", origin.Host)
		req.URL.Scheme = "http"
		req.URL.Host = origin.Host
		req.URL.Path = origin.Path
	}
}

// ServePluginOK checks the plugin url and uses reverse proxy to allow redirection for content not being served by rclone
func ServePluginOK(w http.ResponseWriter, r *http.Request, pluginsMatchResult []string) (ok bool) {
	testPlugin, err := loadedPlugins.GetPluginByName(fmt.Sprintf("%s/%s", pluginsMatchResult[1], pluginsMatchResult[2]))
	if err != nil {
		return false
	}
	if !testPlugin.Rclone.Test {
		return false
	}
	origin, _ := url.Parse(fmt.Sprintf("%s/%s", testPlugin.TestURL, pluginsMatchResult[3]))

	director := getDirectorForProxy(origin)

	pluginsProxy.Director = director
	pluginsProxy.ServeHTTP(w, r)
	return true
}

var referrerPathReg = regexp.MustCompile("^(https?):\\/\\/(.+):([0-9]+)?\\/(.*)\\/?\\?(.*)$")

// ServePluginWithReferrerOK check if redirectReferrer is set for the referred a plugin, if yes,
// sends a redirect to actual url. This function is useful for plugins to refer to absolute paths when
// the referrer in http.Request is set
func ServePluginWithReferrerOK(w http.ResponseWriter, r *http.Request, path string) (ok bool) {
	referrer := r.Referer()
	referrerPathMatch := referrerPathReg.FindStringSubmatch(referrer)

	if referrerPathMatch != nil && len(referrerPathMatch) > 3 {
		referrerPluginMatch := PluginsMatch.FindStringSubmatch(referrerPathMatch[4])
		if referrerPluginMatch != nil && len(referrerPluginMatch) > 2 {
			pluginKey := fmt.Sprintf("%s/%s", referrerPluginMatch[1], referrerPluginMatch[2])
			currentPlugin, err := loadedPlugins.GetPluginByName(pluginKey)
			if err != nil {
				return false
			}
			if currentPlugin.Rclone.RedirectReferrer {
				path = fmt.Sprintf("/plugins/%s/%s/%s", referrerPluginMatch[1], referrerPluginMatch[2], path)

				http.Redirect(w, r, path, http.StatusMovedPermanently)
				return true
			}
		}
	}
	return false
}
