package server

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
	"github.com/AlliotTech/openalist/internal/conf"
	"github.com/AlliotTech/openalist/internal/model"
	"github.com/AlliotTech/openalist/internal/op"
	"github.com/AlliotTech/openalist/internal/setting"
	"github.com/AlliotTech/openalist/pkg/utils"
	"github.com/AlliotTech/openalist/server/ftp"
	"math/rand"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
)

type FtpMainDriver struct {
	settings     *ftpserver.Settings
	proxyHeader  *http.Header
	clients      map[uint32]ftpserver.ClientContext
	shutdownLock sync.RWMutex
	isShutdown   bool
	tlsConfig    *tls.Config
}

func NewMainDriver() (*FtpMainDriver, error) {
	header := &http.Header{}
	header.Add("User-Agent", setting.GetStr(conf.FTPProxyUserAgent))
	transferType := ftpserver.TransferTypeASCII
	if conf.Conf.FTP.DefaultTransferBinary {
		transferType = ftpserver.TransferTypeBinary
	}
	activeConnCheck := ftpserver.IPMatchDisabled
	if conf.Conf.FTP.EnableActiveConnIPCheck {
		activeConnCheck = ftpserver.IPMatchRequired
	}
	pasvConnCheck := ftpserver.IPMatchDisabled
	if conf.Conf.FTP.EnablePasvConnIPCheck {
		pasvConnCheck = ftpserver.IPMatchRequired
	}
	tlsRequired := ftpserver.ClearOrEncrypted
	if setting.GetBool(conf.FTPImplicitTLS) {
		tlsRequired = ftpserver.ImplicitEncryption
	} else if setting.GetBool(conf.FTPMandatoryTLS) {
		tlsRequired = ftpserver.MandatoryEncryption
	}
	tlsConf, err := getTlsConf(setting.GetStr(conf.FTPTLSPrivateKeyPath), setting.GetStr(conf.FTPTLSPublicCertPath))
	if err != nil && tlsRequired != ftpserver.ClearOrEncrypted {
		return nil, fmt.Errorf("FTP mandatory TLS has been enabled, but the certificate failed to load: %w", err)
	}
	return &FtpMainDriver{
		settings: &ftpserver.Settings{
			ListenAddr:                conf.Conf.FTP.Listen,
			PublicHost:                lookupIP(setting.GetStr(conf.FTPPublicHost)),
			PassiveTransferPortGetter: newPortMapper(setting.GetStr(conf.FTPPasvPortMap)),
			FindPasvPortAttempts:      conf.Conf.FTP.FindPasvPortAttempts,
			ActiveTransferPortNon20:   conf.Conf.FTP.ActiveTransferPortNon20,
			IdleTimeout:               conf.Conf.FTP.IdleTimeout,
			ConnectionTimeout:         conf.Conf.FTP.ConnectionTimeout,
			DisableMLSD:               false,
			DisableMLST:               false,
			DisableMFMT:               true,
			Banner:                    setting.GetStr(conf.Announcement),
			TLSRequired:               tlsRequired,
			DisableLISTArgs:           false,
			DisableSite:               false,
			DisableActiveMode:         conf.Conf.FTP.DisableActiveMode,
			EnableHASH:                false,
			DisableSTAT:               false,
			DisableSYST:               false,
			EnableCOMB:                false,
			DefaultTransferType:       transferType,
			ActiveConnectionsCheck:    activeConnCheck,
			PasvConnectionsCheck:      pasvConnCheck,
			SiteHandlers: map[string]ftpserver.SiteHandler{
				"SIZE": ftp.HandleSIZE,
			},
		},
		proxyHeader:  header,
		clients:      make(map[uint32]ftpserver.ClientContext),
		shutdownLock: sync.RWMutex{},
		isShutdown:   false,
		tlsConfig:    tlsConf,
	}, nil
}

func (d *FtpMainDriver) GetSettings() (*ftpserver.Settings, error) {
	return d.settings, nil
}

func (d *FtpMainDriver) ClientConnected(cc ftpserver.ClientContext) (string, error) {
	if d.isShutdown || !d.shutdownLock.TryRLock() {
		return "", errors.New("server has shutdown")
	}
	defer d.shutdownLock.RUnlock()
	d.clients[cc.ID()] = cc
	return "AList FTP Endpoint", nil
}

func (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {
	err := cc.Close()
	if err != nil {
		utils.Log.Errorf("failed to close client: %v", err)
	}
	delete(d.clients, cc.ID())
}

func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {
	var userObj *model.User
	var err error
	if user == "anonymous" || user == "guest" {
		userObj, err = op.GetGuest()
		if err != nil {
			return nil, err
		}
	} else {
		userObj, err = op.GetUserByName(user)
		if err != nil {
			return nil, err
		}
		passHash := model.StaticHash(pass)
		if err = userObj.ValidatePwdStaticHash(passHash); err != nil {
			return nil, err
		}
	}
	if userObj.Disabled || !userObj.CanFTPAccess() {
		return nil, errors.New("user is not allowed to access via FTP")
	}

	ctx := context.Background()
	ctx = context.WithValue(ctx, "user", userObj)
	if user == "anonymous" || user == "guest" {
		ctx = context.WithValue(ctx, "meta_pass", pass)
	} else {
		ctx = context.WithValue(ctx, "meta_pass", "")
	}
	ctx = context.WithValue(ctx, "client_ip", cc.RemoteAddr().String())
	ctx = context.WithValue(ctx, "proxy_header", d.proxyHeader)
	return ftp.NewAferoAdapter(ctx), nil
}

func (d *FtpMainDriver) GetTLSConfig() (*tls.Config, error) {
	if d.tlsConfig == nil {
		return nil, errors.New("TLS config not provided")
	}
	return d.tlsConfig, nil
}

func (d *FtpMainDriver) Stop() {
	d.isShutdown = true
	d.shutdownLock.Lock()
	defer d.shutdownLock.Unlock()
	for _, value := range d.clients {
		_ = value.Close()
	}
}

func lookupIP(host string) string {
	if host == "" || net.ParseIP(host) != nil {
		return host
	}
	ips, err := net.LookupIP(host)
	if err != nil || len(ips) == 0 {
		utils.Log.Fatalf("given FTP public host is invalid, and the default value will be used: %v", err)
		return ""
	}
	for _, ip := range ips {
		if ip.To4() != nil {
			return ip.String()
		}
	}
	v6 := ips[0].String()
	utils.Log.Warnf("no IPv4 record looked up, %s will be used as public host, and it might do not work.", v6)
	return v6
}

func newPortMapper(str string) ftpserver.PasvPortGetter {
	if str == "" {
		return nil
	}
	pasvPortMappers := strings.Split(strings.Replace(str, "\n", ",", -1), ",")
	type group struct {
		ExposedStart  int
		ListenedStart int
		Length        int
	}
	groups := make([]group, len(pasvPortMappers))
	totalLength := 0
	convertToPorts := func(str string) (int, int, error) {
		start, end, multi := strings.Cut(str, "-")
		if multi {
			si, err := strconv.Atoi(start)
			if err != nil {
				return 0, 0, err
			}
			ei, err := strconv.Atoi(end)
			if err != nil {
				return 0, 0, err
			}
			if ei < si || ei < 1024 || si < 1024 || ei > 65535 || si > 65535 {
				return 0, 0, errors.New("invalid port")
			}
			return si, ei - si + 1, nil
		} else {
			ret, err := strconv.Atoi(str)
			if err != nil {
				return 0, 0, err
			} else {
				return ret, 1, nil
			}
		}
	}
	for i, mapper := range pasvPortMappers {
		var err error
		exposed, listened, mapped := strings.Cut(mapper, ":")
		for {
			if mapped {
				var es, ls, el, ll int
				es, el, err = convertToPorts(exposed)
				if err != nil {
					break
				}
				ls, ll, err = convertToPorts(listened)
				if err != nil {
					break
				}
				if el != ll {
					err = errors.New("the number of exposed ports and listened ports does not match")
					break
				}
				groups[i].ExposedStart = es
				groups[i].ListenedStart = ls
				groups[i].Length = el
				totalLength += el
			} else {
				var start, length int
				start, length, err = convertToPorts(mapper)
				groups[i].ExposedStart = start
				groups[i].ListenedStart = start
				groups[i].Length = length
				totalLength += length
			}
			break
		}
		if err != nil {
			utils.Log.Fatalf("failed to convert FTP PASV port mapper %s: %v, the port mapper will be ignored.", mapper, err)
			return nil
		}
	}
	return func() (int, int, bool) {
		idxPort := rand.Intn(totalLength)
		for _, g := range groups {
			if idxPort >= g.Length {
				idxPort -= g.Length
			} else {
				return g.ExposedStart + idxPort, g.ListenedStart + idxPort, true
			}
		}
		// unreachable
		return 0, 0, false
	}
}

func getTlsConf(keyPath, certPath string) (*tls.Config, error) {
	if keyPath == "" || certPath == "" {
		return nil, errors.New("private key or certificate is not provided")
	}
	cert, err := os.ReadFile(certPath)
	if err != nil {
		return nil, err
	}
	key, err := os.ReadFile(keyPath)
	if err != nil {
		return nil, err
	}
	tlsCert, err := tls.X509KeyPair(cert, key)
	if err != nil {
		return nil, err
	}
	return &tls.Config{Certificates: []tls.Certificate{tlsCert}}, nil
}
