package api

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/aptly-dev/aptly/utils"
	"github.com/gin-gonic/gin"
	"github.com/saracen/walker"
)

// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
// In production it calls (*os.File).Sync().
var syncFile = func(f *os.File) error { return f.Sync() }

func verifyPath(path string) bool {
	path = filepath.Clean(path)
	for _, part := range strings.Split(path, string(filepath.Separator)) {
		if part == ".." || part == "." {
			return false
		}
	}

	return true

}

func verifyDir(c *gin.Context) bool {
	if !verifyPath(c.Params.ByName("dir")) {
		AbortWithJSONError(c, 400, fmt.Errorf("wrong dir"))
		return false
	}

	return true
}

// @Summary List Directories
// @Description **Get list of upload directories**
// @Description
// @Description **Example:**
// @Description  ```
// @Description  $ curl http://localhost:8080/api/files
// @Description  ["aptly-0.9"]
// @Description  ```
// @Tags Files
// @Produce json
// @Success 200 {array} string "List of files"
// @Router /api/files [get]
func apiFilesListDirs(c *gin.Context) {
	list := []string{}
	listLock := &sync.Mutex{}

	err := walker.Walk(context.UploadPath(), func(path string, info os.FileInfo) error {
		if path == context.UploadPath() {
			return nil
		}

		if info.IsDir() {
			listLock.Lock()
			defer listLock.Unlock()
			list = append(list, filepath.Base(path))
			return filepath.SkipDir
		}

		return nil
	})

	if err != nil && !os.IsNotExist(err) {
		AbortWithJSONError(c, 400, err)
		return
	}

	c.JSON(200, list)
}

// @Summary Upload Files
// @Description **Upload files to a directory**
// @Description
// @Description - one or more files can be uploaded
// @Description - existing uploaded are overwritten
// @Description
// @Description **Example:**
// @Description  ```
// @Description $ curl -X POST -F file=@aptly_0.9~dev+217+ge5d646c_i386.deb http://localhost:8080/api/files/aptly-0.9
// @Description ["aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb"]
// @Description  ```
// @Tags Files
// @Accept multipart/form-data
// @Param dir path string true "Directory to upload files to. Created if does not exist"
// @Param files formData file true "Files to upload"
// @Produce json
// @Success 200 {array} string "list of uploaded files"
// @Failure 400 {object} Error "Bad Request"
// @Failure 404 {object} Error "Not Found"
// @Failure 500 {object} Error "Internal Server Error"
// @Router /api/files/{dir} [post]
func apiFilesUpload(c *gin.Context) {
	if !verifyDir(c) {
		return
	}

	path := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
	err := os.MkdirAll(path, 0777)

	if err != nil {
		AbortWithJSONError(c, 500, err)
		return
	}

	err = c.Request.ParseMultipartForm(10 * 1024 * 1024)
	if err != nil {
		AbortWithJSONError(c, 400, err)
		return
	}

	stored := []string{}
	openFiles := []*os.File{}

	// Write all files first
	for _, files := range c.Request.MultipartForm.File {
		for _, file := range files {
			src, err := file.Open()
			if err != nil {
				// Close any files we've opened
				for _, f := range openFiles {
					_ = f.Close()
				}
				AbortWithJSONError(c, 500, err)
				return
			}

			destPath := filepath.Join(path, filepath.Base(file.Filename))
			dst, err := os.Create(destPath)
			if err != nil {
				_ = src.Close()
				// Close any files we've opened
				for _, f := range openFiles {
					_ = f.Close()
				}
				AbortWithJSONError(c, 500, err)
				return
			}

			_, err = io.Copy(dst, src)
			_ = src.Close()
			if err != nil {
				_ = dst.Close()
				// Close any files we've opened
				for _, f := range openFiles {
					_ = f.Close()
				}
				AbortWithJSONError(c, 500, err)
				return
			}

			// Keep file open for batch sync
			openFiles = append(openFiles, dst)
			stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
		}
	}

	// Sync all files at once to catch ENOSPC errors
	for i, dst := range openFiles {
		err := syncFile(dst)
		if err != nil {
			// Close all files
			for _, f := range openFiles {
				_ = f.Close()
			}
			AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", stored[i], err))
			return
		}
	}

	// Close all files
	for _, dst := range openFiles {
		_ = dst.Close()
	}

	apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
	c.JSON(200, stored)
}

// @Summary List Files
// @Description **Show uploaded files in upload directory**
// @Description
// @Description **Example:**
// @Description  ```
// @Description $ curl http://localhost:8080/api/files/aptly-0.9
// @Description ["aptly_0.9~dev+217+ge5d646c_i386.deb"]
// @Description  ```
// @Tags Files
// @Produce json
// @Param dir path string true "Directory to list"
// @Success 200 {array} string "Files found in directory"
// @Failure 404 {object} Error "Not Found"
// @Failure 500 {object} Error "Internal Server Error"
// @Router /api/files/{dir} [get]
func apiFilesListFiles(c *gin.Context) {
	if !verifyDir(c) {
		return
	}

	list := []string{}
	listLock := &sync.Mutex{}
	root := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))

	err := filepath.Walk(root, func(path string, _ os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if path == root {
			return nil
		}

		listLock.Lock()
		defer listLock.Unlock()
		list = append(list, filepath.Base(path))

		return nil
	})

	if err != nil {
		if os.IsNotExist(err) {
			AbortWithJSONError(c, 404, err)
		} else {
			AbortWithJSONError(c, 500, err)
		}
		return
	}

	c.JSON(200, list)
}

// @Summary Delete Directory
// @Description **Delete upload directory and uploaded files within**
// @Description
// @Description **Example:**
// @Description  ```
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9
// @Description {}
// @Description  ```
// @Tags Files
// @Produce json
// @Param dir path string true "Directory"
// @Success 200 {object} string "msg"
// @Failure 500 {object} Error "Internal Server Error"
// @Router /api/files/{dir} [delete]
func apiFilesDeleteDir(c *gin.Context) {
	if !verifyDir(c) {
		return
	}

	err := os.RemoveAll(filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir"))))
	if err != nil {
		AbortWithJSONError(c, 500, err)
		return
	}

	c.JSON(200, gin.H{})
}

// @Summary Delete File
// @Description **Delete a uploaded file in upload directory**
// @Description
// @Description **Example:**
// @Description  ```
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb
// @Description {}
// @Description  ```
// @Tags Files
// @Produce json
// @Param dir path string true "Directory to delete from"
// @Param name path string true "File to delete"
// @Success 200 {object} string "msg"
// @Failure 400 {object} Error "Bad Request"
// @Failure 500 {object} Error "Internal Server Error"
// @Router /api/files/{dir}/{name} [delete]
func apiFilesDeleteFile(c *gin.Context) {
	if !verifyDir(c) {
		return
	}

	dir := utils.SanitizePath(c.Params.ByName("dir"))
	name := utils.SanitizePath(c.Params.ByName("name"))
	if !verifyPath(name) {
		AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
		return
	}

	err := os.Remove(filepath.Join(context.UploadPath(), dir, name))
	if err != nil {
		if err1, ok := err.(*os.PathError); !ok || !os.IsNotExist(err1.Err) {
			AbortWithJSONError(c, 500, err)
			return
		}
	}

	c.JSON(200, gin.H{})
}
