package photosapi

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

const (
	MaxUploadPartSize = 4 << 20 // 4MB
)

var (
	ErrUploadNotExists       = errors.New("upload id doesn't exists")
	ErrUploadPartTooLarge    = fmt.Errorf("upload part too large (> %d B)", MaxUploadPartSize)
	ErrUploadPartWrongSha256 = errors.New("upload part wrong sha256")
)

// Model
type File struct {
	ID        uint32       `gorm:"primary_key" json:"id"`
	Name      string       `gorm:"not null" json:"name"`
	Checksum  string       `gorm:"unique;size:44;not null"`
	Size      uint64       `gorm:"not null"`
	Author    *Account     `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE" json:"author"`
	AuthorId  *uint32      `json:"-"`
	Chunks    []*FileChunk `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"`
	CreatedAt time.Time    `json:"created_at"`
	UpdatedAt time.Time    `json:"updated_at"`
}

type FileChunk struct {
	FileId    uint32
	File      *File `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"`
	Part      uint32
	Checksum  string    `gorm:"unique;size:44;not null"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

func (s *Service) UploadCreate(c *gin.Context) {
	sha, err := uuid.NewRandom()
	if err != nil {
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	if err := s.StorageTmp.Create(sha.String()); err != nil {
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"upload_id": sha.String(),
	})
}

type UploadUri struct {
	Id string `uri:"upload_id" binding:"required,uuid"`
}

type UploadPartQuery struct {
	Part       uint   `form:"part" binding:"required"`
	PartSha256 string `form:"sha256" binding:"required,sha256"`
}

func (s *Service) UploadPart(c *gin.Context) {
	var (
		upload     UploadUri
		uploadPart UploadPartQuery
	)

	if c.BindUri(&upload) != nil || c.BindQuery(&uploadPart) != nil {
		return
	}

	if !s.StorageTmp.Exists(upload.Id) {
		c.AbortWithError(http.StatusNotFound, ErrUploadNotExists)
		return
	}

	if c.Request.ContentLength > MaxUploadPartSize {
		c.AbortWithError(http.StatusRequestEntityTooLarge, ErrUploadPartTooLarge)
		return
	}

	tmp_file := s.StorageTmp.Join(upload.Id, fmt.Sprintf("._tmp_%d", uploadPart.Part))
	file := s.StorageTmp.Join(upload.Id, fmt.Sprint(uploadPart.Part))

	f, err := os.Create(tmp_file)
	if err != nil {
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	sha := sha256.New()
	t := io.TeeReader(c.Request.Body, sha)
	w, err := io.Copy(f, t)
	if err != nil {
		f.Close()
		os.Remove(tmp_file)
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	f.Close()

	shastr := hex.EncodeToString(sha.Sum(nil))
	if shastr != uploadPart.PartSha256 {
		os.Remove(tmp_file)
		c.AbortWithError(http.StatusBadRequest, ErrUploadPartWrongSha256)
		return
	}

	if err = os.Rename(tmp_file, file); err != nil {
		os.Remove(tmp_file)
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"upload_id": upload.Id,
		"part":      uploadPart.Part,
		"size":      w,
		"sha256":    uploadPart.PartSha256,
	})
}

func (s *Service) UploadCancel(c *gin.Context) {
	var upload UploadUri
	if c.BindUri(&upload) != nil {
		return
	}

	if err := s.StorageTmp.Delete(upload.Id); err != nil {
		c.AbortWithError(http.StatusNotFound, ErrUploadNotExists)
		return
	}

	c.Status(http.StatusNoContent)
}

type UploadCompleteRequest struct {
	Sha256 string `json:"sha256" binding:"required,sha256"`
	Name   string `json:"name" binding:"required"`
	Parts  uint   `json:"parts" binding:"required"`
}

func (s *Service) UploadComplete(c *gin.Context) {
	var (
		upload                UploadUri
		uploadCompleteRequest UploadCompleteRequest
	)
	if c.BindUri(&upload) != nil || c.BindJSON(&uploadCompleteRequest) != nil {
		return
	}

	if !s.StorageTmp.Exists(upload.Id) {
		c.AbortWithError(http.StatusNotFound, ErrUploadNotExists)
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"sha256": uploadCompleteRequest.Sha256,
		"parts":  uploadCompleteRequest.Parts,
		"name":   uploadCompleteRequest.Name,
	})
}

func (s *Service) UploadInit() {
	upload := s.Gin.Group("/upload")

	// start
	upload.POST("", s.UploadCreate)
	// Cancel
	upload.DELETE("/:upload_id", s.UploadCancel)
	// Add part
	upload.PUT("/:upload_id", s.UploadPart)
	// Complete
	upload.POST("/:upload_id", s.UploadComplete)
}