// Copyright (C) 2019-2022  Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package webdavd

import (
	"context"
	"encoding/xml"
	"errors"
	"io"
	"mime"
	"net/http"
	"os"
	"path"
	"sync/atomic"
	"time"

	"github.com/eikenb/pipeat"
	"golang.org/x/net/webdav"

	"github.com/drakkan/sftpgo/v2/internal/common"
	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
	"github.com/drakkan/sftpgo/v2/internal/logger"
	"github.com/drakkan/sftpgo/v2/internal/util"
	"github.com/drakkan/sftpgo/v2/internal/vfs"
)

var (
	errTransferAborted = errors.New("transfer aborted")
	lastModifiedProps  = []string{"Win32LastModifiedTime", "getlastmodified"}
)

type webDavFile struct {
	*common.BaseTransfer
	writer      io.WriteCloser
	reader      io.ReadCloser
	info        os.FileInfo
	startOffset int64
	isFinished  bool
	readTryed   atomic.Bool
}

func newWebDavFile(baseTransfer *common.BaseTransfer, pipeWriter *vfs.PipeWriter, pipeReader *pipeat.PipeReaderAt) *webDavFile {
	var writer io.WriteCloser
	var reader io.ReadCloser
	if baseTransfer.File != nil {
		writer = baseTransfer.File
		reader = baseTransfer.File
	} else if pipeWriter != nil {
		writer = pipeWriter
	} else if pipeReader != nil {
		reader = pipeReader
	}
	f := &webDavFile{
		BaseTransfer: baseTransfer,
		writer:       writer,
		reader:       reader,
		isFinished:   false,
		startOffset:  0,
		info:         nil,
	}
	f.readTryed.Store(false)
	return f
}

type webDavFileInfo struct {
	os.FileInfo
	Fs          vfs.Fs
	virtualPath string
	fsPath      string
}

// ContentType implements webdav.ContentTyper interface
func (fi *webDavFileInfo) ContentType(ctx context.Context) (string, error) {
	extension := path.Ext(fi.virtualPath)
	contentType := mime.TypeByExtension(extension)
	if contentType != "" {
		return contentType, nil
	}
	contentType = mimeTypeCache.getMimeFromCache(extension)
	if contentType != "" {
		return contentType, nil
	}
	contentType, err := fi.Fs.GetMimeType(fi.fsPath)
	if contentType != "" {
		mimeTypeCache.addMimeToCache(extension, contentType)
		return contentType, err
	}
	return "", webdav.ErrNotImplemented
}

// Readdir reads directory entries from the handle
func (f *webDavFile) Readdir(count int) ([]os.FileInfo, error) {
	if !f.Connection.User.HasPerm(dataprovider.PermListItems, f.GetVirtualPath()) {
		return nil, f.Connection.GetPermissionDeniedError()
	}
	fileInfos, err := f.Connection.ListDir(f.GetVirtualPath())
	if err != nil {
		return nil, err
	}
	result := make([]os.FileInfo, 0, len(fileInfos))
	for _, fileInfo := range fileInfos {
		result = append(result, &webDavFileInfo{
			FileInfo:    fileInfo,
			Fs:          f.Fs,
			virtualPath: path.Join(f.GetVirtualPath(), fileInfo.Name()),
			fsPath:      f.Fs.Join(f.GetFsPath(), fileInfo.Name()),
		})
	}
	return result, nil
}

// Stat the handle
func (f *webDavFile) Stat() (os.FileInfo, error) {
	if f.GetType() == common.TransferDownload && !f.Connection.User.HasPerm(dataprovider.PermListItems, path.Dir(f.GetVirtualPath())) {
		return nil, f.Connection.GetPermissionDeniedError()
	}
	f.Lock()
	errUpload := f.ErrTransfer
	f.Unlock()
	if f.GetType() == common.TransferUpload && errUpload == nil {
		info := &webDavFileInfo{
			FileInfo:    vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Unix(0, 0), false),
			Fs:          f.Fs,
			virtualPath: f.GetVirtualPath(),
			fsPath:      f.GetFsPath(),
		}
		return info, nil
	}
	info, err := f.Fs.Stat(f.GetFsPath())
	if err != nil {
		return nil, err
	}
	if vfs.IsCryptOsFs(f.Fs) {
		info = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)
	}
	fi := &webDavFileInfo{
		FileInfo:    info,
		Fs:          f.Fs,
		virtualPath: f.GetVirtualPath(),
		fsPath:      f.GetFsPath(),
	}
	return fi, nil
}

// Read reads the contents to downloads.
func (f *webDavFile) Read(p []byte) (n int, err error) {
	if f.AbortTransfer.Load() {
		return 0, errTransferAborted
	}
	if !f.readTryed.Load() {
		if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
			return 0, f.Connection.GetPermissionDeniedError()
		}
		transferQuota := f.BaseTransfer.GetTransferQuota()
		if !transferQuota.HasDownloadSpace() {
			f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
			return 0, f.Connection.GetReadQuotaExceededError()
		}

		if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
			f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
			return 0, f.Connection.GetErrorForDeniedFile(policy)
		}
		err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
		if err != nil {
			f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
			return 0, f.Connection.GetPermissionDeniedError()
		}
		f.readTryed.Store(true)
	}

	f.Connection.UpdateLastActivity()

	// the file is read sequentially we don't need to check for concurrent reads and so
	// lock the transfer while opening the remote file
	if f.reader == nil {
		if f.GetType() != common.TransferDownload {
			f.TransferError(common.ErrOpUnsupported)
			return 0, common.ErrOpUnsupported
		}
		_, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
		f.Lock()
		if e == nil {
			f.reader = r
			f.BaseTransfer.SetCancelFn(cancelFn)
		}
		f.ErrTransfer = e
		f.startOffset = 0
		f.Unlock()
		if e != nil {
			return 0, e
		}
	}

	n, err = f.reader.Read(p)
	f.BytesSent.Add(int64(n))
	if err == nil {
		err = f.CheckRead()
	}
	if err != nil && err != io.EOF {
		f.TransferError(err)
		return
	}
	f.HandleThrottle()
	return
}

// Write writes the uploaded contents.
func (f *webDavFile) Write(p []byte) (n int, err error) {
	if f.AbortTransfer.Load() {
		return 0, errTransferAborted
	}

	f.Connection.UpdateLastActivity()

	n, err = f.writer.Write(p)
	f.BytesReceived.Add(int64(n))

	if err == nil {
		err = f.CheckWrite()
	}
	if err != nil {
		f.TransferError(err)
		return
	}
	f.HandleThrottle()
	return
}

func (f *webDavFile) updateStatInfo() error {
	if f.info != nil {
		return nil
	}
	info, err := f.Fs.Stat(f.GetFsPath())
	if err != nil {
		return err
	}
	if vfs.IsCryptOsFs(f.Fs) {
		info = f.Fs.(*vfs.CryptFs).ConvertFileInfo(info)
	}
	f.info = info
	return nil
}

func (f *webDavFile) updateTransferQuotaOnSeek() {
	transferQuota := f.GetTransferQuota()
	if transferQuota.HasSizeLimits() {
		go func(ulSize, dlSize int64, user dataprovider.User) {
			dataprovider.UpdateUserTransferQuota(&user, ulSize, dlSize, false) //nolint:errcheck
		}(f.BytesReceived.Load(), f.BytesSent.Load(), f.Connection.User)
	}
}

// Seek sets the offset for the next Read or Write on the writer to offset,
// interpreted according to whence: 0 means relative to the origin of the file,
// 1 means relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
func (f *webDavFile) Seek(offset int64, whence int) (int64, error) {
	f.Connection.UpdateLastActivity()
	if f.File != nil {
		ret, err := f.File.Seek(offset, whence)
		if err != nil {
			f.TransferError(err)
		}
		return ret, err
	}
	if f.GetType() == common.TransferDownload {
		readOffset := f.startOffset + f.BytesSent.Load()
		if offset == 0 && readOffset == 0 {
			if whence == io.SeekStart {
				return 0, nil
			} else if whence == io.SeekEnd {
				if err := f.updateStatInfo(); err != nil {
					return 0, err
				}
				return f.info.Size(), nil
			}
		}

		// close the reader and create a new one at startByte
		if f.reader != nil {
			f.reader.Close() //nolint:errcheck
			f.reader = nil
		}
		startByte := int64(0)
		f.BytesReceived.Store(0)
		f.BytesSent.Store(0)
		f.updateTransferQuotaOnSeek()

		switch whence {
		case io.SeekStart:
			startByte = offset
		case io.SeekCurrent:
			startByte = readOffset + offset
		case io.SeekEnd:
			if err := f.updateStatInfo(); err != nil {
				f.TransferError(err)
				return 0, err
			}
			startByte = f.info.Size() - offset
		}

		_, r, cancelFn, err := f.Fs.Open(f.GetFsPath(), startByte)

		f.Lock()
		if err == nil {
			f.startOffset = startByte
			f.reader = r
		}
		f.ErrTransfer = err
		f.BaseTransfer.SetCancelFn(cancelFn)
		f.Unlock()

		return startByte, err
	}
	return 0, common.ErrOpUnsupported
}

// Close closes the open directory or the current transfer
func (f *webDavFile) Close() error {
	if err := f.setFinished(); err != nil {
		return err
	}
	err := f.closeIO()
	if f.isTransfer() {
		errBaseClose := f.BaseTransfer.Close()
		if errBaseClose != nil {
			err = errBaseClose
		}
	} else {
		f.Connection.RemoveTransfer(f.BaseTransfer)
	}
	return f.Connection.GetFsError(f.Fs, err)
}

func (f *webDavFile) closeIO() error {
	var err error
	if f.File != nil {
		err = f.File.Close()
	} else if f.writer != nil {
		err = f.writer.Close()
		f.Lock()
		// we set ErrTransfer here so quota is not updated, in this case the uploads are atomic
		if err != nil && f.ErrTransfer == nil {
			f.ErrTransfer = err
		}
		f.Unlock()
	} else if f.reader != nil {
		err = f.reader.Close()
	}
	return err
}

func (f *webDavFile) setFinished() error {
	f.Lock()
	defer f.Unlock()

	if f.isFinished {
		return common.ErrTransferClosed
	}
	f.isFinished = true
	return nil
}

func (f *webDavFile) isTransfer() bool {
	if f.GetType() == common.TransferDownload {
		return f.readTryed.Load()
	}
	return true
}

// DeadProps returns a copy of the dead properties held.
// We always return nil for now, we only support the last modification time
// and it is already included in "live" properties
func (f *webDavFile) DeadProps() (map[xml.Name]webdav.Property, error) {
	return nil, nil
}

// Patch patches the dead properties held.
// In our minimal implementation we just support Win32LastModifiedTime and
// getlastmodified to set the the modification time.
// We ignore any other property and just return an OK response if the patch sets
// the modification time, otherwise a Forbidden response
func (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error) {
	resp := make([]webdav.Propstat, 0, len(patches))
	hasError := false
	for _, patch := range patches {
		status := http.StatusForbidden
		pstat := webdav.Propstat{}
		for _, p := range patch.Props {
			if status == http.StatusForbidden && !hasError {
				if !patch.Remove && util.Contains(lastModifiedProps, p.XMLName.Local) {
					parsed, err := http.ParseTime(string(p.InnerXML))
					if err != nil {
						f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",
							string(p.InnerXML), err)
						hasError = true
						continue
					}
					attrs := &common.StatAttributes{
						Flags: common.StatAttrTimes,
						Atime: parsed,
						Mtime: parsed,
					}
					if err := f.Connection.SetStat(f.GetVirtualPath(), attrs); err != nil {
						f.Connection.Log(logger.LevelWarn, "unable to set modification time for %q, err :%v",
							f.GetVirtualPath(), err)
						hasError = true
						continue
					}
					status = http.StatusOK
				}
			}
			pstat.Props = append(pstat.Props, webdav.Property{XMLName: p.XMLName})
		}
		pstat.Status = status
		resp = append(resp, pstat)
	}
	return resp, nil
}
