//
//  rhimportd
//
//  The Radio Helsinki Rivendell Import Daemon
//
//
//  Copyright (C) 2015-2016 Christian Pointner <equinox@helsinki.at>
//
//  This file is part of rhimportd.
//
//  rhimportd is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  any later version.
//
//  rhimportd 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 General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with rhimportd. If not, see <http://www.gnu.org/licenses/>.
//

package main

import (
	"bytes"
	"code.helsinki.at/rhrd-go/rddb"
	"code.helsinki.at/rhrd-go/rhimport"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"
)

type webUploadResponseData struct {
	ResponseCode int    `json:"RESPONSE_CODE"`
	ErrorString  string `json:"ERROR_STRING"`
	SourceFile   string `json:"SOURCE_FILE"`
}

func webUploadErrorResponse(w http.ResponseWriter, code int, errStr string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	encoder := json.NewEncoder(w)
	respdata := webUploadResponseData{code, errStr, ""}
	encoder.Encode(respdata)
}

func webUploadResponse(w http.ResponseWriter, file string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	encoder := json.NewEncoder(w)
	respdata := webUploadResponseData{http.StatusOK, "success", file}
	encoder.Encode(respdata)
}

const (
	webUploadMaxRequestSize = (2 << 30) - 1 // 2GB, (2 << 30) overflows int on 32-bit systems therefore we use 2GB - 1 Byte
)

func webUploadParseForm(w http.ResponseWriter, r *http.Request) (username, password, srcfile string, src *multipart.Part, ok bool) {
	mpr, err := r.MultipartReader()
	if err != nil {
		rhl.Printf("WebUploadHandler: error while parsing multipart-form: %v", err)
		webUploadErrorResponse(w, http.StatusBadRequest, err.Error())
		return
	}

	for {
		p, err := mpr.NextPart()
		if err != nil {
			rhl.Printf("WebUploadHandler: error while parsing multipart-form: %v", err)
			webUploadErrorResponse(w, http.StatusBadRequest, err.Error())
			return
		}
		var dstfield *string
		switch p.FormName() {
		case "LOGIN_NAME":
			dstfield = &username
		case "PASSWORD":
			dstfield = &password
		case "FILENAME":
			srcfile = p.FileName()
			src = p
			ok = true
			return
		default:
			rhl.Printf("WebUploadHandler: unknown form field: '%s'", p.FormName())
			webUploadErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("unknown field %s", p.FormName()))
			return
		}

		var buf bytes.Buffer
		if _, err := io.CopyN(&buf, p, 1<<10); err != nil && err != io.EOF { // 1kB should be enough
			rhl.Printf("WebUploadHandler: error while extracting form field: %v", err)
			webUploadErrorResponse(w, http.StatusBadRequest, err.Error())
			return
		}
		*dstfield = buf.String()
		p.Close()
	}
}

func webUploadHandler(conf *rhimport.Config, db *rddb.DBChan, sessions *rhimport.SessionStoreChan, trusted bool, w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		http.Redirect(w, r, "/static/upload-form.html", http.StatusTemporaryRedirect)
		return
	}

	if r.Method != "POST" {
		rhl.Printf("WebUploadHandler: got invalid request method")
		webUploadErrorResponse(w, http.StatusMethodNotAllowed, "only POST and GET method are allowed")
		return
	}

	// This is from: stackoverflow.com/questions/26392196
	if r.ContentLength > webUploadMaxRequestSize {
		rhl.Printf("WebUploadHandler: request is too large: %d > %d", r.ContentLength, webUploadMaxRequestSize)
		webUploadErrorResponse(w, http.StatusExpectationFailed, "request too large")
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, webUploadMaxRequestSize)

	username, password, srcfile, src, ok := webUploadParseForm(w, r)
	if !ok {
		return
	}
	if username == "" {
		webUploadErrorResponse(w, http.StatusBadRequest, "missing field LOGIN_NAME")
		return
	}
	if password == "" {
		webUploadErrorResponse(w, http.StatusBadRequest, "missing field PASSWORD")
		return
	}

	if authenticated, err := db.CheckPassword(username, password); err != nil {
		rhl.Printf("WebUploadHandler: error checking username/password: %v", err)
		webUploadErrorResponse(w, http.StatusUnauthorized, err.Error())
		return
	} else if !authenticated {
		rhl.Printf("WebUploadHandler: invalid username/password")
		webUploadErrorResponse(w, http.StatusUnauthorized, "invalid username/password")
		return
	}

	rhdl.Printf("WebUploadHandler: got request from user '%s', filename='%s'", username, srcfile)

	dstpath, err := ioutil.TempDir(conf.TempDir, "webupload-"+username+"-")
	if err != nil {
		rhl.Printf("WebUploadHandler: error creating temporary directory: %v", err)
		webUploadErrorResponse(w, http.StatusInternalServerError, err.Error())
		return
	}

	dstfile := filepath.Join(dstpath, path.Clean("/"+srcfile))
	dst, err := os.OpenFile(dstfile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		rhl.Printf("WebUploadHandler: Unable to create file '%s': %v", dstfile, err)
		webUploadErrorResponse(w, http.StatusInternalServerError, err.Error())
		return
	}
	defer dst.Close()

	size, err := io.Copy(dst, src)
	if err != nil {
		rhl.Printf("WebUploadHandler: error storing uploaded file '%s': %v", dstfile, err)
		webUploadErrorResponse(w, http.StatusInternalServerError, err.Error())
		return
	}
	rhl.Printf("WebUploadHandler: stored file '%s' (size: %d Bytes)", dstfile, size)
	webUploadResponse(w, "tmp://"+strings.TrimPrefix(dstfile, conf.TempDir))
}

func webUploadCleanerRun(dir *os.File, conf *rhimport.Config, maxAge time.Duration) {
	t := time.NewTicker(1 * time.Minute)
	defer t.Stop()
	for now := range t.C {
		var err error
		if _, err = dir.Seek(0, 0); err != nil {
			rhl.Printf("webUploadCleaner: reading directory contents failed: %s", err)
			return
		}

		var entries []os.FileInfo
		if entries, err = dir.Readdir(0); err != nil {
			rhl.Printf("webUploadCleaner: reading directory contents failed: %s", err)
			return
		}

		for _, entry := range entries {
			if !strings.HasPrefix(entry.Name(), "webupload-") {
				continue
			}

			age := now.Sub(entry.ModTime())
			if age <= maxAge {
				continue
			}

			if err := os.RemoveAll(conf.TempDir + "/" + entry.Name()); err != nil {
				rhl.Printf("webUploadCleaner: deleting dir '%s' failed: %v", entry.Name(), err)
				continue
			}
			rhl.Printf("webUploadCleaner: successfully deleted dir '%s', Age: %v", entry.Name(), age)
		}
	}
}

func webUploadCleaner(conf *rhimport.Config, maxAge time.Duration) {
	first := true
	for {
		if !first {
			time.Sleep(5 * time.Second)
		}
		first = false

		dir, err := os.Open(conf.TempDir)
		if err != nil {
			rhl.Printf("webUploadCleaner: %s", err)
			continue
		}
		if i, err := dir.Stat(); err != nil {
			rhl.Printf("webUploadCleaner: %s", err)
			dir.Close()
			continue
		} else {
			if !i.IsDir() {
				rhl.Printf("webUploadCleaner: %s is not a directory", conf.TempDir)
				dir.Close()
				continue
			}
		}
		rhdl.Printf("webUploadCleaner: running with max age: %v", maxAge)
		webUploadCleanerRun(dir, conf, maxAge)
		rhdl.Printf("webUploadCleanerRun: returned - restarting in 5 sec...")
		dir.Close()
	}
}