package api

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

	"git.sp4ke.com/sp4ke/bit4sat/db"
	"git.sp4ke.com/sp4ke/bit4sat/ln"
	"git.sp4ke.com/sp4ke/bit4sat/storage"
	"git.sp4ke.com/sp4ke/bit4sat/utils"
	"github.com/gin-gonic/gin"
)

type UploadCtrl struct{}

//TODO: tell client to avoid sending duplicate if we already have hash
func (ctrl UploadCtrl) New(c *gin.Context) {
	uploadForm := UploadForm{}

	if err := c.ShouldBindJSON(&uploadForm); err != nil {
		log.Printf("error parsing form: %s", err)
		utils.JSONErr(c, http.StatusNotAcceptable,
			"invalid form")
		return
	}

	// Create unique id
	uploadId, err := storage.GetShortId()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	tx, err := db.DB.Sql.Beginx()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if uploadForm.RequestPayment {
		if _, ok := ln.CurrencyID[uploadForm.PaymentCurrency]; !ok {
			utils.JSONErr(c, http.StatusInternalServerError,
				"currency not handled or recognized")
			return

		}
	}

	// Create new upload
	up := &storage.Upload{}
	up.UploadId = uploadId
	up.AskFee = uploadForm.RequestPayment
	up.AskCurrency = uploadForm.PaymentCurrency
	up.AskAmount = uploadForm.RequestPaymentAmount

	// Generate unique download id
	//
	dlId, err := storage.GetShortId()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}
	up.DownloadId = dlId

	err = up.TxWrite(tx)

	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		db.RollbackTx(tx, nil)
		return
	}

	for _, file := range uploadForm.Files {
		fup := &storage.FileUpload{}
		fup.UploadId = uploadId

		fup.FileName, fup.FileExt = utils.CleanFileName(file.Name)

		fup.FileSize = file.Size
		fup.FileType = file.Type
		fup.SHA256 = file.SHA256

		err := fup.TxWrite(tx)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)
			db.RollbackTx(tx, nil)
			return
		}
	}

	err = tx.Commit()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// Set upload status to new
	err = storage.SetUploadStatus(uploadId, storage.UpNew)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// Upload seems valid, handle new upload procedure
	//
	// Get the upload session
	upSession, err := SessionStore.Get(c.Request, UpSessionKey)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// Set the uploadId as last upload for session
	upSession.AddFlash(uploadId)

	err = upSession.Save(c.Request, c.Writer)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// If not free upload generate a an invoice
	// TODO: check if upload fee is free
	if uploadForm.RequestPayment {

		invoiceOpts := ln.InvoiceOpts{
			Amount: uploadForm.RequestPaymentAmount,
			Curr:   ln.CurrencyID[uploadForm.PaymentCurrency],
			Memo:   fmt.Sprintf("bit4sat upload: %s", uploadId),
		}
		invoice, err := ln.NewInvoice(invoiceOpts)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)
			return
		}

		// Update Upload Invoice
		err = storage.SetUploadInvoice(uploadId, invoice)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)
			return

		}

		// Start watching upload payment status for this client

		//err = WatchUploadPayment(invoice)
		//if err != nil {
		//log.Println("error watching payment ", err)
		//utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		//return
		//}

		c.JSON(http.StatusOK, gin.H{
			"status":    storage.UpNew,
			"invoice":   invoice,
			"upload_id": uploadId,
		})

		return

		// Handle free uploads
	} else {

		log.Println("new upload created")
		c.JSON(http.StatusOK, gin.H{
			"status": "ready for upload",
			"result": gin.H{
				"id": uploadId,
			},
		})

	}
}

func (ctrl UploadCtrl) CheckStatus(c *gin.Context) {
	uploadId := c.Param("id")

	exists, err := storage.IdExists(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if !exists {
		utils.JSONErr(c, http.StatusNotFound, "upload id not found")
		return
	}

	// Get upload status
	uploadStatus, err := storage.GetUploadStatus(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if uploadStatus.IsNew() {
		c.JSON(http.StatusOK, gin.H{
			"status": uploadStatus,
		})
		return
	}

	// Get invoice id for upload
	invoiceId, err := storage.GetUploadIdInvoiceId(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// No invoice found
	if invoiceId == "" {
		utils.JSONErr(c, http.StatusNotFound, "no invoice found")
		return
	}

	// Get invoice
	invoice, err := ln.CheckInvoice(invoiceId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	c.JSON(ln.InvoiceHttpCode(invoice), gin.H{
		"upload_id": uploadId,
		"invoice":   invoice,
		"status":    uploadStatus,
	})

}

func (ctrl UploadCtrl) PollStatus(c *gin.Context) {
	uploadId := c.Param("id")

	exists, err := storage.IdExists(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if !exists {
		utils.JSONErr(c, http.StatusNotFound, "id not found")
		return
	}

	uploadStatus, err := storage.GetUploadStatus(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if uploadStatus&storage.UpNew != 0 {
		c.JSON(http.StatusCreated, gin.H{
			"status": uploadStatus,
		})
		return
	}

	// we are not in UpNew state so we should have an invoice
	invoice, err := storage.GetUploadInvoice(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if invoice == nil {
		utils.JSONErr(c, http.StatusInternalServerError, "no invoice found")
		return
	}

	if uploadStatus.WaitPay() {

		invoiceChan := make(chan *ln.Invoice)
		errorChan := make(chan error)

		log.Printf("starting invoice poll for %s", invoice.RHash)
		// If waiting payment, wait until invoice is paid
		go ln.PollPaidInvoice(invoice.RHash, invoiceChan, errorChan)

		// Block until payment done or error
		log.Println("blocking")
		select {

		case invoice := <-invoiceChan:
			log.Printf("poll: received invoice notif %s", invoice.RHash)

			////////////////
			///// Invoice was paid
			////////////////
			//
			log.Println("POLL: invoice was paid")

			uploadStatus |= storage.UpPaid
			err := storage.SetUploadStatus(uploadId, uploadStatus)
			if err != nil {
				log.Printf("Redis error: %s", err)
			}

			// Update Upload Invoice
			err = storage.SetUploadInvoice(uploadId, invoice)
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}

			// Generate admin token and save it
			adminToken, err := storage.GetLongId()
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}

			err = storage.SetUploadAdminToken(uploadId, adminToken)
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}

			// Get the download id
			dlId, err := storage.GetDownloadId(uploadId)
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}

			// Clear the session
			upSession, err := SessionStore.Get(c.Request, UpSessionKey)
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}
			upSession.Flashes()
			err = upSession.Save(c.Request, c.Writer)
			if err != nil {
				utils.JSONErrPriv(c, http.StatusInternalServerError, err)
				return
			}

			// invoice was paid
			c.JSON(http.StatusAccepted, gin.H{
				"status":      uploadStatus,
				"invoice":     invoice,
				"admin_token": adminToken,
				"download_id": dlId,
			})

			return

		case err := <-errorChan:
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)
			return
		}

		// We stopped waiting for the invoice, it must have expired
		err := storage.SetUploadStatus(uploadId, storage.UpPayExpired)
		if err != nil {
			log.Printf("Redis error: %s", err)
		}

		c.JSON(http.StatusGone, gin.H{
			"status":  uploadStatus,
			"invoice": invoice,
		})

		return

	} else {
		c.JSON(http.StatusOK, gin.H{
			"status":  uploadStatus,
			"invoice": invoice,
		})
	}

	return
}

func (ctrl UploadCtrl) Upload(c *gin.Context) {

	// Check that upload ID exists
	uploadId := c.Param("id")

	exists, err := storage.IdExists(uploadId)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	if !exists {
		utils.JSONErr(c, http.StatusNotFound, "id not found")
		return
	}

	// Check if payment done
	// NOTE: we pre upload and remove the files if the payment was expired
	//uploadStatus, err := GetUploadStatus(uploadId)
	//if err != nil {
	//utils.JSONErrPriv(c, http.StatusInternalServerError, err)
	//return
	//}

	//if uploadStatus < UpWaitingStorage {

	//utils.JSONErr(c, http.StatusPaymentRequired,
	//fmt.Sprintf("invoice not paid"))
	//return
	//}

	form, err := c.MultipartForm()
	if err != nil {
		utils.JSONErr(c, http.StatusNotAcceptable,
			fmt.Sprintf("Could not parse form: %s", err.Error()))
		return
	}

	files := form.File["upload[]"]

	tx, err := db.DB.Sql.Beginx()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	for _, file := range files {
		log.Printf("Handling %s", file.Filename)

		// Get the file's sha256
		hasher := sha256.New()

		formFd, err := file.Open()
		defer formFd.Close()

		if err != nil {
			utils.JSONErr(
				c, http.StatusInternalServerError,
				fmt.Sprintf("Could not read file %s", file.Filename),
			)
			db.RollbackTx(tx, nil)
			return
		}

		_, err = io.Copy(hasher, formFd)
		if err != nil {
			utils.JSONErr(c, http.StatusInternalServerError,
				"")
			db.RollbackTx(tx, nil)
			return
		}

		sum256 := hex.EncodeToString(hasher.Sum(nil))

		// Get the file's metadata from upload table
		up, err := storage.GetByHashID(sum256, uploadId)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusNotFound, err)
			db.RollbackTx(tx, nil)
			return
		}

		log.Printf("Found received file's %s metadata in local database\n", up.FileName)

		// If file already stored for this upload report conflict
		if up.Stored {
			utils.JSONErr(c, http.StatusConflict,
				fmt.Sprintf("%s already uploaded", up.FileName))
			return
		}
		//
		//
		log.Println("Storing file")
		err = storage.StoreFormFile(formFd, up.SHA256+up.FileExt)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)
			db.RollbackTx(tx, nil)
			return
		}

		log.Printf("%s stored at %s", up.FileName,
			storage.GetStoreDestination(up.SHA256+up.FileExt))

		// Setting status to stored
		log.Println("Updating file upload stored status")

		err = up.TxSetFileStored(tx)
		if err != nil {
			utils.JSONErrPriv(c, http.StatusInternalServerError, err)

			// Rollback and remove the file
			db.RollbackTx(tx, func() {
				err := os.Remove(storage.GetStoreDestination(up.SHA256 + up.FileExt))
				if err != nil {
					log.Println(err)
				}
			})
			return
		}
	}

	err = tx.Commit()
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	// Set the whole transaction as stored
	err = storage.SetUploadStatus(uploadId, storage.UpStored)
	if err != nil {
		utils.JSONErrPriv(c, http.StatusInternalServerError, err)
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"status":       http.StatusOK,
		"store_status": storage.UpStored,
		"upload_id":    uploadId,
	})

}
