package plg_backend_ftp

import (
	"context"
	"crypto/tls"
	"fmt"
	. "github.com/mickael-kerjean/filestash/server/common"
	//"github.com/secsy/goftp" <- FTP issue with microsoft FTP
	"github.com/prasad83/goftp"
	"io"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

var FtpCache AppCache

type Ftp struct {
	client *goftp.Client
	p      map[string]string
	wg     *sync.WaitGroup
	ctx    context.Context
}

func init() {
	Backend.Register("ftp", Ftp{})

	FtpCache = NewAppCache(2, 1)
	FtpCache.OnEvict(func(key string, value interface{}) {
		c := value.(*Ftp)
		if c == nil {
			Log.Warning("plg_backend_ftp::ftp is nil on close")
			return
		} else if c.wg == nil {
			Log.Warning("plg_backend_ftp::wg is nil on close")
			c.Close()
			return
		}
		c.wg.Wait()
		Log.Debug("plg_backend_ftp::vacuum")
		c.Close()
	})
}

func (f Ftp) Init(params map[string]string, app *App) (IBackend, error) {
	if c := FtpCache.Get(params); c != nil {
		d := c.(*Ftp)
		if d == nil {
			Log.Warning("plg_backend_ftp::ftp is nil on get")
			return nil, ErrInternal
		} else if d.wg == nil {
			Log.Warning("plg_backend_ftp::wg is nil on get")
			return nil, ErrInternal
		}
		d.wg.Add(1)
		d.ctx = app.Context
		go func() {
			<-d.ctx.Done()
			d.wg.Done()
		}()
		return d, nil
	}
	if params["hostname"] == "" {
		params["hostname"] = "localhost"
	}

	if params["port"] == "" {
		params["port"] = "21"
	}
	if params["username"] == "" {
		params["acl"] = "r"
		params["username"] = "anonymous"
	}
	if params["username"] == "anonymous" && params["password"] == "" {
		params["password"] = "anonymous"
	}
	conn := 5
	if params["conn"] != "" {
		if i, err := strconv.Atoi(params["conn"]); err == nil && i > 0 {
			conn = i
		}
	}

	connectStrategy := []string{"ftp", "ftps::implicit", "ftps::explicit"}
	if strings.HasPrefix(params["hostname"], "ftp://") {
		connectStrategy = []string{"ftp"}
		params["hostname"] = strings.TrimPrefix(params["hostname"], "ftp://")
	} else if strings.HasPrefix(params["hostname"], "ftps://") {
		connectStrategy = []string{"ftps::implicit", "ftps::explicit"}
		params["hostname"] = strings.TrimPrefix(params["hostname"], "ftps://")
	}

	var backend *Ftp = nil
	hostname := fmt.Sprintf("%s:%s", params["hostname"], params["port"])
	cfgBuilder := func(timeout time.Duration, withTLS bool) goftp.Config {
		cfg := goftp.Config{
			User:               params["username"],
			Password:           params["password"],
			ConnectionsPerHost: conn,
			Timeout:            timeout * time.Second,
		}
		cfg.Timeout = timeout
		if withTLS {
			cfg.TLSConfig = &tls.Config{
				InsecureSkipVerify:     true,
				ClientSessionCache:     tls.NewLRUClientSessionCache(0),
				SessionTicketsDisabled: false,
				ServerName:             hostname,
			}
		}
		return cfg
	}
	for i := 0; i < len(connectStrategy); i++ {
		if connectStrategy[i] == "ftp" {
			client, err := goftp.DialConfig(cfgBuilder(5*time.Second, false), hostname)
			if err != nil {
				Log.Debug("plg_backend_ftp::ftp dial %s", err.Error())
				continue
			} else if _, err := client.ReadDir("/"); err != nil {
				client.Close()
				Log.Debug("plg_backend_ftp::ftp verify %s", err.Error())
				continue
			}
			client.Close()
			client, err = goftp.DialConfig(cfgBuilder(60*time.Second, false), hostname)
			if err != nil {
				continue
			}
			params["mode"] = connectStrategy[i]
			backend = &Ftp{client, params, nil, app.Context}
			break
		} else if connectStrategy[i] == "ftps::implicit" {
			cfg := cfgBuilder(60*time.Second, true)
			cfg.TLSMode = goftp.TLSImplicit
			client, err := goftp.DialConfig(cfg, hostname)
			if err != nil {
				Log.Debug("plg_backend_ftp::ftps::implicit dial %s", err.Error())
				continue
			} else if _, err := client.ReadDir("/"); err != nil {
				Log.Debug("plg_backend_ftp::ftps::implicit verify %s", err.Error())
				client.Close()
				continue
			}
			params["mode"] = connectStrategy[i]
			backend = &Ftp{client, params, nil, app.Context}
			break
		} else if connectStrategy[i] == "ftps::explicit" {
			cfg := cfgBuilder(5*time.Second, true)
			cfg.TLSMode = goftp.TLSExplicit
			client, err := goftp.DialConfig(cfg, hostname)
			if err != nil {
				Log.Debug("plg_backend_ftp::ftps::explicit dial '%s'", err.Error())
				continue
			} else if _, err := client.ReadDir("/"); err != nil {
				Log.Debug("plg_backend_ftp::ftps::explicit verify %s", err.Error())
				client.Close()
				continue
			}
			client.Close()
			cfg = cfgBuilder(60*time.Second, true)
			cfg.TLSMode = goftp.TLSExplicit
			client, err = goftp.DialConfig(cfg, hostname)
			if err != nil {
				continue
			}
			params["mode"] = connectStrategy[i]
			backend = &Ftp{client, params, nil, app.Context}
			break
		}
	}
	if backend == nil {
		return nil, ErrAuthenticationFailed
	}
	backend.wg = new(sync.WaitGroup)
	backend.wg.Add(1)
	backend.ctx = app.Context
	go func() {
		<-backend.ctx.Done()
		backend.wg.Done()
	}()
	FtpCache.Set(params, backend)
	return backend, nil
}

func (f Ftp) LoginForm() Form {
	return Form{
		Elmnts: []FormElement{
			{
				Name:  "type",
				Type:  "hidden",
				Value: "ftp",
			},
			{
				Name:        "hostname",
				Type:        "text",
				Placeholder: "Hostname*",
			},
			{
				Name:        "username",
				Type:        "text",
				Placeholder: "Username",
			},
			{
				Name:        "password",
				Type:        "password",
				Placeholder: "Password",
			},
			{
				Name:        "advanced",
				Type:        "enable",
				Placeholder: "Advanced",
				Target:      []string{"ftp_path", "ftp_port", "ftp_conn"},
			},
			{
				Id:          "ftp_path",
				Name:        "path",
				Type:        "text",
				Placeholder: "Path",
			},
			{
				Id:          "ftp_port",
				Name:        "port",
				Type:        "number",
				Placeholder: "Port",
			},
			{
				Id:          "ftp_conn",
				Name:        "conn",
				Type:        "number",
				Placeholder: "Number of connections",
			},
		},
	}
}

func (f Ftp) Meta(path string) Metadata {
	if f.p["acl"] == "r" {
		return Metadata{
			CanCreateFile:      NewBool(false),
			CanCreateDirectory: NewBool(false),
			CanRename:          NewBool(false),
			CanMove:            NewBool(false),
			CanUpload:          NewBool(false),
			CanDelete:          NewBool(false),
		}
	}
	return Metadata{}
}

func (f Ftp) Home() (home string, err error) {
	f.Execute(func(client *goftp.Client) error {
		home, err = f.client.Getwd()
		return err
	})
	return home, err
}

func (f Ftp) Ls(path string) (files []os.FileInfo, err error) {
	f.Execute(func(client *goftp.Client) error {
		files, err = client.ReadDir(path)
		return err
	})
	return files, err
}

func (f Ftp) Cat(path string) (reader io.ReadCloser, err error) {
	f.Execute(func(client *goftp.Client) error {
		if _, err = client.Stat(path); err != nil {
			return err
		}
		pr, pw := io.Pipe()
		go func() {
			err = client.Retrieve(path, pw)
			if err != nil {
				pr.CloseWithError(NewError("Problem", 409))
			}
			pw.Close()
		}()
		reader = pr
		return nil
	})
	return reader, err
}

func (f Ftp) Mkdir(path string) (err error) {
	f.Execute(func(client *goftp.Client) error {
		_, err = client.Mkdir(path)
		return err
	})
	return err
}

func (f Ftp) Rm(path string) (err error) {
	isDirectory := func(p string) bool {
		return regexp.MustCompile(`\/$`).MatchString(p)
	}
	transformError := func(e error) error {
		// For some reasons bsftp is struggling with the library
		// sometimes returning a 200 OK
		if e == nil {
			return nil
		}
		if obj, ok := e.(goftp.Error); ok {
			if obj.Code() < 300 && obj.Code() > 0 {
				return nil
			}
		}
		return e
	}
	var recursiveDelete func(client *goftp.Client, _path string) error
	recursiveDelete = func(client *goftp.Client, _path string) error {
		if isDirectory(_path) {
			entries, err := client.ReadDir(_path)
			if transformError(err) != nil {
				return err
			}
			for _, entry := range entries {
				if entry.IsDir() {
					err = recursiveDelete(client, _path+entry.Name()+"/")
					if transformError(err) != nil {
						return err
					}
				} else {
					err = recursiveDelete(client, _path+entry.Name())
					if transformError(err) != nil {
						return err
					}
				}
			}
			err = client.Rmdir(_path)
			return transformError(err)
		}
		err = client.Delete(_path)
		return transformError(err)
	}
	f.Execute(func(client *goftp.Client) error {
		err = recursiveDelete(client, path)
		return err
	})
	return err
}

func (f Ftp) Mv(from string, to string) (err error) {
	f.Execute(func(client *goftp.Client) error {
		err = client.Rename(from, to)
		return err
	})
	return err
}

func (f Ftp) Touch(path string) (err error) {
	f.Execute(func(client *goftp.Client) error {
		err = client.Store(path, strings.NewReader(""))
		return err
	})
	return err
}

func (f Ftp) Save(path string, file io.Reader) (err error) {
	f.Execute(func(client *goftp.Client) error {
		err = client.Store(path, file)
		return err
	})
	return err
}

func (f Ftp) Close() error {
	return f.client.Close()
}

func (f Ftp) Execute(fn func(*goftp.Client) error) {
	err := fn(f.client)
	if ftpErr, ok := err.(goftp.Error); ok {
		code := ftpErr.Code()
		if code == 421 || (code == 0 && err.Error() == "error reading response: EOF") {
			f.Close()
			FtpCache.Set(f.p, nil)
			if b, err := f.Init(f.p, &App{Context: f.ctx}); err == nil {
				fn(b.(*Ftp).client)
			}
		}
	}
}
