package sharings

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"

	"github.com/cozy/cozy-stack/model/sharing"
	"github.com/cozy/cozy-stack/model/vfs"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/jsonapi"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/labstack/echo/v4"
)

// RevsDiff is part of the replicator
func RevsDiff(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Sharing was not found: %s", err)
		return wrapErrors(err)
	}
	var changed sharing.Changed
	if err = json.NewDecoder(c.Request().Body).Decode(&changed); err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Changes cannot be bound: %s", err)
		return wrapErrors(err)
	}
	if changed == nil {
		inst.Logger().WithNamespace("replicator").Infof("No changes")
		return echo.NewHTTPError(http.StatusBadRequest)
	}
	missings, err := s.ComputeRevsDiff(inst, changed)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Error on compute: %s", err)
		return wrapErrors(err)
	}
	return c.JSON(http.StatusOK, missings)
}

// BulkDocs is part of the replicator
func BulkDocs(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Sharing was not found: %s", err)
		return wrapErrors(err)
	}
	var docs sharing.DocsByDoctype
	if err = json.NewDecoder(c.Request().Body).Decode(&docs); err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Docs cannot be bound: %s", err)
		return wrapErrors(err)
	}
	if docs == nil {
		inst.Logger().WithNamespace("replicator").Infof("No bulk docs")
		return echo.NewHTTPError(http.StatusBadRequest)
	}
	err = s.ApplyBulkDocs(inst, docs)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Warnf("Error on apply: %s", err)
		return wrapErrors(err)
	}
	return c.JSON(http.StatusOK, []interface{}{})
}

// GetFolder returns informations about a folder
func GetFolder(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Sharing was not found: %s", err)
		return wrapErrors(err)
	}
	member, err := requestMember(c, s)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Member was not found: %s", err)
		return wrapErrors(err)
	}
	folderID := c.Param("id")
	folder, err := s.GetFolder(inst, member, folderID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Folder %s was not found: %s", folderID, err)
		return wrapErrors(err)
	}
	return c.JSON(http.StatusOK, folder)
}

// SyncFile will try to synchronize a file from just its metadata. If it's not
// possible, it will respond with a key that allow to send the content to
// finish the synchronization.
func SyncFile(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Sharing was not found: %s", err)
		return wrapErrors(err)
	}
	var fileDoc sharing.FileDocWithRevisions
	if err = c.Bind(&fileDoc); err != nil {
		inst.Logger().WithNamespace("replicator").Infof("File cannot be bound: %s", err)
		return wrapErrors(err)
	}
	if c.Param("id") != fileDoc.DocID {
		err = errors.New("The identifiers in the URL and in the doc are not the same")
		return jsonapi.InvalidAttribute("id", err)
	}
	key, err := s.SyncFile(inst, &fileDoc)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Error on sync file: %s", err)
		return wrapErrors(err)
	}
	if key == nil {
		return c.NoContent(http.StatusNoContent)
	}
	return c.JSON(http.StatusOK, key)
}

// FileHandler is used to receive a file upload
func FileHandler(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Sharing was not found: %s", err)
		return wrapErrors(err)
	}

	create := func(fs vfs.VFS, newdoc, olddoc *vfs.FileDoc) error {
		file, err := fs.CreateFile(newdoc, olddoc)
		if err != nil {
			return err
		}
		_, err = io.Copy(file, c.Request().Body)
		if cerr := file.Close(); cerr != nil && err == nil {
			return cerr
		}
		return err
	}

	if err := s.HandleFileUpload(inst, c.Param("id"), create); err != nil {
		inst.Logger().WithNamespace("replicator").Infof("Error on file upload: %s", err)
		return wrapErrors(err)
	}
	return c.NoContent(http.StatusNoContent)
}

// ReuploadHandler is used to try sending again files
func ReuploadHandler(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		return wrapErrors(err)
	}
	sharing.PushUploadJob(s, inst)
	return c.NoContent(http.StatusNoContent)
}

// EndInitial is used for ending the initial sync phase of a sharing
func EndInitial(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	sharingID := c.Param("sharing-id")
	s, err := sharing.FindSharing(inst, sharingID)
	if err != nil {
		return wrapErrors(err)
	}
	if err := s.EndInitial(inst); err != nil {
		return wrapErrors(err)
	}
	return c.NoContent(http.StatusNoContent)
}

// replicatorRoutes sets the routing for the replicator
func replicatorRoutes(router *echo.Group) {
	group := router.Group("", checkSharingPermissions)
	group.POST("/:sharing-id/_revs_diff", RevsDiff, checkSharingWritePermissions)
	group.POST("/:sharing-id/_bulk_docs", BulkDocs, checkSharingWritePermissions)
	group.GET("/:sharing-id/io.cozy.files/:id", GetFolder, checkSharingReadPermissions)
	group.PUT("/:sharing-id/io.cozy.files/:id/metadata", SyncFile, checkSharingWritePermissions)
	group.PUT("/:sharing-id/io.cozy.files/:id", FileHandler, checkSharingWritePermissions)
	group.POST("/:sharing-id/reupload", ReuploadHandler, checkSharingReadPermissions)
	group.DELETE("/:sharing-id/initial", EndInitial, checkSharingWritePermissions)
}

func checkSharingReadPermissions(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		sharingID := c.Param("sharing-id")
		requestPerm, err := middlewares.GetPermission(c)
		if err != nil {
			middlewares.GetInstance(c).Logger().WithNamespace("replicator").
				Infof("Invalid permission: %s", err)
			return err
		}
		if !requestPerm.Permissions.AllowID("GET", consts.Sharings, sharingID) {
			middlewares.GetInstance(c).Logger().WithNamespace("replicator").
				Infof("Not allowed (%s)", sharingID)
			return echo.NewHTTPError(http.StatusForbidden)
		}
		return next(c)
	}
}

func checkSharingWritePermissions(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if err := hasSharingWritePermissions(c); err != nil {
			return err
		}
		return next(c)
	}
}

func hasSharingWritePermissions(c echo.Context) error {
	sharingID := c.Param("sharing-id")
	requestPerm, err := middlewares.GetPermission(c)
	if err != nil {
		middlewares.GetInstance(c).Logger().WithNamespace("replicator").
			Infof("Invalid permission: %s", err)
		return err
	}
	if !requestPerm.Permissions.AllowID("POST", consts.Sharings, sharingID) {
		middlewares.GetInstance(c).Logger().WithNamespace("replicator").
			Infof("Not allowed (%s)", sharingID)
		return echo.NewHTTPError(http.StatusForbidden)
	}
	return nil
}

func checkSharingPermissions(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		sharingID := c.Param("sharing-id")
		requestPerm, err := middlewares.GetPermission(c)
		if err != nil {
			middlewares.GetInstance(c).Logger().WithNamespace("replicator").
				Infof("Invalid permission: %s", err)
			return err
		}
		if !requestPerm.Permissions.AllowID("GET", consts.Sharings, sharingID) {
			middlewares.GetInstance(c).Logger().WithNamespace("replicator").
				Infof("Not allowed (%s)", sharingID)
			return echo.NewHTTPError(http.StatusForbidden)
		}
		return next(c)
	}
}

func requestMember(c echo.Context, s *sharing.Sharing) (*sharing.Member, error) {
	requestPerm, err := middlewares.GetPermission(c)
	if err != nil {
		return nil, err
	}
	return s.FindMemberByInboundClientID(requestPerm.SourceID)
}
